nexus-channel 1.6.5
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 +147 -0
- package/dist/channel-config-api.js +18 -0
- package/dist/index.js +13 -0
- package/dist/setup-entry.js +82 -0
- package/dist/src/channel.js +203 -0
- package/dist/src/config/migrate-legacy.js +14 -0
- package/dist/src/config/resolve-config.js +92 -0
- package/dist/src/config/schema.js +10 -0
- package/dist/src/gateway/chat-client.js +99 -0
- package/dist/src/hub/agents.js +24 -0
- package/dist/src/hub/api-client.js +17 -0
- package/dist/src/hub/context.js +48 -0
- package/dist/src/runtime/directory.js +31 -0
- package/dist/src/runtime/event-handler.js +88 -0
- package/dist/src/runtime/outbound.js +64 -0
- package/dist/src/runtime/status.js +46 -0
- package/dist/src/runtime/tools.js +59 -0
- package/dist/src/transport/frames.js +51 -0
- package/dist/src/transport/reconnect.js +33 -0
- package/dist/src/transport/resume-store.js +111 -0
- package/dist/src/transport/ws-client.js +249 -0
- package/dist/tests/unit/config.resolve.test.js +31 -0
- package/dist/tests/unit/runtime.event-handler.test.js +23 -0
- package/dist/tests/unit/transport.frames.test.js +31 -0
- package/dist/tests/unit/transport.reconnect.test.js +26 -0
- package/openclaw.plugin.json +43 -0
- package/package.json +83 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadAgentMap = loadAgentMap;
|
|
4
|
+
const schema_1 = require("../config/schema");
|
|
5
|
+
const api_client_1 = require("./api-client");
|
|
6
|
+
async function loadAgentMap(hub2dUrl, config) {
|
|
7
|
+
const result = new Map();
|
|
8
|
+
try {
|
|
9
|
+
const apiBase = (0, api_client_1.normalizeHub2dHttpBase)(hub2dUrl, config.apiPort ?? schema_1.DEFAULT_API_PORT);
|
|
10
|
+
const headers = (0, api_client_1.buildAuthHeaders)(config);
|
|
11
|
+
const resp = await fetch(`${apiBase}/api/agents`, { headers, signal: AbortSignal.timeout(5000) });
|
|
12
|
+
if (!resp.ok) {
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
const agents = await resp.json();
|
|
16
|
+
for (const agent of agents) {
|
|
17
|
+
result.set(agent.username.toLowerCase(), agent.user_id);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.error(`[nexus] failed to load agent map: ${error.message}`);
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeHub2dHttpBase = normalizeHub2dHttpBase;
|
|
4
|
+
exports.buildAuthHeaders = buildAuthHeaders;
|
|
5
|
+
function normalizeHub2dHttpBase(hub2dUrl, apiPort) {
|
|
6
|
+
const wsUrl = hub2dUrl.startsWith('ws') ? hub2dUrl : hub2dUrl.replace(/^http/, 'ws');
|
|
7
|
+
const parsed = new URL(wsUrl);
|
|
8
|
+
return `http://${parsed.hostname}:${apiPort}`;
|
|
9
|
+
}
|
|
10
|
+
function buildAuthHeaders(config) {
|
|
11
|
+
const headers = {};
|
|
12
|
+
const apiKey = config.nexusApiKey ?? process.env.NEXUS_API_KEY ?? '';
|
|
13
|
+
if (apiKey) {
|
|
14
|
+
headers['x-api-key'] = apiKey;
|
|
15
|
+
}
|
|
16
|
+
return headers;
|
|
17
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchNexusContext = fetchNexusContext;
|
|
4
|
+
const schema_1 = require("../config/schema");
|
|
5
|
+
const api_client_1 = require("./api-client");
|
|
6
|
+
const CONTEXT_INJECTION_TTL_MS = 60000;
|
|
7
|
+
const CONTEXT_MAX_CHARS = 2000;
|
|
8
|
+
const contextCache = new Map();
|
|
9
|
+
async function fetchNexusContext(roomId, hub2dUrl, config) {
|
|
10
|
+
const mode = config.contextInjection ?? 'P0';
|
|
11
|
+
if (mode === 'OFF')
|
|
12
|
+
return '';
|
|
13
|
+
const agentName = config.agentName || 'unknown';
|
|
14
|
+
const cacheKey = `${roomId}:${agentName}`;
|
|
15
|
+
const cached = contextCache.get(cacheKey);
|
|
16
|
+
if (cached && Date.now() - cached.fetchedAt < CONTEXT_INJECTION_TTL_MS) {
|
|
17
|
+
return cached.content;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const apiBase = (0, api_client_1.normalizeHub2dHttpBase)(hub2dUrl, config.apiPort ?? schema_1.DEFAULT_API_PORT);
|
|
21
|
+
const headers = (0, api_client_1.buildAuthHeaders)(config);
|
|
22
|
+
const url = `${apiBase}/api/context/inject?room_id=${encodeURIComponent(roomId)}&level=P0&node=${encodeURIComponent(agentName)}`;
|
|
23
|
+
const resp = await fetch(url, { headers, signal: AbortSignal.timeout(5000) });
|
|
24
|
+
if (!resp.ok) {
|
|
25
|
+
console.error(`[nexus] context API error: ${resp.status}`);
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
const data = await resp.json();
|
|
29
|
+
if (!data?.enabled || !Array.isArray(data.items) || data.items.length === 0) {
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
const lines = data.items.map((item) => {
|
|
33
|
+
const sender = item.from || item.human_id || item.sender || 'unknown';
|
|
34
|
+
return `[${sender}] ${item.content}`;
|
|
35
|
+
});
|
|
36
|
+
let contentBody = lines.join('\n');
|
|
37
|
+
if (contentBody.length > CONTEXT_MAX_CHARS) {
|
|
38
|
+
contentBody = `${contentBody.slice(0, CONTEXT_MAX_CHARS)}\n[TRUNCATED context_too_long]`;
|
|
39
|
+
}
|
|
40
|
+
const content = `[Room Context: latest ${data.items.length} msgs]\n${contentBody}\n[End Context]\n\n`;
|
|
41
|
+
contextCache.set(cacheKey, { content, fetchedAt: Date.now() });
|
|
42
|
+
return content;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.error(`[nexus] context fetch failed: ${error.message}`);
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.messagingAdapter = exports.directoryAdapter = exports.KNOWN_AGENTS = exports.KNOWN_ROOMS = void 0;
|
|
4
|
+
exports.KNOWN_ROOMS = [
|
|
5
|
+
{ kind: 'group', id: 'general', name: 'General' },
|
|
6
|
+
{ kind: 'group', id: 'boss', name: 'Boss' },
|
|
7
|
+
{ kind: 'group', id: 'alpha', name: 'Alpha' },
|
|
8
|
+
];
|
|
9
|
+
exports.KNOWN_AGENTS = [
|
|
10
|
+
{ kind: 'user', id: 'serina', name: 'Serina 💠' },
|
|
11
|
+
{ kind: 'user', id: 'cortana', name: 'Cortana 🤍' },
|
|
12
|
+
{ kind: 'user', id: 'roland', name: 'Roland 🟠' },
|
|
13
|
+
{ kind: 'user', id: 'oracle', name: 'Oracle 🔮' },
|
|
14
|
+
];
|
|
15
|
+
exports.directoryAdapter = {
|
|
16
|
+
listGroups: async () => {
|
|
17
|
+
return [...exports.KNOWN_ROOMS, ...exports.KNOWN_AGENTS.map((agent) => ({ ...agent, kind: 'group' }))];
|
|
18
|
+
},
|
|
19
|
+
listPeers: async () => {
|
|
20
|
+
return exports.KNOWN_AGENTS;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
exports.messagingAdapter = {
|
|
24
|
+
targetResolver: {
|
|
25
|
+
hint: 'Use room name (general, boss) or agent name (serina, cortana, roland, oracle). Prefix with @ for agents.',
|
|
26
|
+
looksLikeId: (raw) => {
|
|
27
|
+
const lower = raw.toLowerCase().trim();
|
|
28
|
+
return exports.KNOWN_ROOMS.some((room) => room.id === lower) || exports.KNOWN_AGENTS.some((agent) => agent.id === lower);
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getEventTimestamp = getEventTimestamp;
|
|
4
|
+
exports.getEventDecision = getEventDecision;
|
|
5
|
+
exports.buildEventPrompt = buildEventPrompt;
|
|
6
|
+
exports.processEvent = processEvent;
|
|
7
|
+
const schema_1 = require("../config/schema");
|
|
8
|
+
const chat_client_1 = require("../gateway/chat-client");
|
|
9
|
+
const context_1 = require("../hub/context");
|
|
10
|
+
const STALE_MESSAGE_TTL_MS = 20 * 60 * 1000;
|
|
11
|
+
function getEventTimestamp(eventId) {
|
|
12
|
+
const ts = parseInt(eventId.split('-')[0], 10);
|
|
13
|
+
return Number.isNaN(ts) ? 0 : ts;
|
|
14
|
+
}
|
|
15
|
+
function getEventDecision(params) {
|
|
16
|
+
const { event, config, selfUserId, isRecent } = params;
|
|
17
|
+
const agentName = config.agentName || schema_1.DEFAULT_AGENT_NAME;
|
|
18
|
+
if (event.from === agentName)
|
|
19
|
+
return 'skip-own';
|
|
20
|
+
const eventTs = getEventTimestamp(event.event_id);
|
|
21
|
+
const ageMs = Date.now() - eventTs;
|
|
22
|
+
if (eventTs > 0 && ageMs > STALE_MESSAGE_TTL_MS)
|
|
23
|
+
return 'skip-stale';
|
|
24
|
+
const toActorIds = event.to_actor_ids || [];
|
|
25
|
+
if (toActorIds.length > 0 && (selfUserId === undefined || !toActorIds.includes(selfUserId))) {
|
|
26
|
+
return 'skip-actor-gate';
|
|
27
|
+
}
|
|
28
|
+
const mentions = event.mentions || [];
|
|
29
|
+
if (toActorIds.length === 0 && mentions.length > 0 && !mentions.includes(agentName)) {
|
|
30
|
+
return 'skip-mention-gate';
|
|
31
|
+
}
|
|
32
|
+
if (isRecent(event.event_id))
|
|
33
|
+
return 'skip-duplicate';
|
|
34
|
+
return 'process';
|
|
35
|
+
}
|
|
36
|
+
function buildEventPrompt(event, config) {
|
|
37
|
+
const threshold = config.longTextThreshold || schema_1.DEFAULT_LONG_TEXT;
|
|
38
|
+
const from = event.from || 'unknown';
|
|
39
|
+
let content = event.text || '';
|
|
40
|
+
if (content.length > threshold) {
|
|
41
|
+
content = `${content.slice(0, threshold)} [TRUNCATED orig_len=${event.text.length} kept=${threshold}]`;
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(event.attachments) && event.attachments.length > 0) {
|
|
44
|
+
const attLines = event.attachments.map((att) => {
|
|
45
|
+
const parts = [`[附件:${att.type || 'file'}] ${att.name || 'unknown'}`];
|
|
46
|
+
if (att.size)
|
|
47
|
+
parts.push(`${Math.round(att.size / 1024)}KB`);
|
|
48
|
+
if (att.width && att.height)
|
|
49
|
+
parts.push(`${att.width}×${att.height}`);
|
|
50
|
+
return parts.join(' ');
|
|
51
|
+
});
|
|
52
|
+
content += `\n${attLines.join('\n')}`;
|
|
53
|
+
}
|
|
54
|
+
return `[from: ${from}] ${content}`;
|
|
55
|
+
}
|
|
56
|
+
async function processEvent(params) {
|
|
57
|
+
const { event, config, selfUserId, isRecent, markRecent, gatewayConfig, sendReply, sendAck, updateResumeToken } = params;
|
|
58
|
+
const decision = getEventDecision({ event, config, selfUserId, isRecent });
|
|
59
|
+
const roomId = event.room_id || schema_1.DEFAULT_ROOM_ID;
|
|
60
|
+
if (decision !== 'process') {
|
|
61
|
+
if (decision !== 'skip-own' && decision !== 'skip-duplicate') {
|
|
62
|
+
sendAck(roomId, event.event_id);
|
|
63
|
+
updateResumeToken(roomId, event.event_id);
|
|
64
|
+
}
|
|
65
|
+
return decision;
|
|
66
|
+
}
|
|
67
|
+
markRecent(event.event_id);
|
|
68
|
+
try {
|
|
69
|
+
let prompt = buildEventPrompt(event, config);
|
|
70
|
+
const hub2dUrl = config.hub2dUrl || 'ws://127.0.0.1:3001';
|
|
71
|
+
const context = await (0, context_1.fetchNexusContext)(roomId, hub2dUrl, config);
|
|
72
|
+
if (context) {
|
|
73
|
+
prompt = `${context}${prompt}`;
|
|
74
|
+
}
|
|
75
|
+
const sessionKey = `nexus:${roomId}`;
|
|
76
|
+
const response = await (0, chat_client_1.callGatewayWithRetry)([{ role: 'user', content: prompt }], sessionKey, gatewayConfig);
|
|
77
|
+
sendReply(event.event_id, roomId, event.from || 'unknown', response.content, 'done');
|
|
78
|
+
sendAck(roomId, event.event_id);
|
|
79
|
+
updateResumeToken(roomId, event.event_id);
|
|
80
|
+
return 'process';
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
sendReply(event.event_id, roomId, event.from || 'unknown', `[ERROR] ${error.code || 'unknown_error'}: ${String(error.message || '').slice(0, 200)}`, 'error');
|
|
84
|
+
sendAck(roomId, event.event_id);
|
|
85
|
+
updateResumeToken(roomId, event.event_id);
|
|
86
|
+
return 'process';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KNOWN_AGENTS = exports.KNOWN_ROOMS = void 0;
|
|
4
|
+
exports.resolveOutboundTarget = resolveOutboundTarget;
|
|
5
|
+
exports.isResolvedOutboundTargetError = isResolvedOutboundTargetError;
|
|
6
|
+
exports.createOutboundClient = createOutboundClient;
|
|
7
|
+
const schema_1 = require("../config/schema");
|
|
8
|
+
exports.KNOWN_ROOMS = [
|
|
9
|
+
{ kind: 'group', id: 'general', name: 'General' },
|
|
10
|
+
{ kind: 'group', id: 'boss', name: 'Boss' },
|
|
11
|
+
{ kind: 'group', id: 'alpha', name: 'Alpha' },
|
|
12
|
+
];
|
|
13
|
+
exports.KNOWN_AGENTS = [
|
|
14
|
+
{ kind: 'user', id: 'serina', name: 'Serina 💠' },
|
|
15
|
+
{ kind: 'user', id: 'cortana', name: 'Cortana 🤍' },
|
|
16
|
+
{ kind: 'user', id: 'roland', name: 'Roland 🟠' },
|
|
17
|
+
{ kind: 'user', id: 'oracle', name: 'Oracle 🔮' },
|
|
18
|
+
];
|
|
19
|
+
const SILENT_PATTERNS = ['NO_REPLY', 'HEARTBEAT_OK', '(no reply)'];
|
|
20
|
+
function resolveOutboundTarget(to) {
|
|
21
|
+
const room = exports.KNOWN_ROOMS.find((item) => item.id === to || item.name.toLowerCase() === to.toLowerCase());
|
|
22
|
+
if (room)
|
|
23
|
+
return { ok: true, to: room.id };
|
|
24
|
+
const agent = exports.KNOWN_AGENTS.find((item) => item.id === to || item.name.toLowerCase().startsWith(to.toLowerCase()));
|
|
25
|
+
if (agent)
|
|
26
|
+
return { ok: true, to: agent.id };
|
|
27
|
+
if (!to || !to.trim()) {
|
|
28
|
+
return { ok: false, error: new Error('target required') };
|
|
29
|
+
}
|
|
30
|
+
return { ok: true, to };
|
|
31
|
+
}
|
|
32
|
+
function isResolvedOutboundTargetError(value) {
|
|
33
|
+
return value.ok === false;
|
|
34
|
+
}
|
|
35
|
+
function createOutboundClient(config, sendMessage) {
|
|
36
|
+
const agentName = config.agentName || schema_1.DEFAULT_AGENT_NAME;
|
|
37
|
+
const defaultRoom = (config.roomId || schema_1.DEFAULT_ROOM_ID).split(',')[0].trim();
|
|
38
|
+
return {
|
|
39
|
+
async sendText(params) {
|
|
40
|
+
if (!params.text.trim() || SILENT_PATTERNS.some((pattern) => params.text.trim().startsWith(pattern))) {
|
|
41
|
+
return { channel: 'nexus-channel', messageId: `suppressed-${Date.now()}` };
|
|
42
|
+
}
|
|
43
|
+
const resolved = resolveOutboundTarget(params.to);
|
|
44
|
+
if (isResolvedOutboundTargetError(resolved)) {
|
|
45
|
+
throw resolved.error;
|
|
46
|
+
}
|
|
47
|
+
const isAgent = exports.KNOWN_AGENTS.some((agent) => agent.id === resolved.to);
|
|
48
|
+
const targetRoom = exports.KNOWN_ROOMS.some((room) => room.id === resolved.to) ? resolved.to : defaultRoom;
|
|
49
|
+
const mentions = isAgent && resolved.to !== agentName ? [resolved.to] : undefined;
|
|
50
|
+
sendMessage(targetRoom, params.text, mentions, `out-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
51
|
+
return { channel: 'nexus-channel', messageId: `out-${Date.now()}` };
|
|
52
|
+
},
|
|
53
|
+
async sendMedia(params) {
|
|
54
|
+
const resolved = resolveOutboundTarget(params.to);
|
|
55
|
+
if (isResolvedOutboundTargetError(resolved)) {
|
|
56
|
+
throw resolved.error;
|
|
57
|
+
}
|
|
58
|
+
const targetRoom = exports.KNOWN_ROOMS.some((room) => room.id === resolved.to) ? resolved.to : defaultRoom;
|
|
59
|
+
const fallbackText = params.text || `[Media: ${params.mediaUrl || 'unsupported'}]`;
|
|
60
|
+
sendMessage(targetRoom, fallbackText, undefined, `media-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
61
|
+
return { channel: 'nexus-channel', messageId: `media-${Date.now()}` };
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.statusAdapter = exports.defaultStatusSnapshot = void 0;
|
|
4
|
+
exports.defaultStatusSnapshot = {
|
|
5
|
+
accountId: 'default',
|
|
6
|
+
enabled: true,
|
|
7
|
+
configured: false,
|
|
8
|
+
running: false,
|
|
9
|
+
connected: false,
|
|
10
|
+
lastConnectedAt: null,
|
|
11
|
+
lastInboundAt: null,
|
|
12
|
+
lastEventAt: null,
|
|
13
|
+
lastError: null,
|
|
14
|
+
};
|
|
15
|
+
exports.statusAdapter = {
|
|
16
|
+
defaultRuntime: { ...exports.defaultStatusSnapshot },
|
|
17
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
18
|
+
configured: Boolean(snapshot?.configured),
|
|
19
|
+
linked: Boolean(snapshot?.connected),
|
|
20
|
+
connected: Boolean(snapshot?.connected),
|
|
21
|
+
running: Boolean(snapshot?.running),
|
|
22
|
+
lastError: snapshot?.lastError ?? null,
|
|
23
|
+
authAgeMs: typeof snapshot?.lastConnectedAt === 'number' ? Math.max(0, Date.now() - snapshot.lastConnectedAt) : null,
|
|
24
|
+
}),
|
|
25
|
+
buildAccountSnapshot: ({ account, runtime }) => {
|
|
26
|
+
const meta = account?.__nexusMeta || { configured: false };
|
|
27
|
+
return {
|
|
28
|
+
accountId: account?.accountId || 'default',
|
|
29
|
+
name: account?.agentName || account?.accountId || 'Nexus',
|
|
30
|
+
enabled: account?.enabled ?? true,
|
|
31
|
+
configured: Boolean(meta.configured),
|
|
32
|
+
running: runtime?.running ?? false,
|
|
33
|
+
connected: runtime?.connected ?? false,
|
|
34
|
+
lastConnectedAt: runtime?.lastConnectedAt ?? null,
|
|
35
|
+
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
36
|
+
lastEventAt: runtime?.lastEventAt ?? null,
|
|
37
|
+
lastError: runtime?.lastError ?? null,
|
|
38
|
+
meta,
|
|
39
|
+
extra: {
|
|
40
|
+
hub2dUrl: account?.hub2dUrl ?? null,
|
|
41
|
+
roomId: account?.roomId ?? null,
|
|
42
|
+
agentName: account?.agentName ?? null,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createAgentTools = createAgentTools;
|
|
4
|
+
const schema_1 = require("../config/schema");
|
|
5
|
+
const context_1 = require("../hub/context");
|
|
6
|
+
const DispatchMessageSchema = {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
target: { type: 'string', description: 'Agent name or room id to send to.' },
|
|
10
|
+
room_id: { type: 'string', description: 'Room ID to dispatch from.' },
|
|
11
|
+
text: { type: 'string', description: 'Message content.' },
|
|
12
|
+
},
|
|
13
|
+
required: ['text'],
|
|
14
|
+
};
|
|
15
|
+
const GetRoomSummarySchema = {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
room_id: { type: 'string', description: 'Room ID to query.' },
|
|
19
|
+
max_chars: { type: 'number', minimum: 100, maximum: 5000 },
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
function createAgentTools(deps) {
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
name: 'nexus_dispatch_message',
|
|
26
|
+
description: 'Dispatch a message to another agent or room in Nexus Hub.',
|
|
27
|
+
schema: DispatchMessageSchema,
|
|
28
|
+
ownerOnly: false,
|
|
29
|
+
handler: async (args) => {
|
|
30
|
+
const text = String(args?.text || '').trim();
|
|
31
|
+
if (!text)
|
|
32
|
+
return { content: '[ERROR] nexus_dispatch_message: text is required.' };
|
|
33
|
+
const cfg = deps.getConfig();
|
|
34
|
+
const roomId = args?.room_id || (cfg.roomId || schema_1.DEFAULT_ROOM_ID).split(',')[0].trim();
|
|
35
|
+
const target = args?.target ? String(args.target) : undefined;
|
|
36
|
+
const result = await deps.dispatchMessage(roomId, text, target);
|
|
37
|
+
return { content: `[OK] Message dispatched. event_id=${result.event_id}` };
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'nexus_get_room_summary',
|
|
42
|
+
description: 'Fetch recent room summary from Nexus Hub context API.',
|
|
43
|
+
schema: GetRoomSummarySchema,
|
|
44
|
+
ownerOnly: false,
|
|
45
|
+
handler: async (args) => {
|
|
46
|
+
const cfg = deps.getConfig();
|
|
47
|
+
const roomId = args?.room_id || (cfg.roomId || schema_1.DEFAULT_ROOM_ID).split(',')[0].trim();
|
|
48
|
+
const maxChars = Number(args?.max_chars || 2000);
|
|
49
|
+
const hub2dUrl = cfg.hub2dUrl || 'ws://127.0.0.1:3001';
|
|
50
|
+
const context = await (0, context_1.fetchNexusContext)(roomId, hub2dUrl, cfg);
|
|
51
|
+
if (!context) {
|
|
52
|
+
return { content: `[INFO] No recent context available for room '${roomId}'.` };
|
|
53
|
+
}
|
|
54
|
+
const truncated = context.length > maxChars ? `${context.slice(0, maxChars)}\n[...truncated ${context.length - maxChars} chars]` : context;
|
|
55
|
+
return { content: `## Room Summary: ${roomId}\n\n${truncated}` };
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildConnectFrame = buildConnectFrame;
|
|
4
|
+
exports.buildAckFrame = buildAckFrame;
|
|
5
|
+
exports.buildReplyFrame = buildReplyFrame;
|
|
6
|
+
exports.buildSendFrame = buildSendFrame;
|
|
7
|
+
function buildConnectFrame(params) {
|
|
8
|
+
const frame = {
|
|
9
|
+
type: 'connect',
|
|
10
|
+
node: params.node,
|
|
11
|
+
rooms: params.rooms,
|
|
12
|
+
resume_token: params.resumeToken || {},
|
|
13
|
+
clientInfo: {
|
|
14
|
+
openclawVersion: params.openclawVersion || 'unknown',
|
|
15
|
+
pluginVersion: params.pluginVersion || 'unknown',
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
if (params.token) {
|
|
19
|
+
frame.token = params.token;
|
|
20
|
+
}
|
|
21
|
+
return frame;
|
|
22
|
+
}
|
|
23
|
+
function buildAckFrame(roomId, eventId) {
|
|
24
|
+
return {
|
|
25
|
+
type: 'ack',
|
|
26
|
+
room_id: roomId,
|
|
27
|
+
event_id: eventId,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function buildReplyFrame(params) {
|
|
31
|
+
return {
|
|
32
|
+
type: 'reply',
|
|
33
|
+
event_id: params.eventId,
|
|
34
|
+
room_id: params.roomId,
|
|
35
|
+
from: params.from,
|
|
36
|
+
to: params.to,
|
|
37
|
+
text: params.text,
|
|
38
|
+
content: params.text,
|
|
39
|
+
status: params.status,
|
|
40
|
+
blocks: [],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function buildSendFrame(params) {
|
|
44
|
+
return {
|
|
45
|
+
type: 'send',
|
|
46
|
+
room_id: params.roomId,
|
|
47
|
+
text: params.text,
|
|
48
|
+
client_msg_id: params.clientMsgId,
|
|
49
|
+
mentions: params.mentions,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createReconnectState = createReconnectState;
|
|
4
|
+
exports.scheduleReconnect = scheduleReconnect;
|
|
5
|
+
exports.resetReconnectDelay = resetReconnectDelay;
|
|
6
|
+
exports.incrementConnId = incrementConnId;
|
|
7
|
+
exports.isStaleConnection = isStaleConnection;
|
|
8
|
+
const MAX_RECONNECT_DELAY = 30000;
|
|
9
|
+
const BASE_RECONNECT_DELAY = 1000;
|
|
10
|
+
function createReconnectState() {
|
|
11
|
+
return {
|
|
12
|
+
currentDelay: BASE_RECONNECT_DELAY,
|
|
13
|
+
connId: 0
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function scheduleReconnect(state, callback) {
|
|
17
|
+
const jitter = Math.floor(Math.random() * 250);
|
|
18
|
+
const delay = state.currentDelay + jitter;
|
|
19
|
+
const timer = setTimeout(() => {
|
|
20
|
+
callback();
|
|
21
|
+
}, delay);
|
|
22
|
+
state.currentDelay = Math.min(state.currentDelay * 2, MAX_RECONNECT_DELAY);
|
|
23
|
+
return { delay, timer };
|
|
24
|
+
}
|
|
25
|
+
function resetReconnectDelay(state) {
|
|
26
|
+
state.currentDelay = BASE_RECONNECT_DELAY;
|
|
27
|
+
}
|
|
28
|
+
function incrementConnId(state) {
|
|
29
|
+
return ++state.connId;
|
|
30
|
+
}
|
|
31
|
+
function isStaleConnection(state, connId) {
|
|
32
|
+
return connId !== state.connId;
|
|
33
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.loadResumeTokens = loadResumeTokens;
|
|
37
|
+
exports.saveResumeTokens = saveResumeTokens;
|
|
38
|
+
exports.initResumeTokenStore = initResumeTokenStore;
|
|
39
|
+
exports.updateResumeToken = updateResumeToken;
|
|
40
|
+
exports.getResumeTokens = getResumeTokens;
|
|
41
|
+
exports.flushResumeTokens = flushResumeTokens;
|
|
42
|
+
exports.resetResumeTokenStore = resetResumeTokenStore;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const RESUME_TOKEN_PATH = path.join(process.env.HOME || '~', '.openclaw', 'state', 'nexus-resume.json');
|
|
46
|
+
function loadResumeTokens() {
|
|
47
|
+
try {
|
|
48
|
+
if (fs.existsSync(RESUME_TOKEN_PATH)) {
|
|
49
|
+
const data = JSON.parse(fs.readFileSync(RESUME_TOKEN_PATH, 'utf-8'));
|
|
50
|
+
return data || {};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
console.error(`[nexus] Failed to load resume_tokens: ${e.message}`);
|
|
55
|
+
}
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
function saveResumeTokens(tokens) {
|
|
59
|
+
try {
|
|
60
|
+
const dir = path.dirname(RESUME_TOKEN_PATH);
|
|
61
|
+
if (!fs.existsSync(dir)) {
|
|
62
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
fs.writeFileSync(RESUME_TOKEN_PATH, JSON.stringify(tokens, null, 2));
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
console.error(`[nexus] Failed to save resume_tokens: ${e.message}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
let flushTimer = null;
|
|
71
|
+
let tokensDirty = false;
|
|
72
|
+
let currentTokens = {};
|
|
73
|
+
function initResumeTokenStore(initialTokens) {
|
|
74
|
+
currentTokens = initialTokens || loadResumeTokens();
|
|
75
|
+
tokensDirty = false;
|
|
76
|
+
}
|
|
77
|
+
function updateResumeToken(room, eventId) {
|
|
78
|
+
currentTokens[room] = eventId;
|
|
79
|
+
tokensDirty = true;
|
|
80
|
+
// Debounce flush: write at most every 2s
|
|
81
|
+
if (!flushTimer) {
|
|
82
|
+
flushTimer = setTimeout(() => {
|
|
83
|
+
flushTimer = null;
|
|
84
|
+
if (tokensDirty) {
|
|
85
|
+
saveResumeTokens(currentTokens);
|
|
86
|
+
tokensDirty = false;
|
|
87
|
+
}
|
|
88
|
+
}, 2000);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function getResumeTokens() {
|
|
92
|
+
return { ...currentTokens };
|
|
93
|
+
}
|
|
94
|
+
function flushResumeTokens() {
|
|
95
|
+
if (flushTimer) {
|
|
96
|
+
clearTimeout(flushTimer);
|
|
97
|
+
flushTimer = null;
|
|
98
|
+
}
|
|
99
|
+
if (tokensDirty) {
|
|
100
|
+
saveResumeTokens(currentTokens);
|
|
101
|
+
tokensDirty = false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function resetResumeTokenStore() {
|
|
105
|
+
if (flushTimer) {
|
|
106
|
+
clearTimeout(flushTimer);
|
|
107
|
+
flushTimer = null;
|
|
108
|
+
}
|
|
109
|
+
currentTokens = {};
|
|
110
|
+
tokensDirty = false;
|
|
111
|
+
}
|