@omnixal/openclaw-nats-plugin 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PLUGIN.md +9 -6
- package/README.md +12 -1
- package/cli/docker-setup.ts +3 -3
- package/cli/setup.ts +0 -6
- package/dashboard/src/lib/CronPanel.svelte +206 -27
- package/dashboard/src/lib/LogsPanel.svelte +211 -0
- package/dashboard/src/lib/RoutesPanel.svelte +157 -13
- package/dashboard/src/lib/api.ts +77 -0
- package/dashboard/src/lib/components/ui/modal/index.ts +1 -0
- package/dashboard/src/lib/components/ui/modal/modal.svelte +49 -0
- package/dashboard/src/lib/utils.ts +8 -0
- package/package.json +1 -1
- package/sidecar/bun.lock +2 -2
- package/sidecar/package.json +1 -1
- package/sidecar/src/app.module.ts +2 -0
- package/sidecar/src/consumer/consumer.controller.ts +20 -12
- package/sidecar/src/consumer/consumer.module.ts +2 -1
- package/sidecar/src/db/migrations/0005_strong_supernaut.sql +13 -0
- package/sidecar/src/db/migrations/meta/0005_snapshot.json +389 -0
- package/sidecar/src/db/migrations/meta/_journal.json +7 -0
- package/sidecar/src/db/schema.ts +17 -0
- package/sidecar/src/logs/log.controller.ts +50 -0
- package/sidecar/src/logs/log.module.ts +11 -0
- package/sidecar/src/logs/log.repository.ts +78 -0
- package/sidecar/src/logs/log.service.ts +116 -0
- package/sidecar/src/router/router.controller.ts +28 -6
- package/sidecar/src/router/router.repository.ts +8 -0
- package/sidecar/src/router/router.service.ts +4 -0
- package/sidecar/src/scheduler/scheduler.controller.ts +32 -3
- package/sidecar/src/scheduler/scheduler.module.ts +2 -1
- package/sidecar/src/scheduler/scheduler.repository.ts +8 -0
- package/sidecar/src/scheduler/scheduler.service.ts +91 -25
- package/sidecar/src/validation/schemas.ts +27 -0
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
Controller,
|
|
3
3
|
Get,
|
|
4
4
|
Post,
|
|
5
|
+
Patch,
|
|
5
6
|
Delete,
|
|
6
7
|
Body,
|
|
7
8
|
Param,
|
|
@@ -12,7 +13,11 @@ import {
|
|
|
12
13
|
} from '@onebun/core';
|
|
13
14
|
import { RouterService } from './router.service';
|
|
14
15
|
import { ApiKeyMiddleware } from '../auth/api-key.middleware';
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
createRouteBodySchema, type CreateRouteBody,
|
|
18
|
+
updateRouteBodySchema, type UpdateRouteBody,
|
|
19
|
+
isValidAgentSubject,
|
|
20
|
+
} from '../validation/schemas';
|
|
16
21
|
|
|
17
22
|
@Controller('/api/routes')
|
|
18
23
|
@UseMiddleware(ApiKeyMiddleware)
|
|
@@ -32,8 +37,10 @@ export class RouterController extends BaseController {
|
|
|
32
37
|
const routes = await this.routerService.listRoutes();
|
|
33
38
|
const now = Date.now();
|
|
34
39
|
const result = routes.map(r => ({
|
|
40
|
+
id: r.id,
|
|
35
41
|
pattern: r.pattern,
|
|
36
42
|
target: r.target,
|
|
43
|
+
priority: r.priority,
|
|
37
44
|
enabled: r.enabled,
|
|
38
45
|
lastDeliveredAt: r.lastDeliveredAt?.toISOString() ?? null,
|
|
39
46
|
lastEventSubject: r.lastEventSubject ?? null,
|
|
@@ -44,18 +51,21 @@ export class RouterController extends BaseController {
|
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
@Get()
|
|
47
|
-
async getRoutes(
|
|
54
|
+
async getRoutes(
|
|
55
|
+
@Query('pattern') pattern?: string,
|
|
56
|
+
@Query('target') target?: string,
|
|
57
|
+
): Promise<OneBunResponse> {
|
|
48
58
|
const filters: { pattern?: string; target?: string } = {};
|
|
49
|
-
if (
|
|
50
|
-
if (
|
|
59
|
+
if (pattern) filters.pattern = pattern;
|
|
60
|
+
if (target) filters.target = target;
|
|
51
61
|
const routes = await this.routerService.listRoutes(filters);
|
|
52
62
|
return this.success(routes);
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
@Post()
|
|
56
66
|
async createRoute(@Body(createRouteBodySchema) body: CreateRouteBody): Promise<OneBunResponse> {
|
|
57
|
-
if (!body.pattern
|
|
58
|
-
return this.error('pattern must start with agent.events.', 400, 400);
|
|
67
|
+
if (!isValidAgentSubject(body.pattern)) {
|
|
68
|
+
return this.error('pattern must start with "agent.events." followed by at least one token and must not end with "."', 400, 400);
|
|
59
69
|
}
|
|
60
70
|
const { route, created } = await this.routerService.subscribe(
|
|
61
71
|
body.pattern,
|
|
@@ -65,6 +75,18 @@ export class RouterController extends BaseController {
|
|
|
65
75
|
return this.success({ ...route, created });
|
|
66
76
|
}
|
|
67
77
|
|
|
78
|
+
@Patch('/:id')
|
|
79
|
+
async updateRoute(
|
|
80
|
+
@Param('id') id: string,
|
|
81
|
+
@Body(updateRouteBodySchema) body: UpdateRouteBody,
|
|
82
|
+
): Promise<OneBunResponse> {
|
|
83
|
+
const updated = await this.routerService.updateById(id, body);
|
|
84
|
+
if (!updated) {
|
|
85
|
+
return this.error('Route not found', 404, 404);
|
|
86
|
+
}
|
|
87
|
+
return this.success(updated);
|
|
88
|
+
}
|
|
89
|
+
|
|
68
90
|
@Delete('/:id')
|
|
69
91
|
async deleteRoute(@Param('id') id: string): Promise<OneBunResponse> {
|
|
70
92
|
const deleted = await this.routerService.deleteById(id);
|
|
@@ -49,6 +49,14 @@ export class RouterRepository extends BaseService {
|
|
|
49
49
|
return { route: result, created };
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
async updateById(id: string, fields: Partial<Pick<DbEventRoute, 'target' | 'priority' | 'enabled'>>): Promise<DbEventRoute | null> {
|
|
53
|
+
const [result] = await this.db.update(eventRoutes)
|
|
54
|
+
.set(fields)
|
|
55
|
+
.where(eq(eventRoutes.id, id))
|
|
56
|
+
.returning();
|
|
57
|
+
return result ?? null;
|
|
58
|
+
}
|
|
59
|
+
|
|
52
60
|
async recordDelivery(routeId: string, subject: string): Promise<void> {
|
|
53
61
|
await this.db.update(eventRoutes)
|
|
54
62
|
.set({
|
|
@@ -54,6 +54,10 @@ export class RouterService extends BaseService {
|
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
async updateById(id: string, fields: { target?: string; priority?: number; enabled?: boolean }): Promise<DbEventRoute | null> {
|
|
58
|
+
return this.repo.updateById(id, fields);
|
|
59
|
+
}
|
|
60
|
+
|
|
57
61
|
async recordDelivery(routeId: string, subject: string): Promise<void> {
|
|
58
62
|
await this.repo.recordDelivery(routeId, subject);
|
|
59
63
|
}
|
|
@@ -4,10 +4,18 @@ import {
|
|
|
4
4
|
UseMiddleware, Subscribe, OnQueueReady,
|
|
5
5
|
type Message,
|
|
6
6
|
type OneBunResponse,
|
|
7
|
+
OnQueueError,
|
|
8
|
+
OnMessageReceived,
|
|
9
|
+
OnMessageProcessed,
|
|
10
|
+
OnMessageFailed,
|
|
7
11
|
} from '@onebun/core';
|
|
8
12
|
import { SchedulerService } from './scheduler.service';
|
|
9
13
|
import { ApiKeyMiddleware } from '../auth/api-key.middleware';
|
|
10
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
createCronBodySchema, type CreateCronBody,
|
|
16
|
+
updateCronBodySchema, type UpdateCronBody,
|
|
17
|
+
isValidAgentSubject,
|
|
18
|
+
} from '../validation/schemas';
|
|
11
19
|
|
|
12
20
|
@Controller('/api/cron')
|
|
13
21
|
@UseMiddleware(ApiKeyMiddleware)
|
|
@@ -18,13 +26,21 @@ export class SchedulerController extends BaseController {
|
|
|
18
26
|
|
|
19
27
|
@OnQueueReady()
|
|
20
28
|
async onQueueReady(): Promise<void> {
|
|
29
|
+
this.logger.info('Queue connected, starting scheduler');
|
|
30
|
+
this.scheduler.markQueueReady();
|
|
21
31
|
await this.scheduler.restoreJobs();
|
|
22
32
|
}
|
|
23
33
|
|
|
34
|
+
@OnQueueError()
|
|
35
|
+
handleError(error: Error) {
|
|
36
|
+
this.logger.error('Queue error (publish stopped)', error);
|
|
37
|
+
this.scheduler.markQueueReady(false);
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
@Post()
|
|
25
41
|
async createJob(@Body(createCronBodySchema) body: CreateCronBody): Promise<OneBunResponse> {
|
|
26
|
-
if (!body.subject
|
|
27
|
-
return this.error('subject must start with agent.events.', 400, 400);
|
|
42
|
+
if (!isValidAgentSubject(body.subject)) {
|
|
43
|
+
return this.error('subject must start with "agent.events." followed by at least one token and must not end with "."', 400, 400);
|
|
28
44
|
}
|
|
29
45
|
const job = await this.scheduler.add({
|
|
30
46
|
name: body.name,
|
|
@@ -42,6 +58,19 @@ export class SchedulerController extends BaseController {
|
|
|
42
58
|
return this.success(jobs);
|
|
43
59
|
}
|
|
44
60
|
|
|
61
|
+
@Patch('/:name')
|
|
62
|
+
async updateJob(
|
|
63
|
+
@Param('name') name: string,
|
|
64
|
+
@Body(updateCronBodySchema) body: UpdateCronBody,
|
|
65
|
+
): Promise<OneBunResponse> {
|
|
66
|
+
if (body.subject !== undefined && !isValidAgentSubject(body.subject)) {
|
|
67
|
+
return this.error('subject must start with "agent.events." followed by at least one token and must not end with "."', 400, 400);
|
|
68
|
+
}
|
|
69
|
+
const updated = await this.scheduler.update(name, body);
|
|
70
|
+
if (!updated) return this.error('Job not found', 404, 404);
|
|
71
|
+
return this.success(updated);
|
|
72
|
+
}
|
|
73
|
+
|
|
45
74
|
@Patch('/:name/toggle')
|
|
46
75
|
async toggleJob(@Param('name') name: string): Promise<OneBunResponse> {
|
|
47
76
|
const result = await this.scheduler.toggle(name);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Module } from '@onebun/core';
|
|
2
2
|
import { PublisherModule } from '../publisher/publisher.module';
|
|
3
|
+
import { LogModule } from '../logs/log.module';
|
|
3
4
|
import { SchedulerRepository } from './scheduler.repository';
|
|
4
5
|
import { SchedulerService } from './scheduler.service';
|
|
5
6
|
import { SchedulerController } from './scheduler.controller';
|
|
6
7
|
|
|
7
8
|
@Module({
|
|
8
|
-
imports: [PublisherModule],
|
|
9
|
+
imports: [PublisherModule, LogModule],
|
|
9
10
|
controllers: [SchedulerController],
|
|
10
11
|
providers: [SchedulerRepository, SchedulerService],
|
|
11
12
|
exports: [SchedulerService],
|
|
@@ -44,6 +44,14 @@ export class SchedulerRepository extends BaseService {
|
|
|
44
44
|
return result;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
async updateByName(name: string, fields: Partial<Pick<DbCronJob, 'expr' | 'subject' | 'payload' | 'timezone' | 'enabled'>>): Promise<DbCronJob | null> {
|
|
48
|
+
const [result] = await this.db.update(cronJobs)
|
|
49
|
+
.set(fields)
|
|
50
|
+
.where(eq(cronJobs.name, name))
|
|
51
|
+
.returning();
|
|
52
|
+
return result ?? null;
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
async deleteByName(name: string): Promise<boolean> {
|
|
48
56
|
const result = await this.db.delete(cronJobs)
|
|
49
57
|
.where(eq(cronJobs.name, name)).returning();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Service, BaseService, QueueService } from '@onebun/core';
|
|
2
2
|
import { SchedulerRepository } from './scheduler.repository';
|
|
3
3
|
import { PublisherService } from '../publisher/publisher.service';
|
|
4
|
+
import { LogService } from '../logs/log.service';
|
|
4
5
|
import { ulid } from 'ulid';
|
|
5
6
|
|
|
6
7
|
interface AddJobInput {
|
|
@@ -13,10 +14,13 @@ interface AddJobInput {
|
|
|
13
14
|
|
|
14
15
|
@Service()
|
|
15
16
|
export class SchedulerService extends BaseService {
|
|
17
|
+
private _queueReady = false;
|
|
18
|
+
|
|
16
19
|
constructor(
|
|
17
20
|
private repo: SchedulerRepository,
|
|
18
21
|
private queueService: QueueService,
|
|
19
22
|
private publisher: PublisherService,
|
|
23
|
+
private logService: LogService,
|
|
20
24
|
) {
|
|
21
25
|
super();
|
|
22
26
|
}
|
|
@@ -25,17 +29,27 @@ export class SchedulerService extends BaseService {
|
|
|
25
29
|
return this.queueService.getScheduler();
|
|
26
30
|
}
|
|
27
31
|
|
|
32
|
+
markQueueReady(isReady = true): void {
|
|
33
|
+
this._queueReady = isReady;
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
async restoreJobs(): Promise<void> {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
try {
|
|
38
|
+
const jobs = await this.repo.findAllEnabled();
|
|
39
|
+
for (const job of jobs) {
|
|
40
|
+
this.scheduler.addCronJob(
|
|
41
|
+
job.name,
|
|
42
|
+
job.expr,
|
|
43
|
+
`scheduler.fire.${job.name}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (jobs.length > 0) {
|
|
47
|
+
this.logger.info(`Restored ${jobs.length} cron jobs from DB`);
|
|
48
|
+
} else {
|
|
49
|
+
this.logger.info('No cron jobs found in DB');
|
|
50
|
+
}
|
|
51
|
+
} catch (err) {
|
|
52
|
+
this.logger.error('Failed to restore cron jobs', err);
|
|
39
53
|
}
|
|
40
54
|
}
|
|
41
55
|
|
|
@@ -65,6 +79,33 @@ export class SchedulerService extends BaseService {
|
|
|
65
79
|
return job;
|
|
66
80
|
}
|
|
67
81
|
|
|
82
|
+
async update(name: string, fields: { cron?: string; subject?: string; payload?: unknown; timezone?: string; enabled?: boolean }) {
|
|
83
|
+
const updates: Record<string, unknown> = {};
|
|
84
|
+
if (fields.cron !== undefined) updates.expr = fields.cron;
|
|
85
|
+
if (fields.subject !== undefined) updates.subject = fields.subject;
|
|
86
|
+
if (fields.payload !== undefined) updates.payload = fields.payload;
|
|
87
|
+
if (fields.timezone !== undefined) updates.timezone = fields.timezone;
|
|
88
|
+
if (fields.enabled !== undefined) updates.enabled = fields.enabled;
|
|
89
|
+
|
|
90
|
+
if (Object.keys(updates).length === 0) {
|
|
91
|
+
return this.repo.findByName(name);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const updated = await this.repo.updateByName(name, updates as any);
|
|
95
|
+
if (!updated) return null;
|
|
96
|
+
|
|
97
|
+
// Re-register in runtime scheduler if expr or enabled changed
|
|
98
|
+
if (this.scheduler.hasJob(name)) {
|
|
99
|
+
this.scheduler.removeJob(name);
|
|
100
|
+
}
|
|
101
|
+
if (updated.enabled) {
|
|
102
|
+
this.scheduler.addCronJob(name, updated.expr, `scheduler.fire.${name}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.logger.info(`Cron job '${name}' updated`);
|
|
106
|
+
return updated;
|
|
107
|
+
}
|
|
108
|
+
|
|
68
109
|
async remove(name: string): Promise<boolean> {
|
|
69
110
|
const deleted = await this.repo.deleteByName(name);
|
|
70
111
|
if (this.scheduler.hasJob(name)) {
|
|
@@ -79,6 +120,8 @@ export class SchedulerService extends BaseService {
|
|
|
79
120
|
const runtime = this.scheduler.getJob(job.name);
|
|
80
121
|
return {
|
|
81
122
|
...job,
|
|
123
|
+
lastRunAt: job.lastRunAt?.getTime() ?? null,
|
|
124
|
+
createdAt: job.createdAt.getTime(),
|
|
82
125
|
nextRun: runtime?.nextRun ?? null,
|
|
83
126
|
isRunning: runtime?.isRunning ?? false,
|
|
84
127
|
};
|
|
@@ -109,30 +152,53 @@ export class SchedulerService extends BaseService {
|
|
|
109
152
|
return this.repo.findByName(name);
|
|
110
153
|
}
|
|
111
154
|
|
|
155
|
+
private buildCronPayload(job: { name: string; payload: unknown }, manual: boolean): Record<string, unknown> {
|
|
156
|
+
const base = (job.payload && typeof job.payload === 'object' && !Array.isArray(job.payload))
|
|
157
|
+
? (job.payload as Record<string, unknown>)
|
|
158
|
+
: {};
|
|
159
|
+
return { ...base, _cron: { jobName: job.name, firedAt: new Date().toISOString(), ...(manual ? { manual: true } : {}) } };
|
|
160
|
+
}
|
|
161
|
+
|
|
112
162
|
async fireNow(name: string): Promise<boolean> {
|
|
113
163
|
const job = await this.repo.findByName(name);
|
|
114
164
|
if (!job) return false;
|
|
115
165
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
166
|
+
if (!this._queueReady) {
|
|
167
|
+
this.logger.warn(`Cron job '${name}' fire skipped — queue not ready`);
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const payload = this.buildCronPayload(job, true);
|
|
173
|
+
await this.publisher.publish(job.subject, payload);
|
|
174
|
+
await this.repo.updateLastRun(name);
|
|
175
|
+
await this.logService.logCronFire(job.id, job.subject, true);
|
|
176
|
+
this.logger.info(`Cron job '${name}' manually fired -> ${job.subject}`);
|
|
177
|
+
return true;
|
|
178
|
+
} catch (err) {
|
|
179
|
+
await this.logService.logError('cron', job.id, job.subject, err);
|
|
180
|
+
throw err;
|
|
181
|
+
}
|
|
124
182
|
}
|
|
125
183
|
|
|
126
184
|
async handleFire(jobName: string): Promise<void> {
|
|
127
185
|
const job = await this.repo.findByName(jobName);
|
|
128
186
|
if (!job || !job.enabled) return;
|
|
129
187
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
188
|
+
if (!this._queueReady) {
|
|
189
|
+
this.logger.warn(`Cron fire skipped for '${jobName}' — queue not ready`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const payload = this.buildCronPayload(job, false);
|
|
195
|
+
await this.publisher.publish(job.subject, payload);
|
|
196
|
+
await this.repo.updateLastRun(job.name);
|
|
197
|
+
await this.logService.logCronFire(job.id, job.subject, false);
|
|
198
|
+
this.logger.debug(`Cron fired: ${job.name} -> ${job.subject}`);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
await this.logService.logError('cron', job.id, job.subject, err);
|
|
201
|
+
this.logger.error(`Cron fire failed: ${job.name}`, err);
|
|
202
|
+
}
|
|
137
203
|
}
|
|
138
204
|
}
|
|
@@ -35,3 +35,30 @@ export const createCronBodySchema = type({
|
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
export type CreateCronBody = typeof createCronBodySchema.infer;
|
|
38
|
+
|
|
39
|
+
export const updateRouteBodySchema = type({
|
|
40
|
+
'target?': 'string',
|
|
41
|
+
'priority?': 'number',
|
|
42
|
+
'enabled?': 'boolean',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export type UpdateRouteBody = typeof updateRouteBodySchema.infer;
|
|
46
|
+
|
|
47
|
+
export const updateCronBodySchema = type({
|
|
48
|
+
'cron?': 'string',
|
|
49
|
+
'subject?': 'string',
|
|
50
|
+
'payload?': 'unknown',
|
|
51
|
+
'timezone?': 'string',
|
|
52
|
+
'enabled?': 'boolean',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export type UpdateCronBody = typeof updateCronBodySchema.infer;
|
|
56
|
+
|
|
57
|
+
/** Validate that subject has content after 'agent.events.' prefix and doesn't end with '.' */
|
|
58
|
+
export function isValidAgentSubject(subject: string): boolean {
|
|
59
|
+
if (!subject.startsWith('agent.events.')) return false;
|
|
60
|
+
const rest = subject.slice('agent.events.'.length);
|
|
61
|
+
if (rest.length === 0) return false;
|
|
62
|
+
if (rest.endsWith('.')) return false;
|
|
63
|
+
return true;
|
|
64
|
+
}
|