@phronesis-io/openclaw-eigenflux 0.0.1 → 0.0.2
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 +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/notifier.d.ts +1 -17
- package/dist/notifier.d.ts.map +1 -1
- package/dist/notifier.js +1 -94
- package/dist/notifier.js.map +1 -1
- package/openclaw.plugin.json +4 -4
- package/package.json +1 -2
- package/src/agent-prompt-templates.ts +0 -91
- package/src/config.test.ts +0 -188
- package/src/config.ts +0 -410
- package/src/credentials-loader.test.ts +0 -78
- package/src/credentials-loader.ts +0 -121
- package/src/gateway-rpc-client.test.ts +0 -190
- package/src/gateway-rpc-client.ts +0 -373
- package/src/index.integration.test.ts +0 -437
- package/src/index.test.ts +0 -454
- package/src/index.ts +0 -758
- package/src/logger.ts +0 -27
- package/src/notification-route-resolver.test.ts +0 -136
- package/src/notification-route-resolver.ts +0 -430
- package/src/notifier.test.ts +0 -374
- package/src/notifier.ts +0 -558
- package/src/openclaw-plugin-sdk.d.ts +0 -121
- package/src/pm-polling-client.test.ts +0 -390
- package/src/pm-polling-client.ts +0 -257
- package/src/polling-client.test.ts +0 -279
- package/src/polling-client.ts +0 -283
- package/src/session-route-memory.ts +0 -106
package/src/index.ts
DELETED
|
@@ -1,758 +0,0 @@
|
|
|
1
|
-
import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
EigenFluxPollingClient,
|
|
5
|
-
AuthRequiredEvent,
|
|
6
|
-
FeedResponse,
|
|
7
|
-
} from './polling-client';
|
|
8
|
-
import { EigenFluxPmPollingClient, PmFetchResponse } from './pm-polling-client';
|
|
9
|
-
import { Logger } from './logger';
|
|
10
|
-
import { AuthState, CredentialsLoader } from './credentials-loader';
|
|
11
|
-
import {
|
|
12
|
-
PLUGIN_CONFIG,
|
|
13
|
-
PLUGIN_CONFIG_SCHEMA,
|
|
14
|
-
resolvePluginConfig,
|
|
15
|
-
resolveServerSkillPath,
|
|
16
|
-
type ResolvedEigenFluxPluginConfig,
|
|
17
|
-
type ResolvedEigenFluxServerConfig,
|
|
18
|
-
} from './config';
|
|
19
|
-
import { resolveNotificationRoute } from './notification-route-resolver';
|
|
20
|
-
import {
|
|
21
|
-
buildAuthRequiredPromptTemplate,
|
|
22
|
-
buildFeedPayloadPromptTemplate,
|
|
23
|
-
buildPmPayloadPromptTemplate,
|
|
24
|
-
type EigenFluxPromptServerContext,
|
|
25
|
-
} from './agent-prompt-templates';
|
|
26
|
-
import { EigenFluxNotifier } from './notifier';
|
|
27
|
-
import { writeStoredNotificationRoute } from './session-route-memory';
|
|
28
|
-
|
|
29
|
-
type JsonRecord = Record<string, unknown>;
|
|
30
|
-
|
|
31
|
-
type JsonApiSuccess<T extends JsonRecord> = {
|
|
32
|
-
code: number;
|
|
33
|
-
msg: string;
|
|
34
|
-
data: T;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
type ProfileResponseData = {
|
|
38
|
-
agent: JsonRecord;
|
|
39
|
-
profile: JsonRecord;
|
|
40
|
-
influence: JsonRecord;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
type AuthPromptContext = {
|
|
44
|
-
authEvent: AuthRequiredEvent;
|
|
45
|
-
authState?: AuthState;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
type CommandRouteContext = {
|
|
49
|
-
channel?: string;
|
|
50
|
-
to?: string;
|
|
51
|
-
from?: string;
|
|
52
|
-
accountId?: string;
|
|
53
|
-
getCurrentConversationBinding?: () => Promise<{
|
|
54
|
-
channel: string;
|
|
55
|
-
accountId: string;
|
|
56
|
-
conversationId: string;
|
|
57
|
-
parentConversationId?: string;
|
|
58
|
-
} | null>;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
type ServerRuntime = {
|
|
62
|
-
server: ResolvedEigenFluxServerConfig;
|
|
63
|
-
credentialsLoader: CredentialsLoader;
|
|
64
|
-
notifier: EigenFluxNotifier;
|
|
65
|
-
pollingClient: EigenFluxPollingClient;
|
|
66
|
-
pmPollingClient: EigenFluxPmPollingClient;
|
|
67
|
-
getPromptContext: () => EigenFluxPromptServerContext;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
type ParsedCommandArgs = {
|
|
71
|
-
command: string;
|
|
72
|
-
serverName?: string;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
type ServerRuntimeSelection = {
|
|
76
|
-
runtime?: ServerRuntime;
|
|
77
|
-
error?: string;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const COMMAND_NAMES = ['auth', 'profile', 'servers', 'feed', 'pm', 'here'] as const;
|
|
81
|
-
const COMMAND_NAME_SET = new Set<string>(COMMAND_NAMES);
|
|
82
|
-
|
|
83
|
-
function readServerSessionStorePath(
|
|
84
|
-
server: ResolvedEigenFluxServerConfig
|
|
85
|
-
): string | undefined {
|
|
86
|
-
return (server as ResolvedEigenFluxServerConfig & { sessionStorePath?: string })
|
|
87
|
-
.sessionStorePath;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function register(api: OpenClawPluginApi): void {
|
|
91
|
-
const logger = new Logger(api.logger);
|
|
92
|
-
logger.info('EigenFlux activating...');
|
|
93
|
-
|
|
94
|
-
const pluginConfig = resolvePluginConfig(api.pluginConfig, api.config as any);
|
|
95
|
-
const runtimes = pluginConfig.servers.map((server) =>
|
|
96
|
-
createServerRuntime(api, logger, pluginConfig, server)
|
|
97
|
-
);
|
|
98
|
-
const enabledRuntimes = runtimes.filter((runtime) => runtime.server.enabled);
|
|
99
|
-
|
|
100
|
-
if (!pluginConfig.gatewayToken) {
|
|
101
|
-
logger.warn(
|
|
102
|
-
'OpenClaw gateway token not found in config.gateway.auth.token or plugin config; Gateway RPC fallback may fail when gateway auth mode is token'
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (enabledRuntimes.length === 0) {
|
|
107
|
-
logger.warn('No enabled EigenFlux servers configured; background polling services will not start');
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
registerServices(api, logger, enabledRuntimes);
|
|
111
|
-
registerCommand(api, logger, runtimes);
|
|
112
|
-
|
|
113
|
-
logger.info(
|
|
114
|
-
`EigenFlux activated with ${enabledRuntimes.length}/${runtimes.length} enabled server(s)`
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const plugin = {
|
|
119
|
-
id: 'eigenflux',
|
|
120
|
-
name: 'EigenFlux',
|
|
121
|
-
description: 'OpenClaw extension for EigenFlux periodic polling with multi-server delivery',
|
|
122
|
-
configSchema: PLUGIN_CONFIG_SCHEMA,
|
|
123
|
-
register,
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
export default plugin;
|
|
127
|
-
|
|
128
|
-
function createServerRuntime(
|
|
129
|
-
api: OpenClawPluginApi,
|
|
130
|
-
logger: Logger,
|
|
131
|
-
pluginConfig: ResolvedEigenFluxPluginConfig,
|
|
132
|
-
server: ResolvedEigenFluxServerConfig
|
|
133
|
-
): ServerRuntime {
|
|
134
|
-
const credentialsLoader = new CredentialsLoader(logger, server.workdir);
|
|
135
|
-
const notifier = new EigenFluxNotifier(api, logger, {
|
|
136
|
-
gatewayUrl: pluginConfig.gatewayUrl,
|
|
137
|
-
gatewayToken: pluginConfig.gatewayToken,
|
|
138
|
-
workdir: server.workdir,
|
|
139
|
-
sessionKey: server.sessionKey,
|
|
140
|
-
agentId: server.agentId,
|
|
141
|
-
replyChannel: server.replyChannel,
|
|
142
|
-
replyTo: server.replyTo,
|
|
143
|
-
replyAccountId: server.replyAccountId,
|
|
144
|
-
openclawCliBin: pluginConfig.openclawCliBin,
|
|
145
|
-
sessionStorePath: readServerSessionStorePath(server),
|
|
146
|
-
routeOverrides: server.routeOverrides,
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
const getPromptContext = (): EigenFluxPromptServerContext => ({
|
|
150
|
-
serverName: server.name,
|
|
151
|
-
workdir: server.workdir,
|
|
152
|
-
skillPath: resolveServerSkillPath(server),
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
let lastAuthPromptKey: string | null = null;
|
|
156
|
-
|
|
157
|
-
const resetAuthPromptGate = (): void => {
|
|
158
|
-
lastAuthPromptKey = null;
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const notifyAuthRequired = async (authEvent: AuthRequiredEvent): Promise<void> => {
|
|
162
|
-
const promptKey = `${authEvent.reason}:${authEvent.credentialsPath}:${authEvent.source || 'unknown'}`;
|
|
163
|
-
if (lastAuthPromptKey === promptKey) {
|
|
164
|
-
logger.debug(`Skipping duplicate auth prompt for server=${server.name}, key=${promptKey}`);
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
lastAuthPromptKey = promptKey;
|
|
169
|
-
const authState = credentialsLoader.loadAuthState();
|
|
170
|
-
await notifier.deliver(
|
|
171
|
-
buildAuthRequiredMessage(getPromptContext(), {
|
|
172
|
-
authEvent,
|
|
173
|
-
authState,
|
|
174
|
-
})
|
|
175
|
-
);
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const pollingClient = new EigenFluxPollingClient({
|
|
179
|
-
apiUrl: server.endpoint,
|
|
180
|
-
getAuthState: () => credentialsLoader.loadAuthState(),
|
|
181
|
-
pollIntervalSec: server.pollIntervalSec,
|
|
182
|
-
logger,
|
|
183
|
-
onFeedPolled: async (payload: FeedResponse) => {
|
|
184
|
-
resetAuthPromptGate();
|
|
185
|
-
await notifier.deliver(buildFeedPayloadMessage(getPromptContext(), payload));
|
|
186
|
-
},
|
|
187
|
-
onAuthRequired: notifyAuthRequired,
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
const pmPollingClient = new EigenFluxPmPollingClient({
|
|
191
|
-
apiUrl: server.endpoint,
|
|
192
|
-
getAuthState: () => credentialsLoader.loadAuthState(),
|
|
193
|
-
pollIntervalSec: server.pmPollIntervalSec,
|
|
194
|
-
logger,
|
|
195
|
-
onPmFetched: async (payload: PmFetchResponse) => {
|
|
196
|
-
resetAuthPromptGate();
|
|
197
|
-
await notifier.deliver(buildPmPayloadMessage(getPromptContext(), payload));
|
|
198
|
-
},
|
|
199
|
-
onAuthRequired: notifyAuthRequired,
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
server,
|
|
204
|
-
credentialsLoader,
|
|
205
|
-
notifier,
|
|
206
|
-
pollingClient,
|
|
207
|
-
pmPollingClient,
|
|
208
|
-
getPromptContext,
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function registerServices(
|
|
213
|
-
api: OpenClawPluginApi,
|
|
214
|
-
logger: Logger,
|
|
215
|
-
runtimes: ServerRuntime[]
|
|
216
|
-
): void {
|
|
217
|
-
for (const runtime of runtimes) {
|
|
218
|
-
api.registerService({
|
|
219
|
-
id: `eigenflux:${toServiceIdSegment(runtime.server.name)}`,
|
|
220
|
-
start: async () => {
|
|
221
|
-
logger.info(`Starting EigenFlux polling services for server=${runtime.server.name}`);
|
|
222
|
-
await runtime.pollingClient.start();
|
|
223
|
-
await runtime.pmPollingClient.start();
|
|
224
|
-
},
|
|
225
|
-
stop: async () => {
|
|
226
|
-
logger.info(`Stopping EigenFlux polling services for server=${runtime.server.name}`);
|
|
227
|
-
runtime.pollingClient.stop();
|
|
228
|
-
runtime.pmPollingClient.stop();
|
|
229
|
-
},
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function registerCommand(
|
|
235
|
-
api: OpenClawPluginApi,
|
|
236
|
-
logger: Logger,
|
|
237
|
-
runtimes: ServerRuntime[]
|
|
238
|
-
): void {
|
|
239
|
-
if (!api.registerCommand) {
|
|
240
|
-
logger.warn('registerCommand API unavailable; skipping /eigenflux command registration');
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
api.registerCommand({
|
|
245
|
-
name: 'eigenflux',
|
|
246
|
-
description: 'EigenFlux plugin commands: auth, profile, servers, feed, pm, here',
|
|
247
|
-
acceptsArgs: true,
|
|
248
|
-
handler: async (ctx) => {
|
|
249
|
-
const parsed = parseCommandArgs(ctx.args);
|
|
250
|
-
|
|
251
|
-
if (parsed.command === 'servers') {
|
|
252
|
-
return {
|
|
253
|
-
text: buildServersText(runtimes),
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const selection = selectServerRuntime(runtimes, parsed.serverName);
|
|
258
|
-
if (!selection.runtime) {
|
|
259
|
-
return {
|
|
260
|
-
text: selection.error ?? buildHelpText(runtimes),
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
const runtime = selection.runtime;
|
|
264
|
-
|
|
265
|
-
await rememberCurrentCommandRouteIfPossible(ctx, runtime.server, logger);
|
|
266
|
-
|
|
267
|
-
switch (parsed.command) {
|
|
268
|
-
case 'auth':
|
|
269
|
-
return {
|
|
270
|
-
text: buildAuthStatusText(runtime.server, runtime.credentialsLoader.loadAuthState()),
|
|
271
|
-
};
|
|
272
|
-
case 'profile':
|
|
273
|
-
return {
|
|
274
|
-
text: await buildProfileText(runtime, runtime.credentialsLoader.loadAuthState()),
|
|
275
|
-
};
|
|
276
|
-
case 'feed':
|
|
277
|
-
return {
|
|
278
|
-
text: await buildFeedText(runtime, runtime.credentialsLoader.loadAuthState()),
|
|
279
|
-
};
|
|
280
|
-
case 'pm':
|
|
281
|
-
return {
|
|
282
|
-
text: await buildPmPollText(runtime, runtime.credentialsLoader.loadAuthState()),
|
|
283
|
-
};
|
|
284
|
-
case 'here':
|
|
285
|
-
return {
|
|
286
|
-
text: await buildHereText(ctx, runtime.server, logger),
|
|
287
|
-
};
|
|
288
|
-
default:
|
|
289
|
-
return {
|
|
290
|
-
text: buildHelpText(runtimes),
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
},
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function parseCommandArgs(args: string | undefined): ParsedCommandArgs {
|
|
298
|
-
const tokens = args?.trim().length ? args.trim().split(/\s+/u) : [];
|
|
299
|
-
let serverName: string | undefined;
|
|
300
|
-
const filtered: string[] = [];
|
|
301
|
-
|
|
302
|
-
for (let index = 0; index < tokens.length; index += 1) {
|
|
303
|
-
const token = tokens[index];
|
|
304
|
-
if ((token === '--server' || token === '-s') && tokens[index + 1]) {
|
|
305
|
-
serverName = tokens[index + 1];
|
|
306
|
-
index += 1;
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
filtered.push(token);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const command = filtered[0]?.toLowerCase() ?? '';
|
|
313
|
-
return {
|
|
314
|
-
command,
|
|
315
|
-
serverName,
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function selectServerRuntime(
|
|
320
|
-
runtimes: ServerRuntime[],
|
|
321
|
-
requestedServerName: string | undefined
|
|
322
|
-
): ServerRuntimeSelection {
|
|
323
|
-
if (runtimes.length === 0) {
|
|
324
|
-
return {
|
|
325
|
-
error: 'No EigenFlux servers are configured.',
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (!requestedServerName) {
|
|
330
|
-
return {
|
|
331
|
-
runtime: runtimes[0],
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const normalizedRequestedName = requestedServerName.trim().toLowerCase();
|
|
336
|
-
const runtime = runtimes.find(
|
|
337
|
-
(item) => item.server.name.trim().toLowerCase() === normalizedRequestedName
|
|
338
|
-
);
|
|
339
|
-
if (runtime) {
|
|
340
|
-
return { runtime };
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
return {
|
|
344
|
-
error: [
|
|
345
|
-
`Unknown EigenFlux server: ${requestedServerName}`,
|
|
346
|
-
`Available servers: ${runtimes.map((item) => item.server.name).join(', ')}`,
|
|
347
|
-
].join('\n'),
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function buildServersText(runtimes: ServerRuntime[]): string {
|
|
352
|
-
if (runtimes.length === 0) {
|
|
353
|
-
return 'No EigenFlux servers are configured.';
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const defaultRuntime = runtimes[0];
|
|
357
|
-
|
|
358
|
-
return [
|
|
359
|
-
'EigenFlux servers:',
|
|
360
|
-
...runtimes.map((runtime) => {
|
|
361
|
-
const flags = [
|
|
362
|
-
runtime.server.enabled ? 'enabled' : 'disabled',
|
|
363
|
-
defaultRuntime?.server.name === runtime.server.name ? 'default' : null,
|
|
364
|
-
]
|
|
365
|
-
.filter(Boolean)
|
|
366
|
-
.join(', ');
|
|
367
|
-
return `- ${runtime.server.name}: ${flags}; endpoint=${runtime.server.endpoint}; workdir=${runtime.server.workdir}`;
|
|
368
|
-
}),
|
|
369
|
-
].join('\n');
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function buildHelpText(runtimes: ServerRuntime[]): string {
|
|
373
|
-
const defaultRuntime = runtimes[0];
|
|
374
|
-
const availableCommands = Array.from(COMMAND_NAME_SET).join('|');
|
|
375
|
-
|
|
376
|
-
return [
|
|
377
|
-
`Usage: /eigenflux [--server <name>] <${availableCommands}>`,
|
|
378
|
-
defaultRuntime ? `Default server: ${defaultRuntime.server.name}` : undefined,
|
|
379
|
-
runtimes.length > 0
|
|
380
|
-
? `Available servers: ${runtimes.map((runtime) => runtime.server.name).join(', ')}`
|
|
381
|
-
: undefined,
|
|
382
|
-
'',
|
|
383
|
-
'/eigenflux auth',
|
|
384
|
-
'Show current EigenFlux credential status.',
|
|
385
|
-
'',
|
|
386
|
-
'/eigenflux profile',
|
|
387
|
-
'Fetch /api/v1/agents/me with the current access token.',
|
|
388
|
-
'',
|
|
389
|
-
'/eigenflux servers',
|
|
390
|
-
'List configured EigenFlux servers.',
|
|
391
|
-
'',
|
|
392
|
-
'/eigenflux feed',
|
|
393
|
-
'Run one feed refresh and return the raw feed payload.',
|
|
394
|
-
'',
|
|
395
|
-
'/eigenflux pm',
|
|
396
|
-
'Run one PM fetch and return the raw PM payload.',
|
|
397
|
-
'',
|
|
398
|
-
'/eigenflux here',
|
|
399
|
-
'Remember the current conversation as the default delivery route for the selected server.',
|
|
400
|
-
]
|
|
401
|
-
.filter(Boolean)
|
|
402
|
-
.join('\n');
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function readNonEmptyString(value: unknown): string | undefined {
|
|
406
|
-
if (typeof value !== 'string') {
|
|
407
|
-
return undefined;
|
|
408
|
-
}
|
|
409
|
-
const trimmed = value.trim();
|
|
410
|
-
return trimmed.length > 0 ? trimmed : undefined;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function normalizeChannel(value: unknown): string | undefined {
|
|
414
|
-
return readNonEmptyString(value)?.toLowerCase();
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function isInternalAgentSessionKey(value: string | undefined): boolean {
|
|
418
|
-
const trimmed = readNonEmptyString(value);
|
|
419
|
-
if (!trimmed || trimmed === 'main') {
|
|
420
|
-
return true;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const parts = trimmed.split(':').filter((part) => part.length > 0);
|
|
424
|
-
return parts[0]?.toLowerCase() === 'agent' && parts[2]?.toLowerCase() === 'main';
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function isNormalizedConversationTarget(value: string): boolean {
|
|
428
|
-
return /^(user|chat|channel|room):/u.test(value);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function normalizeReplyTarget(
|
|
432
|
-
value: unknown,
|
|
433
|
-
channel: string | undefined,
|
|
434
|
-
fallbackKind?: 'user' | 'chat' | 'channel' | 'room'
|
|
435
|
-
): string | undefined {
|
|
436
|
-
const trimmed = readNonEmptyString(value);
|
|
437
|
-
if (!trimmed) {
|
|
438
|
-
return undefined;
|
|
439
|
-
}
|
|
440
|
-
if (isNormalizedConversationTarget(trimmed)) {
|
|
441
|
-
return trimmed;
|
|
442
|
-
}
|
|
443
|
-
if (channel && trimmed.startsWith(`${channel}:`)) {
|
|
444
|
-
const inner = trimmed.slice(channel.length + 1).trim();
|
|
445
|
-
if (isNormalizedConversationTarget(inner)) {
|
|
446
|
-
return inner;
|
|
447
|
-
}
|
|
448
|
-
return fallbackKind ? `${fallbackKind}:${inner}` : inner;
|
|
449
|
-
}
|
|
450
|
-
return fallbackKind ? `${fallbackKind}:${trimmed}` : trimmed;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
async function resolveCurrentCommandRoute(
|
|
454
|
-
ctx: CommandRouteContext,
|
|
455
|
-
serverConfig: ResolvedEigenFluxServerConfig,
|
|
456
|
-
logger: Logger
|
|
457
|
-
) {
|
|
458
|
-
const channel = normalizeChannel(ctx.channel);
|
|
459
|
-
const accountId = readNonEmptyString(ctx.accountId);
|
|
460
|
-
|
|
461
|
-
let replyChannel = channel;
|
|
462
|
-
let replyTo =
|
|
463
|
-
normalizeReplyTarget(ctx.to, channel) ?? normalizeReplyTarget(ctx.from, channel, 'user');
|
|
464
|
-
let replyAccountId = accountId;
|
|
465
|
-
|
|
466
|
-
if (typeof ctx.getCurrentConversationBinding === 'function') {
|
|
467
|
-
try {
|
|
468
|
-
const binding = await ctx.getCurrentConversationBinding();
|
|
469
|
-
if (binding) {
|
|
470
|
-
replyChannel = normalizeChannel(binding.channel) ?? replyChannel;
|
|
471
|
-
replyTo =
|
|
472
|
-
normalizeReplyTarget(binding.conversationId, replyChannel) ??
|
|
473
|
-
normalizeReplyTarget(binding.parentConversationId, replyChannel) ??
|
|
474
|
-
replyTo;
|
|
475
|
-
replyAccountId = readNonEmptyString(binding.accountId) ?? replyAccountId;
|
|
476
|
-
}
|
|
477
|
-
} catch (error) {
|
|
478
|
-
logger.debug(
|
|
479
|
-
`Failed to read current conversation binding: ${error instanceof Error ? error.message : String(error)}`
|
|
480
|
-
);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
if (!replyChannel || !replyTo) {
|
|
485
|
-
return undefined;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
const route = resolveNotificationRoute(
|
|
489
|
-
{
|
|
490
|
-
sessionKey: 'main',
|
|
491
|
-
agentId: serverConfig.agentId,
|
|
492
|
-
replyChannel,
|
|
493
|
-
replyTo,
|
|
494
|
-
replyAccountId,
|
|
495
|
-
sessionStorePath: readServerSessionStorePath(serverConfig),
|
|
496
|
-
workdir: serverConfig.workdir,
|
|
497
|
-
routeOverrides: {
|
|
498
|
-
sessionKey: false,
|
|
499
|
-
agentId: false,
|
|
500
|
-
replyChannel: true,
|
|
501
|
-
replyTo: true,
|
|
502
|
-
replyAccountId: replyAccountId !== undefined,
|
|
503
|
-
},
|
|
504
|
-
},
|
|
505
|
-
logger
|
|
506
|
-
);
|
|
507
|
-
|
|
508
|
-
if (!route.replyChannel || !route.replyTo) {
|
|
509
|
-
return undefined;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
if (isInternalAgentSessionKey(route.sessionKey)) {
|
|
513
|
-
const configuredSessionKey = readNonEmptyString(serverConfig.sessionKey);
|
|
514
|
-
if (configuredSessionKey && !isInternalAgentSessionKey(configuredSessionKey)) {
|
|
515
|
-
return {
|
|
516
|
-
sessionKey: configuredSessionKey,
|
|
517
|
-
agentId: readNonEmptyString(serverConfig.agentId) ?? route.agentId,
|
|
518
|
-
replyChannel: route.replyChannel,
|
|
519
|
-
replyTo: route.replyTo,
|
|
520
|
-
replyAccountId: route.replyAccountId,
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
return route;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
async function buildHereText(
|
|
529
|
-
ctx: CommandRouteContext,
|
|
530
|
-
serverConfig: ResolvedEigenFluxServerConfig,
|
|
531
|
-
logger: Logger
|
|
532
|
-
): Promise<string> {
|
|
533
|
-
const route = await resolveCurrentCommandRoute(ctx, serverConfig, logger);
|
|
534
|
-
if (!route || route.sessionKey === 'main' || route.sessionKey.endsWith(':main')) {
|
|
535
|
-
return [
|
|
536
|
-
`Unable to resolve the current external session for server=${serverConfig.name}.`,
|
|
537
|
-
'Run `/eigenflux here` inside the target conversation after OpenClaw has already created a session for it.',
|
|
538
|
-
].join('\n');
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
const saved = writeStoredNotificationRoute(serverConfig.workdir, route, logger);
|
|
542
|
-
if (!saved) {
|
|
543
|
-
return `Failed to persist the current EigenFlux route for server=${serverConfig.name}; check plugin logs for details.`;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
return [
|
|
547
|
-
`EigenFlux server ${serverConfig.name} will deliver to this conversation by default:`,
|
|
548
|
-
`sessionKey: ${route.sessionKey}`,
|
|
549
|
-
`agentId: ${route.agentId}`,
|
|
550
|
-
`channel: ${route.replyChannel ?? 'unknown'}`,
|
|
551
|
-
`target: ${route.replyTo ?? 'unknown'}`,
|
|
552
|
-
route.replyAccountId ? `account: ${route.replyAccountId}` : undefined,
|
|
553
|
-
]
|
|
554
|
-
.filter(Boolean)
|
|
555
|
-
.join('\n');
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
async function rememberCurrentCommandRouteIfPossible(
|
|
559
|
-
ctx: CommandRouteContext,
|
|
560
|
-
serverConfig: ResolvedEigenFluxServerConfig,
|
|
561
|
-
logger: Logger
|
|
562
|
-
): Promise<void> {
|
|
563
|
-
const route = await resolveCurrentCommandRoute(ctx, serverConfig, logger);
|
|
564
|
-
if (!route || route.sessionKey === 'main' || route.sessionKey.endsWith(':main')) {
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
if (writeStoredNotificationRoute(serverConfig.workdir, route, logger)) {
|
|
569
|
-
logger.debug(
|
|
570
|
-
`Remembered current command route for server=${serverConfig.name}: session_key=${route.sessionKey}, channel=${route.replyChannel ?? 'unknown'}, to=${route.replyTo ?? 'unknown'}`
|
|
571
|
-
);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
async function buildProfileText(
|
|
576
|
-
runtime: ServerRuntime,
|
|
577
|
-
authState: AuthState
|
|
578
|
-
): Promise<string> {
|
|
579
|
-
if (authState.status !== 'available') {
|
|
580
|
-
return buildAuthRequiredMessage(runtime.getPromptContext(), {
|
|
581
|
-
authEvent: {
|
|
582
|
-
reason: authState.status === 'expired' ? 'expired_token' : 'missing_token',
|
|
583
|
-
credentialsPath: authState.credentialsPath,
|
|
584
|
-
source: authState.source,
|
|
585
|
-
expiresAt: authState.expiresAt,
|
|
586
|
-
},
|
|
587
|
-
authState,
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
try {
|
|
592
|
-
const payload = await fetchJson<ProfileResponseData>(
|
|
593
|
-
`${runtime.server.endpoint}/api/v1/agents/me`,
|
|
594
|
-
authState.accessToken
|
|
595
|
-
);
|
|
596
|
-
return [
|
|
597
|
-
`EigenFlux profile (server=${runtime.server.name}):`,
|
|
598
|
-
'```json',
|
|
599
|
-
safeJsonStringify(payload),
|
|
600
|
-
'```',
|
|
601
|
-
].join('\n');
|
|
602
|
-
} catch (error) {
|
|
603
|
-
return `Failed to fetch profile for server ${runtime.server.name}: ${error instanceof Error ? error.message : String(error)}`;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
async function buildFeedText(
|
|
608
|
-
runtime: ServerRuntime,
|
|
609
|
-
authState: AuthState
|
|
610
|
-
): Promise<string> {
|
|
611
|
-
const result = await runtime.pollingClient.pollOnce({
|
|
612
|
-
notifyFeed: false,
|
|
613
|
-
notifyAuthRequired: false,
|
|
614
|
-
});
|
|
615
|
-
switch (result.kind) {
|
|
616
|
-
case 'success':
|
|
617
|
-
return [
|
|
618
|
-
`EigenFlux feed result (server=${runtime.server.name}):`,
|
|
619
|
-
'```json',
|
|
620
|
-
safeJsonStringify(result.payload),
|
|
621
|
-
'```',
|
|
622
|
-
].join('\n');
|
|
623
|
-
case 'auth_required':
|
|
624
|
-
return buildAuthRequiredMessage(runtime.getPromptContext(), {
|
|
625
|
-
authEvent: result.authEvent,
|
|
626
|
-
authState,
|
|
627
|
-
});
|
|
628
|
-
case 'error':
|
|
629
|
-
return `EigenFlux feed failed for server ${runtime.server.name}: ${result.error.message}`;
|
|
630
|
-
default:
|
|
631
|
-
return `EigenFlux feed finished with an unknown result for server ${runtime.server.name}.`;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
async function buildPmPollText(
|
|
636
|
-
runtime: ServerRuntime,
|
|
637
|
-
authState: AuthState
|
|
638
|
-
): Promise<string> {
|
|
639
|
-
const result = await runtime.pmPollingClient.pollOnce({
|
|
640
|
-
notifyFeed: false,
|
|
641
|
-
notifyAuthRequired: false,
|
|
642
|
-
});
|
|
643
|
-
switch (result.kind) {
|
|
644
|
-
case 'success':
|
|
645
|
-
return [
|
|
646
|
-
`EigenFlux PM poll result (server=${runtime.server.name}):`,
|
|
647
|
-
'```json',
|
|
648
|
-
safeJsonStringify(result.payload),
|
|
649
|
-
'```',
|
|
650
|
-
].join('\n');
|
|
651
|
-
case 'auth_required':
|
|
652
|
-
return buildAuthRequiredMessage(runtime.getPromptContext(), {
|
|
653
|
-
authEvent: result.authEvent,
|
|
654
|
-
authState,
|
|
655
|
-
});
|
|
656
|
-
case 'error':
|
|
657
|
-
return `EigenFlux PM poll failed for server ${runtime.server.name}: ${result.error.message}`;
|
|
658
|
-
default:
|
|
659
|
-
return `EigenFlux PM poll finished with an unknown result for server ${runtime.server.name}.`;
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
async function fetchJson<T extends JsonRecord>(
|
|
664
|
-
url: string,
|
|
665
|
-
accessToken: string
|
|
666
|
-
): Promise<JsonApiSuccess<T>> {
|
|
667
|
-
const response = await fetch(url, {
|
|
668
|
-
method: 'GET',
|
|
669
|
-
headers: {
|
|
670
|
-
Authorization: `Bearer ${accessToken}`,
|
|
671
|
-
'Content-Type': 'application/json',
|
|
672
|
-
'User-Agent': PLUGIN_CONFIG.USER_AGENT,
|
|
673
|
-
},
|
|
674
|
-
});
|
|
675
|
-
|
|
676
|
-
if (response.status === 401) {
|
|
677
|
-
throw new Error('HTTP 401: unauthorized');
|
|
678
|
-
}
|
|
679
|
-
if (!response.ok) {
|
|
680
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
const payload = (await response.json()) as JsonApiSuccess<T>;
|
|
684
|
-
if (payload.code !== 0) {
|
|
685
|
-
throw new Error(`API error: ${payload.msg}`);
|
|
686
|
-
}
|
|
687
|
-
return payload;
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
function buildAuthStatusText(
|
|
691
|
-
serverConfig: ResolvedEigenFluxServerConfig,
|
|
692
|
-
authState: AuthState
|
|
693
|
-
): string {
|
|
694
|
-
const lines = [`EigenFlux auth status (server=${serverConfig.name}):`];
|
|
695
|
-
lines.push(`- workdir: ${serverConfig.workdir}`);
|
|
696
|
-
lines.push(`- credentials_path: ${authState.credentialsPath}`);
|
|
697
|
-
lines.push(`- status: ${authState.status}`);
|
|
698
|
-
if (authState.source) {
|
|
699
|
-
lines.push(`- source: ${authState.source}`);
|
|
700
|
-
}
|
|
701
|
-
if (authState.expiresAt) {
|
|
702
|
-
lines.push(`- expires_at: ${authState.expiresAt}`);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
if (authState.status === 'available') {
|
|
706
|
-
lines.push(`- token: ${maskToken(authState.accessToken)}`);
|
|
707
|
-
} else {
|
|
708
|
-
lines.push('- token: unavailable');
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
return lines.join('\n');
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function buildAuthRequiredMessage(
|
|
715
|
-
promptContext: EigenFluxPromptServerContext,
|
|
716
|
-
{ authEvent, authState }: AuthPromptContext
|
|
717
|
-
): string {
|
|
718
|
-
return buildAuthRequiredPromptTemplate({
|
|
719
|
-
...promptContext,
|
|
720
|
-
authEvent,
|
|
721
|
-
maskedToken: authState?.status === 'available' ? maskToken(authState.accessToken) : undefined,
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
function buildFeedPayloadMessage(
|
|
726
|
-
promptContext: EigenFluxPromptServerContext,
|
|
727
|
-
payload: FeedResponse
|
|
728
|
-
): string {
|
|
729
|
-
return buildFeedPayloadPromptTemplate(payload, promptContext);
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
function buildPmPayloadMessage(
|
|
733
|
-
promptContext: EigenFluxPromptServerContext,
|
|
734
|
-
payload: PmFetchResponse
|
|
735
|
-
): string {
|
|
736
|
-
return buildPmPayloadPromptTemplate(payload, promptContext);
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
function maskToken(token: string): string {
|
|
740
|
-
const trimmed = token.trim();
|
|
741
|
-
if (trimmed.length <= 10) {
|
|
742
|
-
return `${trimmed.slice(0, 2)}***`;
|
|
743
|
-
}
|
|
744
|
-
return `${trimmed.slice(0, 6)}...${trimmed.slice(-4)}`;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
function safeJsonStringify(value: unknown): string {
|
|
748
|
-
try {
|
|
749
|
-
return JSON.stringify(value, null, 2);
|
|
750
|
-
} catch {
|
|
751
|
-
return String(value);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
function toServiceIdSegment(name: string): string {
|
|
756
|
-
const sanitized = name.trim().toLowerCase().replace(/[^a-z0-9._-]+/gu, '-');
|
|
757
|
-
return sanitized || 'default';
|
|
758
|
-
}
|