@omnixal/openclaw-nats-plugin 0.1.9 → 0.1.11
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/cli/bun-setup.ts +2 -0
- package/package.json +1 -1
- package/sidecar/src/consumer/{consumer.service.ts → consumer.controller.ts} +12 -33
- package/sidecar/src/consumer/consumer.module.ts +3 -4
- package/sidecar/src/health/health.module.ts +1 -2
- package/sidecar/src/health/health.service.ts +11 -4
- package/sidecar/src/index.ts +23 -0
- package/sidecar/src/nats-streams/nats-streams.module.ts +2 -3
- package/sidecar/src/nats-streams/stream-setup.controller.ts +64 -0
- package/sidecar/src/publisher/publisher.module.ts +0 -2
- package/sidecar/src/publisher/publisher.service.ts +3 -8
- package/sidecar/src/nats-streams/nats-adapter.service.ts +0 -133
package/cli/bun-setup.ts
CHANGED
|
@@ -58,11 +58,13 @@ export async function bunSetup(): Promise<void> {
|
|
|
58
58
|
writeEnvVariables(envVars);
|
|
59
59
|
|
|
60
60
|
// Write .env into sidecar dir so loadDotEnv picks it up
|
|
61
|
+
// Explicit localhost values override any container-level env (e.g. OPENCLAW_WS_URL=ws://openclaw:...)
|
|
61
62
|
const sidecarEnv = [
|
|
62
63
|
`PORT=3104`,
|
|
63
64
|
`DB_PATH=${join(DATA_DIR, 'nats-sidecar.db')}`,
|
|
64
65
|
`NATS_SERVERS=nats://127.0.0.1:4222`,
|
|
65
66
|
`NATS_PLUGIN_API_KEY=${apiKey}`,
|
|
67
|
+
`OPENCLAW_WS_URL=ws://127.0.0.1:18789`,
|
|
66
68
|
].join('\n');
|
|
67
69
|
writeFileSync(join(SIDECAR_DIR, '.env'), sidecarEnv, 'utf-8');
|
|
68
70
|
|
package/package.json
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { Message, Subscription } from '@onebun/core';
|
|
3
|
-
import { NatsAdapterService } from '../nats-streams/nats-adapter.service';
|
|
1
|
+
import { Controller, BaseController, Subscribe, OnQueueReady, type Message } from '@onebun/core';
|
|
4
2
|
import { PipelineService } from '../pre-handlers/pipeline.service';
|
|
5
3
|
import { GatewayClientService } from '../gateway/gateway-client.service';
|
|
6
4
|
import { PendingService } from '../pending/pending.service';
|
|
7
5
|
import type { NatsEventEnvelope } from '../publisher/envelope';
|
|
8
6
|
|
|
9
|
-
@
|
|
10
|
-
export class
|
|
11
|
-
private subscription: Subscription | null = null;
|
|
12
|
-
|
|
7
|
+
@Controller('/consumer')
|
|
8
|
+
export class ConsumerController extends BaseController {
|
|
13
9
|
constructor(
|
|
14
|
-
private natsAdapter: NatsAdapterService,
|
|
15
10
|
private pipeline: PipelineService,
|
|
16
11
|
private gatewayClient: GatewayClientService,
|
|
17
12
|
private pendingService: PendingService,
|
|
@@ -19,26 +14,17 @@ export class ConsumerService extends BaseService implements OnModuleInit, OnModu
|
|
|
19
14
|
super();
|
|
20
15
|
}
|
|
21
16
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const consumerName = this.config.get('consumer.name');
|
|
30
|
-
this.subscription = await this.natsAdapter.subscribe(
|
|
31
|
-
'agent.inbound.>',
|
|
32
|
-
(message: Message<unknown>) => this.handleInbound(message),
|
|
33
|
-
{ ackMode: 'manual', group: consumerName },
|
|
34
|
-
);
|
|
35
|
-
this.logger.info(`Consuming from agent_inbound as ${consumerName}`);
|
|
36
|
-
} catch (err: any) {
|
|
37
|
-
this.logger.warn(`Failed to start consumer: ${err?.message}`);
|
|
38
|
-
}
|
|
17
|
+
@OnQueueReady()
|
|
18
|
+
onReady() {
|
|
19
|
+
const consumerName = this.config.get('consumer.name');
|
|
20
|
+
this.logger.info(`Queue connected, consuming as ${consumerName}`);
|
|
39
21
|
}
|
|
40
22
|
|
|
41
|
-
|
|
23
|
+
@Subscribe('agent.inbound.>', {
|
|
24
|
+
ackMode: 'manual',
|
|
25
|
+
group: 'openclaw-main',
|
|
26
|
+
})
|
|
27
|
+
async handleInbound(message: Message<unknown>): Promise<void> {
|
|
42
28
|
try {
|
|
43
29
|
const envelope = this.extractEnvelope(message);
|
|
44
30
|
|
|
@@ -103,11 +89,4 @@ export class ConsumerService extends BaseService implements OnModuleInit, OnModu
|
|
|
103
89
|
private formatMessage(envelope: NatsEventEnvelope): string {
|
|
104
90
|
return `[NATS:${envelope.subject}] ${JSON.stringify(envelope.payload)}`;
|
|
105
91
|
}
|
|
106
|
-
|
|
107
|
-
async onModuleDestroy(): Promise<void> {
|
|
108
|
-
if (this.subscription) {
|
|
109
|
-
await this.subscription.unsubscribe();
|
|
110
|
-
this.subscription = null;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
92
|
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { Module } from '@onebun/core';
|
|
2
|
-
import {
|
|
3
|
-
import { NatsStreamsModule } from '../nats-streams/nats-streams.module';
|
|
2
|
+
import { ConsumerController } from './consumer.controller';
|
|
4
3
|
import { PreHandlersModule } from '../pre-handlers/pre-handlers.module';
|
|
5
4
|
import { GatewayClientModule } from '../gateway/gateway-client.module';
|
|
6
5
|
import { PendingModule } from '../pending/pending.module';
|
|
7
6
|
|
|
8
7
|
@Module({
|
|
9
|
-
imports: [
|
|
10
|
-
|
|
8
|
+
imports: [PreHandlersModule, GatewayClientModule, PendingModule],
|
|
9
|
+
controllers: [ConsumerController],
|
|
11
10
|
})
|
|
12
11
|
export class ConsumerModule {}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { Module } from '@onebun/core';
|
|
2
2
|
import { HealthController } from './health.controller';
|
|
3
3
|
import { HealthService } from './health.service';
|
|
4
|
-
import { NatsStreamsModule } from '../nats-streams/nats-streams.module';
|
|
5
4
|
import { GatewayClientModule } from '../gateway/gateway-client.module';
|
|
6
5
|
import { PendingModule } from '../pending/pending.module';
|
|
7
6
|
|
|
8
7
|
@Module({
|
|
9
|
-
imports: [
|
|
8
|
+
imports: [GatewayClientModule, PendingModule],
|
|
10
9
|
controllers: [HealthController],
|
|
11
10
|
providers: [HealthService],
|
|
12
11
|
})
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { Service, BaseService } from '@onebun/core';
|
|
2
|
-
import { NatsAdapterService } from '../nats-streams/nats-adapter.service';
|
|
1
|
+
import { Service, BaseService, QueueService } from '@onebun/core';
|
|
3
2
|
import { GatewayClientService } from '../gateway/gateway-client.service';
|
|
4
3
|
import { PendingService } from '../pending/pending.service';
|
|
5
4
|
|
|
@@ -20,19 +19,27 @@ export class HealthService extends BaseService {
|
|
|
20
19
|
private readonly startedAt = Date.now();
|
|
21
20
|
|
|
22
21
|
constructor(
|
|
23
|
-
private
|
|
22
|
+
private queueService: QueueService,
|
|
24
23
|
private gateway: GatewayClientService,
|
|
25
24
|
private pending: PendingService,
|
|
26
25
|
) {
|
|
27
26
|
super();
|
|
28
27
|
}
|
|
29
28
|
|
|
29
|
+
private isQueueConnected(): boolean {
|
|
30
|
+
try {
|
|
31
|
+
return this.queueService.getAdapter().isConnected();
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
async getStatus(): Promise<HealthStatus> {
|
|
31
38
|
const pendingCount = await this.pending.countPending();
|
|
32
39
|
|
|
33
40
|
return {
|
|
34
41
|
nats: {
|
|
35
|
-
connected: this.
|
|
42
|
+
connected: this.isQueueConnected(),
|
|
36
43
|
url: this.config.get('nats.servers'),
|
|
37
44
|
},
|
|
38
45
|
gateway: {
|
package/sidecar/src/index.ts
CHANGED
|
@@ -1,13 +1,36 @@
|
|
|
1
1
|
import { OneBunApplication } from '@onebun/core';
|
|
2
|
+
import { JetStreamQueueAdapter, type JetStreamAdapterOptions } from '@onebun/nats';
|
|
2
3
|
import { AppModule } from './app.module';
|
|
3
4
|
import { envSchema } from './config';
|
|
4
5
|
|
|
6
|
+
const natsServers = Bun.env.NATS_SERVERS ?? 'nats://localhost:4222';
|
|
7
|
+
const ackWaitMs = Number(Bun.env.NATS_ACK_WAIT_MS ?? 30000);
|
|
8
|
+
const maxDeliver = Number(Bun.env.NATS_MAX_DELIVER ?? 3);
|
|
9
|
+
|
|
5
10
|
const app = new OneBunApplication(AppModule, {
|
|
6
11
|
development: Bun.env.NODE_ENV !== 'production',
|
|
7
12
|
envSchema,
|
|
8
13
|
envOptions: {
|
|
9
14
|
loadDotEnv: true,
|
|
10
15
|
},
|
|
16
|
+
queue: {
|
|
17
|
+
adapter: JetStreamQueueAdapter as any,
|
|
18
|
+
options: {
|
|
19
|
+
servers: natsServers,
|
|
20
|
+
stream: 'agent_inbound',
|
|
21
|
+
createStream: true,
|
|
22
|
+
streamConfig: {
|
|
23
|
+
subjects: ['agent.inbound.>'],
|
|
24
|
+
retention: 'workqueue',
|
|
25
|
+
storage: 'file',
|
|
26
|
+
replicas: 1,
|
|
27
|
+
},
|
|
28
|
+
consumerConfig: {
|
|
29
|
+
ackWait: ackWaitMs * 1_000_000, // ms → ns
|
|
30
|
+
maxDeliver,
|
|
31
|
+
},
|
|
32
|
+
} as JetStreamAdapterOptions,
|
|
33
|
+
},
|
|
11
34
|
});
|
|
12
35
|
|
|
13
36
|
app.start()
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { Module } from '@onebun/core';
|
|
2
|
-
import {
|
|
2
|
+
import { StreamSetupController } from './stream-setup.controller';
|
|
3
3
|
|
|
4
4
|
@Module({
|
|
5
|
-
|
|
6
|
-
exports: [NatsAdapterService],
|
|
5
|
+
controllers: [StreamSetupController],
|
|
7
6
|
})
|
|
8
7
|
export class NatsStreamsModule {}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Controller, BaseController, OnQueueReady, QueueService } from '@onebun/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ensures additional JetStream streams (agent_events, agent_dlq) exist
|
|
5
|
+
* once the queue adapter is connected.
|
|
6
|
+
*/
|
|
7
|
+
@Controller('/nats-setup')
|
|
8
|
+
export class StreamSetupController extends BaseController {
|
|
9
|
+
constructor(private queueService: QueueService) {
|
|
10
|
+
super();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@OnQueueReady()
|
|
14
|
+
async onReady(): Promise<void> {
|
|
15
|
+
await this.ensureExtraStreams();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
private async ensureExtraStreams(): Promise<void> {
|
|
19
|
+
try {
|
|
20
|
+
const adapter = this.queueService.getAdapter();
|
|
21
|
+
const nc = (adapter as any)?.client?.getConnection?.();
|
|
22
|
+
if (!nc) {
|
|
23
|
+
this.logger.warn('Cannot access NATS connection for stream creation');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const jsModule = await import('@nats-io/jetstream');
|
|
28
|
+
const jsm = await jsModule.jetstreamManager(nc);
|
|
29
|
+
|
|
30
|
+
const SEVEN_DAYS_NS = 7 * 24 * 60 * 60 * 1e9;
|
|
31
|
+
|
|
32
|
+
await this.ensureStream(jsm, {
|
|
33
|
+
name: 'agent_events',
|
|
34
|
+
subjects: ['agent.events.>'],
|
|
35
|
+
retention: 'limits',
|
|
36
|
+
max_age: SEVEN_DAYS_NS,
|
|
37
|
+
storage: 'file',
|
|
38
|
+
num_replicas: 1,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
await this.ensureStream(jsm, {
|
|
42
|
+
name: 'agent_dlq',
|
|
43
|
+
subjects: ['agent.dlq.>'],
|
|
44
|
+
retention: 'limits',
|
|
45
|
+
max_age: SEVEN_DAYS_NS,
|
|
46
|
+
storage: 'file',
|
|
47
|
+
num_replicas: 1,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
this.logger.info('Additional JetStream streams ensured (agent_events, agent_dlq)');
|
|
51
|
+
} catch (err: any) {
|
|
52
|
+
this.logger.warn(`Failed to ensure extra streams: ${err?.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private async ensureStream(jsm: any, config: Record<string, any>): Promise<void> {
|
|
57
|
+
try {
|
|
58
|
+
await jsm.streams.info(config.name);
|
|
59
|
+
await jsm.streams.update(config.name, config);
|
|
60
|
+
} catch {
|
|
61
|
+
await jsm.streams.add(config);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { Module } from '@onebun/core';
|
|
2
2
|
import { PublisherService } from './publisher.service';
|
|
3
3
|
import { PublisherController } from './publisher.controller';
|
|
4
|
-
import { NatsStreamsModule } from '../nats-streams/nats-streams.module';
|
|
5
4
|
|
|
6
5
|
@Module({
|
|
7
|
-
imports: [NatsStreamsModule],
|
|
8
6
|
controllers: [PublisherController],
|
|
9
7
|
providers: [PublisherService],
|
|
10
8
|
exports: [PublisherService],
|
|
@@ -1,20 +1,15 @@
|
|
|
1
|
-
import { Service, BaseService } from '@onebun/core';
|
|
2
|
-
import { NatsAdapterService } from '../nats-streams/nats-adapter.service';
|
|
1
|
+
import { Service, BaseService, QueueService } from '@onebun/core';
|
|
3
2
|
import { createEnvelope, type EnvelopeMeta } from './envelope';
|
|
4
3
|
|
|
5
4
|
@Service()
|
|
6
5
|
export class PublisherService extends BaseService {
|
|
7
|
-
constructor(private
|
|
6
|
+
constructor(private queueService: QueueService) {
|
|
8
7
|
super();
|
|
9
8
|
}
|
|
10
9
|
|
|
11
10
|
async publish(subject: string, payload: unknown, meta?: EnvelopeMeta): Promise<void> {
|
|
12
|
-
if (!this.natsAdapter.isConnected()) {
|
|
13
|
-
this.logger.warn(`NATS not connected, dropping publish to ${subject}`);
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
11
|
const envelope = createEnvelope(subject, payload, meta);
|
|
17
|
-
await this.
|
|
12
|
+
await this.queueService.publish(subject, envelope);
|
|
18
13
|
this.logger.debug(`Published to ${subject}`, { id: envelope.id });
|
|
19
14
|
}
|
|
20
15
|
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { Service, BaseService, type OnModuleInit, type OnModuleDestroy } from '@onebun/core';
|
|
2
|
-
import type { Subscription, MessageHandler, SubscribeOptions, PublishOptions } from '@onebun/core';
|
|
3
|
-
import { JetStreamQueueAdapter, type JetStreamAdapterOptions } from '@onebun/nats';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Manages the JetStreamQueueAdapter lifecycle with graceful degradation.
|
|
7
|
-
*
|
|
8
|
-
* When NATS is unavailable, the service runs in degraded mode:
|
|
9
|
-
* publish and subscribe operations are silently dropped with warnings.
|
|
10
|
-
*
|
|
11
|
-
* Also ensures additional streams (agent_events, agent_dlq) exist.
|
|
12
|
-
*/
|
|
13
|
-
@Service()
|
|
14
|
-
export class NatsAdapterService extends BaseService implements OnModuleInit, OnModuleDestroy {
|
|
15
|
-
private adapter: JetStreamQueueAdapter | null = null;
|
|
16
|
-
private _connected = false;
|
|
17
|
-
|
|
18
|
-
async onModuleInit(): Promise<void> {
|
|
19
|
-
const servers = this.config.get('nats.servers');
|
|
20
|
-
const reconnectTimeWait = this.config.get('nats.reconnectDelayMs');
|
|
21
|
-
const maxReconnectAttempts = this.config.get('nats.maxReconnectAttempts');
|
|
22
|
-
const ackWaitMs = this.config.get('consumer.ackWaitMs');
|
|
23
|
-
const maxDeliver = this.config.get('consumer.maxDeliver');
|
|
24
|
-
|
|
25
|
-
const options: JetStreamAdapterOptions = {
|
|
26
|
-
servers,
|
|
27
|
-
maxReconnectAttempts,
|
|
28
|
-
reconnectTimeWait,
|
|
29
|
-
stream: 'agent_inbound',
|
|
30
|
-
createStream: true,
|
|
31
|
-
streamConfig: {
|
|
32
|
-
subjects: ['agent.inbound.>'],
|
|
33
|
-
retention: 'workqueue',
|
|
34
|
-
storage: 'file',
|
|
35
|
-
replicas: 1,
|
|
36
|
-
},
|
|
37
|
-
consumerConfig: {
|
|
38
|
-
ackWait: ackWaitMs * 1_000_000, // ms → ns
|
|
39
|
-
maxDeliver,
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
this.adapter = new JetStreamQueueAdapter(options);
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
await this.adapter.connect();
|
|
47
|
-
this._connected = true;
|
|
48
|
-
await this.ensureExtraStreams();
|
|
49
|
-
this.logger.info('NATS JetStream connected');
|
|
50
|
-
} catch (err: any) {
|
|
51
|
-
this.logger.warn(`NATS connection failed, running in degraded mode: ${err?.message}`);
|
|
52
|
-
this.adapter = null;
|
|
53
|
-
this._connected = false;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
isConnected(): boolean {
|
|
58
|
-
return this._connected && this.adapter !== null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async publish<T>(pattern: string, data: T, options?: PublishOptions): Promise<string | null> {
|
|
62
|
-
if (!this.adapter || !this.isConnected()) {
|
|
63
|
-
this.logger.warn(`NATS not connected, dropping publish to ${pattern}`);
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
return await this.adapter.publish(pattern, data, options);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async subscribe<T>(
|
|
70
|
-
pattern: string,
|
|
71
|
-
handler: MessageHandler<T>,
|
|
72
|
-
options?: SubscribeOptions,
|
|
73
|
-
): Promise<Subscription | null> {
|
|
74
|
-
if (!this.adapter || !this.isConnected()) {
|
|
75
|
-
this.logger.warn(`NATS not connected, cannot subscribe to ${pattern}`);
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
return await this.adapter.subscribe(pattern, handler, options);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async onModuleDestroy(): Promise<void> {
|
|
82
|
-
if (this.adapter) {
|
|
83
|
-
await this.adapter.disconnect();
|
|
84
|
-
this.logger.info('NATS JetStream disconnected');
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
private async ensureExtraStreams(): Promise<void> {
|
|
89
|
-
try {
|
|
90
|
-
const nc = (this.adapter as any)?.client?.getConnection?.();
|
|
91
|
-
if (!nc) {
|
|
92
|
-
this.logger.warn('Cannot access NATS connection for stream creation');
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const jsModule = await import('@nats-io/jetstream');
|
|
97
|
-
const jsm = await jsModule.jetstreamManager(nc);
|
|
98
|
-
|
|
99
|
-
const SEVEN_DAYS_NS = 7 * 24 * 60 * 60 * 1e9;
|
|
100
|
-
|
|
101
|
-
await this.ensureStream(jsm, {
|
|
102
|
-
name: 'agent_events',
|
|
103
|
-
subjects: ['agent.events.>'],
|
|
104
|
-
retention: 'limits',
|
|
105
|
-
max_age: SEVEN_DAYS_NS,
|
|
106
|
-
storage: 'file',
|
|
107
|
-
num_replicas: 1,
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
await this.ensureStream(jsm, {
|
|
111
|
-
name: 'agent_dlq',
|
|
112
|
-
subjects: ['agent.dlq.>'],
|
|
113
|
-
retention: 'limits',
|
|
114
|
-
max_age: SEVEN_DAYS_NS,
|
|
115
|
-
storage: 'file',
|
|
116
|
-
num_replicas: 1,
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
this.logger.info('Additional JetStream streams ensured (agent_events, agent_dlq)');
|
|
120
|
-
} catch (err: any) {
|
|
121
|
-
this.logger.warn(`Failed to ensure extra streams: ${err?.message}`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
private async ensureStream(jsm: any, config: Record<string, any>): Promise<void> {
|
|
126
|
-
try {
|
|
127
|
-
await jsm.streams.info(config.name);
|
|
128
|
-
await jsm.streams.update(config.name, config);
|
|
129
|
-
} catch {
|
|
130
|
-
await jsm.streams.add(config);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|