u-foo 2.2.3 → 2.3.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/SKILLS/ufoo/SKILL.md +56 -12
- package/SKILLS/uinit/SKILL.md +3 -2
- package/modules/AGENTS.template.md +2 -1
- package/modules/bus/README.md +1 -1
- package/modules/context/SKILLS/uctx/SKILL.md +6 -4
- package/package.json +1 -1
- package/src/agent/codexThreadProvider.js +2 -2
- package/src/agent/controllerToolExecutor.js +24 -1
- package/src/agent/credentials/claude.js +85 -16
- package/src/agent/credentials/codex.js +251 -23
- package/src/agent/defaultBootstrap.js +3 -1
- package/src/agent/directAuthStatus.js +264 -0
- package/src/agent/internalRunner.js +18 -12
- package/src/agent/loopObservability.js +10 -0
- package/src/agent/loopRuntime.js +19 -0
- package/src/agent/ufooAgent.js +43 -13
- package/src/agent/upstreamTransport.js +23 -8
- package/src/bus/index.js +13 -5
- package/src/bus/message.js +157 -9
- package/src/bus/nickname.js +14 -3
- package/src/bus/subscriber.js +30 -10
- package/src/chat/agentDirectory.js +2 -2
- package/src/chat/commandExecutor.js +190 -8
- package/src/chat/commands.js +23 -4
- package/src/chat/completionController.js +30 -7
- package/src/chat/daemonMessageRouter.js +2 -1
- package/src/chat/index.js +9 -8
- package/src/cli/groupCoreCommands.js +5 -0
- package/src/cli.js +309 -0
- package/src/code/UCODE_PROMPT.md +3 -2
- package/src/code/nativeRunner.js +2 -1
- package/src/code/prompts/ufoo.js +3 -2
- package/src/config.js +16 -3
- package/src/context/doctor.js +1 -1
- package/src/daemon/groupOrchestrator.js +13 -9
- package/src/daemon/index.js +35 -18
- package/src/daemon/nicknameScope.js +37 -0
- package/src/daemon/ops.js +22 -10
- package/src/daemon/promptRequest.js +11 -2
- package/src/daemon/reporting.js +2 -3
- package/src/daemon/soloBootstrap.js +15 -3
- package/src/daemon/status.js +5 -1
- package/src/group/bootstrap.js +1 -1
- package/src/group/promptProfiles.js +106 -22
- package/src/group/templates.js +1 -0
- package/src/init/index.js +4 -0
- package/src/memory/historySearch.js +308 -0
- package/src/memory/index.js +653 -8
- package/src/providerapi/redactor.js +4 -1
- package/src/status/index.js +26 -2
- package/src/tools/handlers/memory.js +168 -0
- package/src/tools/index.js +12 -0
- package/src/tools/registry.js +12 -0
- package/src/tools/schemaFixtures.js +213 -0
- package/src/tools/tier1/editMemory.js +14 -0
- package/src/tools/tier1/forget.js +14 -0
- package/src/tools/tier1/recall.js +14 -0
- package/src/tools/tier1/remember.js +14 -0
- package/src/tools/tier1/searchHistory.js +14 -0
- package/src/tools/tier1/searchMemory.js +14 -0
- package/templates/groups/build-lane.json +44 -6
- package/templates/groups/build-ultra.json +6 -5
- package/templates/groups/design-system.json +84 -0
- package/templates/groups/product-discovery.json +9 -4
- package/templates/groups/ui-plan-review.json +84 -0
- package/templates/groups/ui-polish.json +6 -2
- package/templates/groups/verify-ship.json +9 -4
|
@@ -135,6 +135,10 @@ function readRecentLoopSummary(projectRoot, options = {}) {
|
|
|
135
135
|
output_tokens: 0,
|
|
136
136
|
cache_read_tokens: 0,
|
|
137
137
|
cache_creation_tokens: 0,
|
|
138
|
+
cache_semistatic_hit: 0,
|
|
139
|
+
cache_semistatic_miss: 0,
|
|
140
|
+
memory_prefix_tokens: 0,
|
|
141
|
+
dynamic_memory_tokens: 0,
|
|
138
142
|
total_tokens: 0,
|
|
139
143
|
total_latency_ms: 0,
|
|
140
144
|
first_token_ms: 0,
|
|
@@ -154,18 +158,24 @@ function readRecentLoopSummary(projectRoot, options = {}) {
|
|
|
154
158
|
summary.output_tokens += Number(row.output_tokens) || 0;
|
|
155
159
|
summary.cache_read_tokens += Number(row.cache_read_tokens) || 0;
|
|
156
160
|
summary.cache_creation_tokens += Number(row.cache_creation_tokens) || 0;
|
|
161
|
+
summary.cache_semistatic_hit += Number(row.cache_semistatic_hit) || 0;
|
|
162
|
+
summary.cache_semistatic_miss += Number(row.cache_semistatic_miss) || 0;
|
|
163
|
+
summary.memory_prefix_tokens += Number(row.memory_prefix_tokens) || 0;
|
|
164
|
+
summary.dynamic_memory_tokens += Number(row.dynamic_memory_tokens) || 0;
|
|
157
165
|
summary.total_latency_ms += Number(row.latency_ms) || 0;
|
|
158
166
|
summary.first_token_ms += Number(row.first_token_ms) || 0;
|
|
159
167
|
} else if (row.event === "tool_call") {
|
|
160
168
|
summary.tool_calls += 1;
|
|
161
169
|
const name = String(row.tool_name || "").trim() || "unknown";
|
|
162
170
|
toolCounts.set(name, (toolCounts.get(name) || 0) + 1);
|
|
171
|
+
summary.dynamic_memory_tokens += Number(row.dynamic_memory_tokens) || 0;
|
|
163
172
|
} else if (row.event === "loop_terminal") {
|
|
164
173
|
summary.terminal_reason = String(row.terminal_reason || "").trim();
|
|
165
174
|
if ((Number(row.rounds) || 0) > 0) summary.rounds = Number(row.rounds) || summary.rounds;
|
|
166
175
|
if ((Number(row.tool_calls) || 0) >= 0) summary.tool_calls = Number(row.tool_calls) || summary.tool_calls;
|
|
167
176
|
if ((Number(row.total_tokens) || 0) > 0) summary.total_tokens = Number(row.total_tokens) || 0;
|
|
168
177
|
if ((Number(row.total_latency_ms) || 0) > 0) summary.total_latency_ms = Number(row.total_latency_ms) || 0;
|
|
178
|
+
if ((Number(row.dynamic_memory_tokens) || 0) > 0) summary.dynamic_memory_tokens = Number(row.dynamic_memory_tokens) || summary.dynamic_memory_tokens;
|
|
169
179
|
}
|
|
170
180
|
}
|
|
171
181
|
|
package/src/agent/loopRuntime.js
CHANGED
|
@@ -58,6 +58,10 @@ function extractModelMetrics(result) {
|
|
|
58
58
|
output_tokens: toNonNegativeInt(source.output_tokens),
|
|
59
59
|
cache_read_tokens: toNonNegativeInt(source.cache_read_tokens),
|
|
60
60
|
cache_creation_tokens: toNonNegativeInt(source.cache_creation_tokens),
|
|
61
|
+
cache_semistatic_hit: toNonNegativeInt(source.cache_semistatic_hit),
|
|
62
|
+
cache_semistatic_miss: toNonNegativeInt(source.cache_semistatic_miss),
|
|
63
|
+
memory_prefix_tokens: toNonNegativeInt(source.memory_prefix_tokens),
|
|
64
|
+
dynamic_memory_tokens: toNonNegativeInt(source.dynamic_memory_tokens),
|
|
61
65
|
latency_ms: toNonNegativeInt(source.latency_ms),
|
|
62
66
|
first_token_ms: toNonNegativeInt(source.first_token_ms),
|
|
63
67
|
stop_reason: String(source.stop_reason || "").trim(),
|
|
@@ -168,6 +172,7 @@ function buildTerminalPayload(reason, lastPayload, rounds, toolCalls, toolErrors
|
|
|
168
172
|
fallback_used: normalizeFallbackUsed(totals.fallback_used),
|
|
169
173
|
total_tokens: toNonNegativeInt(totals.total_tokens),
|
|
170
174
|
total_latency_ms: toNonNegativeInt(totals.total_latency_ms),
|
|
175
|
+
dynamic_memory_tokens: toNonNegativeInt(totals.dynamic_memory_tokens),
|
|
171
176
|
};
|
|
172
177
|
return payload;
|
|
173
178
|
}
|
|
@@ -205,6 +210,7 @@ async function runPromptWithControllerLoop({
|
|
|
205
210
|
let toolErrors = 0;
|
|
206
211
|
let totalTokens = 0;
|
|
207
212
|
let totalLatencyMs = 0;
|
|
213
|
+
let dynamicMemoryTokens = 0;
|
|
208
214
|
const toolResults = [];
|
|
209
215
|
|
|
210
216
|
const checkCancellation = () => {
|
|
@@ -220,6 +226,7 @@ async function runPromptWithControllerLoop({
|
|
|
220
226
|
fallback_used: FALLBACK_USED_VALUES.NONE,
|
|
221
227
|
total_tokens: totalTokens,
|
|
222
228
|
total_latency_ms: totalLatencyMs,
|
|
229
|
+
dynamic_memory_tokens: dynamicMemoryTokens,
|
|
223
230
|
});
|
|
224
231
|
|
|
225
232
|
const terminate = (reason, payloadBase, roundsCount) => {
|
|
@@ -304,6 +311,10 @@ async function runPromptWithControllerLoop({
|
|
|
304
311
|
output_tokens: metrics.output_tokens,
|
|
305
312
|
cache_read_tokens: metrics.cache_read_tokens,
|
|
306
313
|
cache_creation_tokens: metrics.cache_creation_tokens,
|
|
314
|
+
cache_semistatic_hit: metrics.cache_semistatic_hit,
|
|
315
|
+
cache_semistatic_miss: metrics.cache_semistatic_miss,
|
|
316
|
+
memory_prefix_tokens: metrics.memory_prefix_tokens,
|
|
317
|
+
dynamic_memory_tokens: metrics.dynamic_memory_tokens,
|
|
307
318
|
latency_ms: modelLatency,
|
|
308
319
|
first_token_ms: metrics.first_token_ms,
|
|
309
320
|
tool_call_count: toolCall ? 1 : 0,
|
|
@@ -339,6 +350,7 @@ async function runPromptWithControllerLoop({
|
|
|
339
350
|
fallback_used: FALLBACK_USED_VALUES.NONE,
|
|
340
351
|
total_tokens: totalTokens,
|
|
341
352
|
total_latency_ms: totalLatencyMs,
|
|
353
|
+
dynamic_memory_tokens: dynamicMemoryTokens,
|
|
342
354
|
},
|
|
343
355
|
};
|
|
344
356
|
observer.emit("loop_terminal", finalPayload.loop);
|
|
@@ -382,13 +394,19 @@ async function runPromptWithControllerLoop({
|
|
|
382
394
|
const toolDuration = Math.max(0, now() - toolStartedAt);
|
|
383
395
|
|
|
384
396
|
let toolResultSize = 0;
|
|
397
|
+
let toolDynamicMemoryTokens = 0;
|
|
385
398
|
try {
|
|
386
399
|
toolResultSize = toolResult && toolResult.result !== undefined
|
|
387
400
|
? JSON.stringify(toolResult.result).length
|
|
388
401
|
: 0;
|
|
402
|
+
toolDynamicMemoryTokens = toolResult && toolResult.result && Number.isFinite(Number(toolResult.result.dynamic_memory_tokens))
|
|
403
|
+
? Math.max(0, Math.floor(Number(toolResult.result.dynamic_memory_tokens)))
|
|
404
|
+
: 0;
|
|
389
405
|
} catch {
|
|
390
406
|
toolResultSize = 0;
|
|
407
|
+
toolDynamicMemoryTokens = 0;
|
|
391
408
|
}
|
|
409
|
+
dynamicMemoryTokens += toolDynamicMemoryTokens;
|
|
392
410
|
|
|
393
411
|
observer.emit("tool_call", {
|
|
394
412
|
round,
|
|
@@ -397,6 +415,7 @@ async function runPromptWithControllerLoop({
|
|
|
397
415
|
turn_id: toolResult && toolResult.turn_id ? String(toolResult.turn_id) : `loop-round-${round}`,
|
|
398
416
|
duration_ms: toolDuration,
|
|
399
417
|
result_size: toolResultSize,
|
|
418
|
+
dynamic_memory_tokens: toolDynamicMemoryTokens,
|
|
400
419
|
retry_count: 0,
|
|
401
420
|
final_status: toolResult && toolResult.ok === true ? "ok" : "error",
|
|
402
421
|
});
|
package/src/agent/ufooAgent.js
CHANGED
|
@@ -5,8 +5,9 @@ const { normalizeCliOutput } = require("./normalizeOutput");
|
|
|
5
5
|
const { buildStatus } = require("../daemon/status");
|
|
6
6
|
const { getUfooPaths } = require("../ufoo/paths");
|
|
7
7
|
const { normalizeGateRouterResult } = require("../controller/gateRouter");
|
|
8
|
-
const { sendUpstreamPrompt } = require("./upstreamTransport");
|
|
8
|
+
const { normalizeProvider, sendUpstreamPrompt } = require("./upstreamTransport");
|
|
9
9
|
const { normalizeAgentTypeAlias } = require("../bus/utils");
|
|
10
|
+
const { buildCachedMemoryPrefix } = require("../memory");
|
|
10
11
|
const { listProjectRuntimes, isGlobalControllerProjectRoot } = require("../projects");
|
|
11
12
|
const {
|
|
12
13
|
CONTROLLER_MODES,
|
|
@@ -522,6 +523,14 @@ function buildSystemPrompt(context, options = {}) {
|
|
|
522
523
|
].join("\n");
|
|
523
524
|
}
|
|
524
525
|
|
|
526
|
+
function buildMemoryPrefixResult(projectRoot, limit = 50) {
|
|
527
|
+
try {
|
|
528
|
+
return buildCachedMemoryPrefix(projectRoot, { limit });
|
|
529
|
+
} catch {
|
|
530
|
+
return { prefix: "", estimated_tokens: 0, cache_hit: false, cache_semistatic_hit: 0, cache_semistatic_miss: 0 };
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
525
534
|
function loadHistory(projectRoot, maxTurns = 6) {
|
|
526
535
|
const file = path.join(getUfooPaths(projectRoot).agentDir, "ufoo-agent.history.jsonl");
|
|
527
536
|
try {
|
|
@@ -594,9 +603,9 @@ function extractNickname(prompt) {
|
|
|
594
603
|
return "";
|
|
595
604
|
}
|
|
596
605
|
|
|
597
|
-
function
|
|
598
|
-
const
|
|
599
|
-
return
|
|
606
|
+
function shouldUseDirectProvider(value = "") {
|
|
607
|
+
const provider = normalizeProvider(value);
|
|
608
|
+
return provider === "ucode" || provider === "codex" || provider === "claude";
|
|
600
609
|
}
|
|
601
610
|
|
|
602
611
|
function stripMarkdownFence(text = "") {
|
|
@@ -644,32 +653,42 @@ async function runUfooAgent({
|
|
|
644
653
|
const bus = routingContext || (mode === "global-router"
|
|
645
654
|
? buildGlobalProjectRouterContext(projectRoot)
|
|
646
655
|
: loadBusSummary(projectRoot));
|
|
647
|
-
|
|
656
|
+
let systemPrompt = buildSystemPrompt(bus, {
|
|
648
657
|
routingMode: mode,
|
|
649
658
|
loopRuntime,
|
|
650
659
|
controllerMode: resolvedControllerMode,
|
|
651
660
|
});
|
|
661
|
+
const memoryPrefixResult = buildMemoryPrefixResult(projectRoot);
|
|
662
|
+
const memoryPrefix = String(memoryPrefixResult.prefix || "").trim();
|
|
663
|
+
if (memoryPrefix) {
|
|
664
|
+
systemPrompt = `${systemPrompt}\n\n${memoryPrefix}`;
|
|
665
|
+
}
|
|
652
666
|
const history = loadHistory(projectRoot);
|
|
653
667
|
const historyPrompt = buildHistoryPrompt(history);
|
|
654
668
|
const fullPrompt = historyPrompt ? `${historyPrompt}User: ${prompt}` : prompt;
|
|
655
669
|
|
|
656
670
|
let res;
|
|
657
671
|
|
|
658
|
-
|
|
659
|
-
|
|
672
|
+
const useDirectProvider = shouldUseDirectProvider(provider);
|
|
673
|
+
let usedDirectProvider = false;
|
|
674
|
+
|
|
675
|
+
if (useDirectProvider) {
|
|
660
676
|
res = await runNativeRouterCall({
|
|
661
677
|
projectRoot,
|
|
662
678
|
prompt: fullPrompt,
|
|
663
679
|
systemPrompt,
|
|
680
|
+
provider,
|
|
664
681
|
model,
|
|
665
682
|
});
|
|
666
683
|
if (!res.ok) {
|
|
667
684
|
return { ok: false, error: res.error };
|
|
685
|
+
} else {
|
|
686
|
+
usedDirectProvider = true;
|
|
687
|
+
res = { ok: true, output: res.output, sessionId: "", provider: res.provider, model: res.model };
|
|
668
688
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
// CLI path: spawn codex/claude binary
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (!useDirectProvider) {
|
|
673
692
|
res = await runCliAgent({
|
|
674
693
|
provider,
|
|
675
694
|
model,
|
|
@@ -700,7 +719,7 @@ async function runUfooAgent({
|
|
|
700
719
|
}
|
|
701
720
|
}
|
|
702
721
|
|
|
703
|
-
const rawText =
|
|
722
|
+
const rawText = usedDirectProvider
|
|
704
723
|
? String(res.output || "").trim()
|
|
705
724
|
: normalizeCliOutput(res.output);
|
|
706
725
|
const text = stripMarkdownFence(rawText);
|
|
@@ -733,7 +752,18 @@ async function runUfooAgent({
|
|
|
733
752
|
|
|
734
753
|
appendHistory(projectRoot, { prompt, reply: payload.reply || "" });
|
|
735
754
|
|
|
736
|
-
return {
|
|
755
|
+
return {
|
|
756
|
+
ok: true,
|
|
757
|
+
payload,
|
|
758
|
+
meta: {
|
|
759
|
+
memory_prefix_tokens: memoryPrefixResult.estimated_tokens || 0,
|
|
760
|
+
cache_semistatic_hit: memoryPrefixResult.cache_semistatic_hit || 0,
|
|
761
|
+
cache_semistatic_miss: memoryPrefixResult.cache_semistatic_miss || 0,
|
|
762
|
+
memory_prefix_truncated: memoryPrefixResult.truncated === true,
|
|
763
|
+
memory_prefix_entries: memoryPrefixResult.entry_count || 0,
|
|
764
|
+
memory_prefix_emitted: memoryPrefixResult.emitted_count || 0,
|
|
765
|
+
},
|
|
766
|
+
};
|
|
737
767
|
}
|
|
738
768
|
|
|
739
769
|
async function runUfooRouteAgent({
|
|
@@ -16,7 +16,7 @@ function normalizeProvider(value = "") {
|
|
|
16
16
|
if (!text) return "ucode";
|
|
17
17
|
if (text === "codex-cli" || text === "codex-code" || text === "codex" || text === "openai") return "codex";
|
|
18
18
|
if (text === "claude-cli" || text === "claude-code" || text === "claude" || text === "anthropic") return "claude";
|
|
19
|
-
if (text === "ucode") return "ucode";
|
|
19
|
+
if (text === "ucode" || text === "ufoo" || text === "ufoo-code") return "ucode";
|
|
20
20
|
return text;
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -200,6 +200,7 @@ async function resolveUpstreamRuntime({
|
|
|
200
200
|
provider = "",
|
|
201
201
|
model = "",
|
|
202
202
|
env = process.env,
|
|
203
|
+
fetchImpl = global.fetch,
|
|
203
204
|
loadConfigImpl = loadConfig,
|
|
204
205
|
} = {}) {
|
|
205
206
|
const normalizedProvider = normalizeProvider(provider);
|
|
@@ -208,6 +209,8 @@ async function resolveUpstreamRuntime({
|
|
|
208
209
|
if (normalizedProvider === "codex") {
|
|
209
210
|
const credential = await resolveCodexUpstreamCredentials({
|
|
210
211
|
authPath: config.codexAuthPath,
|
|
212
|
+
refreshWindowMs: Number(config.codexOauthRefreshWindowSec || 300) * 1000,
|
|
213
|
+
fetchImpl,
|
|
211
214
|
env,
|
|
212
215
|
});
|
|
213
216
|
const useCodexResponses = credential.credentialKind === "oauth" && Boolean(credential.accessToken);
|
|
@@ -408,13 +411,25 @@ async function sendUpstreamPrompt({
|
|
|
408
411
|
env = process.env,
|
|
409
412
|
loadConfigImpl = loadConfig,
|
|
410
413
|
} = {}) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
414
|
+
let runtime;
|
|
415
|
+
try {
|
|
416
|
+
runtime = await resolveUpstreamRuntime({
|
|
417
|
+
projectRoot,
|
|
418
|
+
provider,
|
|
419
|
+
model,
|
|
420
|
+
env,
|
|
421
|
+
fetchImpl,
|
|
422
|
+
loadConfigImpl,
|
|
423
|
+
});
|
|
424
|
+
} catch (err) {
|
|
425
|
+
return {
|
|
426
|
+
ok: false,
|
|
427
|
+
error: err && err.message ? err.message : "upstream runtime resolution failed",
|
|
428
|
+
errorCode: err && err.code ? err.code : "UPSTREAM_RUNTIME_RESOLUTION_FAILED",
|
|
429
|
+
provider: normalizeProvider(provider),
|
|
430
|
+
model: String(model || "").trim(),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
418
433
|
|
|
419
434
|
const requestModel = String(runtime.model || "").trim();
|
|
420
435
|
const request = runtime.transport === "anthropic-messages"
|
package/src/bus/index.js
CHANGED
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
sleep,
|
|
17
17
|
} = require("./utils");
|
|
18
18
|
const { shakeTerminalByTty } = require("./shake");
|
|
19
|
+
const { resolveDisplayNickname } = require("../daemon/nicknameScope");
|
|
19
20
|
const QueueManager = require("./queue");
|
|
20
21
|
const SubscriberManager = require("./subscriber");
|
|
21
22
|
const MessageManager = require("./message");
|
|
@@ -64,7 +65,12 @@ class EventBus {
|
|
|
64
65
|
this.messageManager = new MessageManager(
|
|
65
66
|
this.busDir,
|
|
66
67
|
this.busData,
|
|
67
|
-
this.queueManager
|
|
68
|
+
this.queueManager,
|
|
69
|
+
{
|
|
70
|
+
projectRoot: this.projectRoot,
|
|
71
|
+
enableGroupPolicyHook: true,
|
|
72
|
+
warn: logWarn,
|
|
73
|
+
}
|
|
68
74
|
);
|
|
69
75
|
|
|
70
76
|
// 自动清理不活跃的 agents
|
|
@@ -147,7 +153,8 @@ class EventBus {
|
|
|
147
153
|
if (!sessionId && !agentType && currentSubscriber && currentActive) {
|
|
148
154
|
this.subscriberManager.updateLastSeen(currentSubscriber);
|
|
149
155
|
this.saveBusData();
|
|
150
|
-
const
|
|
156
|
+
const currentDisplayNickname = resolveDisplayNickname(this.projectRoot, currentMeta);
|
|
157
|
+
const currentNickname = currentDisplayNickname ? ` (${currentDisplayNickname})` : "";
|
|
151
158
|
logInfo(`Already joined event bus: ${currentSubscriber}${currentNickname}`);
|
|
152
159
|
return currentSubscriber;
|
|
153
160
|
}
|
|
@@ -201,14 +208,15 @@ class EventBus {
|
|
|
201
208
|
/**
|
|
202
209
|
* 重命名订阅者
|
|
203
210
|
*/
|
|
204
|
-
async rename(subscriber, newNickname, publisher = null) {
|
|
211
|
+
async rename(subscriber, newNickname, publisher = null, options = {}) {
|
|
205
212
|
this.ensureBus();
|
|
206
213
|
this.loadBusData();
|
|
207
214
|
|
|
208
215
|
try {
|
|
209
216
|
const result = await this.subscriberManager.rename(
|
|
210
217
|
subscriber,
|
|
211
|
-
newNickname
|
|
218
|
+
newNickname,
|
|
219
|
+
options
|
|
212
220
|
);
|
|
213
221
|
this.saveBusData();
|
|
214
222
|
const pub = publisher || this.getDefaultPublisher() || "unknown";
|
|
@@ -468,7 +476,7 @@ class EventBus {
|
|
|
468
476
|
console.log(" (none)");
|
|
469
477
|
} else {
|
|
470
478
|
for (const sub of active) {
|
|
471
|
-
|
|
479
|
+
const nickname = sub.nickname ? ` (${sub.nickname})` : "";
|
|
472
480
|
console.log(` ${sub.id}${nickname}`);
|
|
473
481
|
}
|
|
474
482
|
}
|
package/src/bus/message.js
CHANGED
|
@@ -8,9 +8,11 @@ const {
|
|
|
8
8
|
readLastLine,
|
|
9
9
|
isPidAlive,
|
|
10
10
|
normalizeAgentTypeAlias,
|
|
11
|
+
logWarn,
|
|
11
12
|
} = require("./utils");
|
|
12
13
|
const NicknameManager = require("./nickname");
|
|
13
14
|
const { buildMessageData } = require("./messageMeta");
|
|
15
|
+
const { getUfooPaths } = require("../ufoo/paths");
|
|
14
16
|
|
|
15
17
|
const SEQ_LOCK_TIMEOUT_MS = 5000;
|
|
16
18
|
const SEQ_LOCK_POLL_MS = 25;
|
|
@@ -20,13 +22,21 @@ const SEQ_LOCK_STALE_MS = 30000;
|
|
|
20
22
|
* 消息管理器
|
|
21
23
|
*/
|
|
22
24
|
class MessageManager {
|
|
23
|
-
constructor(busDir, busData, queueManager) {
|
|
25
|
+
constructor(busDir, busData, queueManager, options = {}) {
|
|
24
26
|
this.busDir = busDir;
|
|
25
27
|
this.busData = busData;
|
|
26
28
|
this.queueManager = queueManager;
|
|
27
29
|
this.eventsDir = path.join(busDir, "events");
|
|
28
30
|
this.seqFile = path.join(busDir, "seq.counter");
|
|
29
31
|
this.seqLockFile = path.join(busDir, "seq.counter.lock");
|
|
32
|
+
this.projectRoot = typeof options.projectRoot === "string" && options.projectRoot.trim()
|
|
33
|
+
? options.projectRoot
|
|
34
|
+
: "";
|
|
35
|
+
this.warn = typeof options.warn === "function" ? options.warn : logWarn;
|
|
36
|
+
this.preSendHooks = Array.isArray(options.preSendHooks) ? options.preSendHooks.slice() : [];
|
|
37
|
+
if (options.enableGroupPolicyHook === true && this.projectRoot) {
|
|
38
|
+
this.preSendHooks.push((context) => this.checkGroupSoftPolicy(context));
|
|
39
|
+
}
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
/**
|
|
@@ -247,7 +257,7 @@ class MessageManager {
|
|
|
247
257
|
if (meta && normalizedTarget === normalizeAgentTypeAlias(meta.agent_type)) return true;
|
|
248
258
|
|
|
249
259
|
// 昵称匹配
|
|
250
|
-
if (meta && target === meta.nickname) return true;
|
|
260
|
+
if (meta && (target === meta.nickname || target === meta.scoped_nickname)) return true;
|
|
251
261
|
|
|
252
262
|
// 通配符
|
|
253
263
|
if (target === "*") return true;
|
|
@@ -255,6 +265,131 @@ class MessageManager {
|
|
|
255
265
|
return false;
|
|
256
266
|
}
|
|
257
267
|
|
|
268
|
+
readActiveGroupStates() {
|
|
269
|
+
if (!this.projectRoot) return [];
|
|
270
|
+
const groupsDir = getUfooPaths(this.projectRoot).groupsDir;
|
|
271
|
+
if (!fs.existsSync(groupsDir)) return [];
|
|
272
|
+
|
|
273
|
+
const groups = [];
|
|
274
|
+
let files = [];
|
|
275
|
+
try {
|
|
276
|
+
files = fs.readdirSync(groupsDir, { withFileTypes: true })
|
|
277
|
+
.filter((item) => item.isFile() && item.name.endsWith(".json"))
|
|
278
|
+
.map((item) => path.join(groupsDir, item.name))
|
|
279
|
+
.sort((a, b) => a.localeCompare(b, "en", { sensitivity: "base" }));
|
|
280
|
+
} catch {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
for (const filePath of files) {
|
|
285
|
+
try {
|
|
286
|
+
const runtime = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
287
|
+
const status = typeof runtime.status === "string" ? runtime.status : "";
|
|
288
|
+
if (status === "active" || status === "starting") {
|
|
289
|
+
groups.push(runtime);
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
// Ignore malformed runtime files; policy warnings must never block send.
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return groups;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
findGroupMember(runtime = {}, subscriberId = "") {
|
|
299
|
+
const members = Array.isArray(runtime.members) ? runtime.members : [];
|
|
300
|
+
const target = typeof subscriberId === "string" ? subscriberId.trim() : "";
|
|
301
|
+
if (!target) return null;
|
|
302
|
+
|
|
303
|
+
return members.find((member) => {
|
|
304
|
+
if (!member || typeof member !== "object") return false;
|
|
305
|
+
return member.subscriber_id === target
|
|
306
|
+
|| member.bootstrapped_subscriber_id === target
|
|
307
|
+
|| member.nickname === target
|
|
308
|
+
|| member.scoped_nickname === target
|
|
309
|
+
|| member.runtime_nickname === target;
|
|
310
|
+
}) || null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
checkGroupSoftPolicy({ publisher, targets } = {}) {
|
|
314
|
+
const resolvedTargets = Array.isArray(targets) ? targets : [];
|
|
315
|
+
if (!publisher || resolvedTargets.length === 0) return [];
|
|
316
|
+
|
|
317
|
+
const groups = this.readActiveGroupStates();
|
|
318
|
+
if (groups.length === 0) return [];
|
|
319
|
+
|
|
320
|
+
const warnings = [];
|
|
321
|
+
const seen = new Set();
|
|
322
|
+
|
|
323
|
+
for (const runtime of groups) {
|
|
324
|
+
const publisherMember = this.findGroupMember(runtime, publisher);
|
|
325
|
+
if (!publisherMember) continue;
|
|
326
|
+
|
|
327
|
+
const publisherNickname = publisherMember.nickname
|
|
328
|
+
|| publisherMember.scoped_nickname
|
|
329
|
+
|| publisherMember.runtime_nickname
|
|
330
|
+
|| publisher;
|
|
331
|
+
for (const targetSubscriber of resolvedTargets) {
|
|
332
|
+
const targetMember = this.findGroupMember(runtime, targetSubscriber);
|
|
333
|
+
if (!targetMember || targetMember === publisherMember) continue;
|
|
334
|
+
|
|
335
|
+
const acceptFrom = Array.isArray(targetMember.accept_from)
|
|
336
|
+
? targetMember.accept_from.filter((item) => typeof item === "string" && item.trim())
|
|
337
|
+
: [];
|
|
338
|
+
const allowed = acceptFrom.includes(publisherNickname)
|
|
339
|
+
|| acceptFrom.includes(publisherMember.scoped_nickname)
|
|
340
|
+
|| acceptFrom.includes(publisherMember.runtime_nickname)
|
|
341
|
+
|| acceptFrom.includes(publisher);
|
|
342
|
+
|
|
343
|
+
if (allowed) continue;
|
|
344
|
+
|
|
345
|
+
const groupId = runtime.group_id || "unknown-group";
|
|
346
|
+
const targetNickname = targetMember.nickname
|
|
347
|
+
|| targetMember.scoped_nickname
|
|
348
|
+
|| targetMember.runtime_nickname
|
|
349
|
+
|| targetSubscriber;
|
|
350
|
+
const key = `${groupId}:${publisherNickname}:${targetNickname}`;
|
|
351
|
+
if (seen.has(key)) continue;
|
|
352
|
+
seen.add(key);
|
|
353
|
+
|
|
354
|
+
const allowedText = acceptFrom.length > 0 ? acceptFrom.join(", ") : "none";
|
|
355
|
+
warnings.push(
|
|
356
|
+
`group policy warning: ${publisherNickname} -> ${targetNickname} violates accept_from for group ${groupId}; allowed: ${allowedText}`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return warnings;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async runPreSendHooks(context) {
|
|
365
|
+
if (!Array.isArray(this.preSendHooks) || this.preSendHooks.length === 0) {
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const warnings = [];
|
|
370
|
+
for (const hook of this.preSendHooks) {
|
|
371
|
+
if (typeof hook !== "function") continue;
|
|
372
|
+
try {
|
|
373
|
+
// eslint-disable-next-line no-await-in-loop
|
|
374
|
+
const result = await hook(context);
|
|
375
|
+
if (Array.isArray(result)) {
|
|
376
|
+
warnings.push(...result.filter(Boolean).map((item) => String(item)));
|
|
377
|
+
} else if (typeof result === "string" && result) {
|
|
378
|
+
warnings.push(result);
|
|
379
|
+
} else if (result && typeof result.message === "string") {
|
|
380
|
+
warnings.push(result.message);
|
|
381
|
+
}
|
|
382
|
+
} catch (err) {
|
|
383
|
+
warnings.push(`preSendHook failed: ${err && err.message ? err.message : err}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
for (const warning of warnings) {
|
|
388
|
+
this.warn(warning);
|
|
389
|
+
}
|
|
390
|
+
return warnings;
|
|
391
|
+
}
|
|
392
|
+
|
|
258
393
|
/**
|
|
259
394
|
* 发送消息
|
|
260
395
|
*/
|
|
@@ -269,6 +404,16 @@ class MessageManager {
|
|
|
269
404
|
throw new Error(`Target "${target}" not found`);
|
|
270
405
|
}
|
|
271
406
|
|
|
407
|
+
const warnings = await this.runPreSendHooks({
|
|
408
|
+
publisher,
|
|
409
|
+
target,
|
|
410
|
+
targets,
|
|
411
|
+
message,
|
|
412
|
+
options,
|
|
413
|
+
busData: this.busData,
|
|
414
|
+
projectRoot: this.projectRoot,
|
|
415
|
+
});
|
|
416
|
+
|
|
272
417
|
const data = buildMessageData(message, options);
|
|
273
418
|
|
|
274
419
|
// 构建事件
|
|
@@ -295,7 +440,7 @@ class MessageManager {
|
|
|
295
440
|
}
|
|
296
441
|
}
|
|
297
442
|
|
|
298
|
-
return { seq, targets };
|
|
443
|
+
return { seq, targets, warnings };
|
|
299
444
|
}
|
|
300
445
|
|
|
301
446
|
/**
|
|
@@ -421,12 +566,15 @@ class MessageManager {
|
|
|
421
566
|
|
|
422
567
|
return false;
|
|
423
568
|
})
|
|
424
|
-
.map(([id, meta]) =>
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
569
|
+
.map(([id, meta]) => {
|
|
570
|
+
const nickname = meta.nickname || meta.scoped_nickname;
|
|
571
|
+
return {
|
|
572
|
+
id,
|
|
573
|
+
...(nickname ? { nickname } : {}),
|
|
574
|
+
agent_type: meta.agent_type,
|
|
575
|
+
last_seen: meta.last_seen,
|
|
576
|
+
};
|
|
577
|
+
});
|
|
430
578
|
|
|
431
579
|
// 如果只有一个候选者,直接返回
|
|
432
580
|
if (candidates.length === 1) {
|
package/src/bus/nickname.js
CHANGED
|
@@ -14,7 +14,7 @@ class NicknameManager {
|
|
|
14
14
|
resolveNickname(nickname) {
|
|
15
15
|
const subscribers = this.busData.agents || {};
|
|
16
16
|
for (const [id, meta] of Object.entries(subscribers)) {
|
|
17
|
-
if (meta.nickname === nickname) {
|
|
17
|
+
if (meta.nickname === nickname || meta.scoped_nickname === nickname) {
|
|
18
18
|
return id;
|
|
19
19
|
}
|
|
20
20
|
}
|
|
@@ -30,7 +30,10 @@ class NicknameManager {
|
|
|
30
30
|
nicknameExists(nickname, excludeSubscriber = null) {
|
|
31
31
|
const subscribers = this.busData.agents || {};
|
|
32
32
|
for (const [id, meta] of Object.entries(subscribers)) {
|
|
33
|
-
if (
|
|
33
|
+
if (
|
|
34
|
+
id !== excludeSubscriber
|
|
35
|
+
&& (meta.nickname === nickname || meta.scoped_nickname === nickname)
|
|
36
|
+
) {
|
|
34
37
|
return true;
|
|
35
38
|
}
|
|
36
39
|
}
|
|
@@ -71,10 +74,15 @@ class NicknameManager {
|
|
|
71
74
|
return meta?.nickname || null;
|
|
72
75
|
}
|
|
73
76
|
|
|
77
|
+
getScopedNickname(subscriber) {
|
|
78
|
+
const meta = this.busData.agents?.[subscriber];
|
|
79
|
+
return meta?.scoped_nickname || meta?.nickname || null;
|
|
80
|
+
}
|
|
81
|
+
|
|
74
82
|
/**
|
|
75
83
|
* 设置订阅者的昵称
|
|
76
84
|
*/
|
|
77
|
-
setNickname(subscriber, nickname) {
|
|
85
|
+
setNickname(subscriber, nickname, scopedNickname = "") {
|
|
78
86
|
if (!this.busData.agents) {
|
|
79
87
|
this.busData.agents = {};
|
|
80
88
|
}
|
|
@@ -82,6 +90,9 @@ class NicknameManager {
|
|
|
82
90
|
this.busData.agents[subscriber] = {};
|
|
83
91
|
}
|
|
84
92
|
this.busData.agents[subscriber].nickname = nickname;
|
|
93
|
+
if (scopedNickname) {
|
|
94
|
+
this.busData.agents[subscriber].scoped_nickname = scopedNickname;
|
|
95
|
+
}
|
|
85
96
|
}
|
|
86
97
|
}
|
|
87
98
|
|