opc-agent 0.9.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +145 -144
- package/dist/channels/discord.d.ts +44 -0
- package/dist/channels/discord.js +189 -0
- package/dist/channels/feishu.d.ts +47 -0
- package/dist/channels/feishu.js +221 -0
- package/dist/channels/web.js +118 -39
- package/dist/cli.js +109 -1
- package/dist/core/errors.d.ts +68 -0
- package/dist/core/errors.js +149 -0
- package/dist/core/security.d.ts +48 -0
- package/dist/core/security.js +146 -0
- package/dist/core/watch.d.ts +73 -0
- package/dist/core/watch.js +106 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +32 -1
- package/dist/plugins/index.d.ts +24 -3
- package/dist/plugins/index.js +109 -4
- package/dist/schema/oad.d.ts +54 -0
- package/dist/schema/oad.js +6 -1
- package/package.json +1 -1
- package/src/channels/discord.ts +192 -0
- package/src/channels/feishu.ts +236 -0
- package/src/channels/web.ts +118 -39
- package/src/cli.ts +108 -1
- package/src/core/errors.ts +148 -0
- package/src/core/security.ts +171 -0
- package/src/core/watch.ts +178 -0
- package/src/index.ts +15 -0
- package/src/plugins/index.ts +128 -7
- package/src/schema/oad.ts +6 -0
- package/tests/errors.test.ts +83 -0
- package/tests/security.test.ts +60 -0
package/dist/plugins/index.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PluginManager = void 0;
|
|
4
|
+
exports.createLoggingPlugin = createLoggingPlugin;
|
|
5
|
+
exports.createAnalyticsPlugin = createAnalyticsPlugin;
|
|
6
|
+
exports.createRateLimitPlugin = createRateLimitPlugin;
|
|
7
|
+
const logger_1 = require("../core/logger");
|
|
4
8
|
class PluginManager {
|
|
5
9
|
plugins = new Map();
|
|
10
|
+
logger = new logger_1.Logger('plugins');
|
|
6
11
|
register(plugin) {
|
|
12
|
+
if (this.plugins.has(plugin.name)) {
|
|
13
|
+
this.logger.warn(`Plugin "${plugin.name}" already registered, replacing`);
|
|
14
|
+
}
|
|
7
15
|
this.plugins.set(plugin.name, plugin);
|
|
16
|
+
this.logger.info(`Plugin registered: ${plugin.name}@${plugin.version}`);
|
|
8
17
|
}
|
|
9
18
|
unregister(name) {
|
|
10
19
|
this.plugins.delete(name);
|
|
@@ -14,9 +23,7 @@ class PluginManager {
|
|
|
14
23
|
}
|
|
15
24
|
list() {
|
|
16
25
|
return Array.from(this.plugins.values()).map(({ name, version, description }) => ({
|
|
17
|
-
name,
|
|
18
|
-
version,
|
|
19
|
-
description,
|
|
26
|
+
name, version, description,
|
|
20
27
|
}));
|
|
21
28
|
}
|
|
22
29
|
has(name) {
|
|
@@ -26,10 +33,58 @@ class PluginManager {
|
|
|
26
33
|
for (const plugin of this.plugins.values()) {
|
|
27
34
|
const hook = plugin.hooks?.[hookName];
|
|
28
35
|
if (hook) {
|
|
29
|
-
|
|
36
|
+
try {
|
|
37
|
+
await hook(...args);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
this.logger.error(`Plugin "${plugin.name}" hook "${hookName}" failed`, {
|
|
41
|
+
error: err instanceof Error ? err.message : String(err),
|
|
42
|
+
});
|
|
43
|
+
// Don't let one plugin break others
|
|
44
|
+
}
|
|
30
45
|
}
|
|
31
46
|
}
|
|
32
47
|
}
|
|
48
|
+
async runOnInit() {
|
|
49
|
+
await this.runHook('onInit');
|
|
50
|
+
await this.runHook('beforeInit');
|
|
51
|
+
await this.runHook('afterInit');
|
|
52
|
+
}
|
|
53
|
+
async runOnMessage(message) {
|
|
54
|
+
let msg = message;
|
|
55
|
+
for (const plugin of this.plugins.values()) {
|
|
56
|
+
if (plugin.hooks?.onMessage) {
|
|
57
|
+
const result = await plugin.hooks.onMessage(msg);
|
|
58
|
+
if (result)
|
|
59
|
+
msg = result;
|
|
60
|
+
}
|
|
61
|
+
if (plugin.hooks?.beforeMessage) {
|
|
62
|
+
await plugin.hooks.beforeMessage({ content: msg.content });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return msg;
|
|
66
|
+
}
|
|
67
|
+
async runOnResponse(message, response) {
|
|
68
|
+
let resp = response;
|
|
69
|
+
for (const plugin of this.plugins.values()) {
|
|
70
|
+
if (plugin.hooks?.onResponse) {
|
|
71
|
+
const result = await plugin.hooks.onResponse(message, resp);
|
|
72
|
+
if (result)
|
|
73
|
+
resp = result;
|
|
74
|
+
}
|
|
75
|
+
if (plugin.hooks?.afterMessage) {
|
|
76
|
+
await plugin.hooks.afterMessage({ content: message.content }, { content: resp.content });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return resp;
|
|
80
|
+
}
|
|
81
|
+
async runOnError(error, context) {
|
|
82
|
+
await this.runHook('onError', error, context);
|
|
83
|
+
}
|
|
84
|
+
async runOnShutdown() {
|
|
85
|
+
await this.runHook('onShutdown');
|
|
86
|
+
await this.runHook('beforeShutdown');
|
|
87
|
+
}
|
|
33
88
|
getAllSkills() {
|
|
34
89
|
const skills = [];
|
|
35
90
|
for (const plugin of this.plugins.values()) {
|
|
@@ -56,4 +111,54 @@ class PluginManager {
|
|
|
56
111
|
}
|
|
57
112
|
}
|
|
58
113
|
exports.PluginManager = PluginManager;
|
|
114
|
+
// ── Built-in Plugins ────────────────────────────────────────
|
|
115
|
+
function createLoggingPlugin() {
|
|
116
|
+
const logger = new logger_1.Logger('agent:messages');
|
|
117
|
+
return {
|
|
118
|
+
name: 'logging',
|
|
119
|
+
version: '1.0.0',
|
|
120
|
+
description: 'Logs all messages and responses',
|
|
121
|
+
hooks: {
|
|
122
|
+
onInit: async () => { logger.info('Agent initialized'); },
|
|
123
|
+
onMessage: async (msg) => { logger.info(`← ${msg.role}: ${msg.content.slice(0, 100)}`); return undefined; },
|
|
124
|
+
onResponse: async (_msg, resp) => { logger.info(`→ ${resp.role}: ${resp.content.slice(0, 100)}`); return undefined; },
|
|
125
|
+
onError: async (err) => { logger.error(`Error: ${err.message}`); },
|
|
126
|
+
onShutdown: async () => { logger.info('Agent shutting down'); },
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function createAnalyticsPlugin() {
|
|
131
|
+
const stats = { messages: 0, errors: 0, startedAt: 0 };
|
|
132
|
+
return {
|
|
133
|
+
name: 'analytics',
|
|
134
|
+
version: '1.0.0',
|
|
135
|
+
description: 'Tracks message counts and error rates',
|
|
136
|
+
hooks: {
|
|
137
|
+
onInit: async () => { stats.startedAt = Date.now(); },
|
|
138
|
+
onMessage: async () => { stats.messages++; return undefined; },
|
|
139
|
+
onError: async () => { stats.errors++; },
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function createRateLimitPlugin(maxPerMinute = 60) {
|
|
144
|
+
const timestamps = [];
|
|
145
|
+
return {
|
|
146
|
+
name: 'rate-limit',
|
|
147
|
+
version: '1.0.0',
|
|
148
|
+
description: `Rate limits to ${maxPerMinute} messages/minute`,
|
|
149
|
+
hooks: {
|
|
150
|
+
onMessage: async () => {
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
const windowStart = now - 60_000;
|
|
153
|
+
while (timestamps.length > 0 && timestamps[0] < windowStart)
|
|
154
|
+
timestamps.shift();
|
|
155
|
+
if (timestamps.length >= maxPerMinute) {
|
|
156
|
+
throw new Error('Rate limit exceeded. Please slow down.');
|
|
157
|
+
}
|
|
158
|
+
timestamps.push(now);
|
|
159
|
+
return undefined;
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
59
164
|
//# sourceMappingURL=index.js.map
|
package/dist/schema/oad.d.ts
CHANGED
|
@@ -77,6 +77,16 @@ export declare const HITLSchema: z.ZodObject<{
|
|
|
77
77
|
defaultTimeoutMs?: number | undefined;
|
|
78
78
|
defaultAction?: "approve" | "deny" | undefined;
|
|
79
79
|
}>;
|
|
80
|
+
export declare const PluginRefSchema: z.ZodObject<{
|
|
81
|
+
name: z.ZodString;
|
|
82
|
+
config: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
83
|
+
}, "strip", z.ZodTypeAny, {
|
|
84
|
+
name: string;
|
|
85
|
+
config?: Record<string, unknown> | undefined;
|
|
86
|
+
}, {
|
|
87
|
+
name: string;
|
|
88
|
+
config?: Record<string, unknown> | undefined;
|
|
89
|
+
}>;
|
|
80
90
|
export declare const AuthSchema: z.ZodObject<{
|
|
81
91
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
82
92
|
apiKeys: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
@@ -479,6 +489,16 @@ export declare const SpecSchema: z.ZodObject<{
|
|
|
479
489
|
apiKeys?: string[] | undefined;
|
|
480
490
|
sessionIsolation?: boolean | undefined;
|
|
481
491
|
}>>;
|
|
492
|
+
plugins: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
493
|
+
name: z.ZodString;
|
|
494
|
+
config: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
495
|
+
}, "strip", z.ZodTypeAny, {
|
|
496
|
+
name: string;
|
|
497
|
+
config?: Record<string, unknown> | undefined;
|
|
498
|
+
}, {
|
|
499
|
+
name: string;
|
|
500
|
+
config?: Record<string, unknown> | undefined;
|
|
501
|
+
}>, "many">>;
|
|
482
502
|
}, "strip", z.ZodTypeAny, {
|
|
483
503
|
model: string;
|
|
484
504
|
skills: {
|
|
@@ -552,6 +572,10 @@ export declare const SpecSchema: z.ZodObject<{
|
|
|
552
572
|
defaultTimeoutMs: number;
|
|
553
573
|
defaultAction: "approve" | "deny";
|
|
554
574
|
} | undefined;
|
|
575
|
+
plugins?: {
|
|
576
|
+
name: string;
|
|
577
|
+
config?: Record<string, unknown> | undefined;
|
|
578
|
+
}[] | undefined;
|
|
555
579
|
}, {
|
|
556
580
|
auth?: {
|
|
557
581
|
enabled?: boolean | undefined;
|
|
@@ -625,6 +649,10 @@ export declare const SpecSchema: z.ZodObject<{
|
|
|
625
649
|
defaultTimeoutMs?: number | undefined;
|
|
626
650
|
defaultAction?: "approve" | "deny" | undefined;
|
|
627
651
|
} | undefined;
|
|
652
|
+
plugins?: {
|
|
653
|
+
name: string;
|
|
654
|
+
config?: Record<string, unknown> | undefined;
|
|
655
|
+
}[] | undefined;
|
|
628
656
|
}>;
|
|
629
657
|
export declare const OADSchema: z.ZodObject<{
|
|
630
658
|
apiVersion: z.ZodLiteral<"opc/v1">;
|
|
@@ -879,6 +907,16 @@ export declare const OADSchema: z.ZodObject<{
|
|
|
879
907
|
apiKeys?: string[] | undefined;
|
|
880
908
|
sessionIsolation?: boolean | undefined;
|
|
881
909
|
}>>;
|
|
910
|
+
plugins: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
911
|
+
name: z.ZodString;
|
|
912
|
+
config: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
913
|
+
}, "strip", z.ZodTypeAny, {
|
|
914
|
+
name: string;
|
|
915
|
+
config?: Record<string, unknown> | undefined;
|
|
916
|
+
}, {
|
|
917
|
+
name: string;
|
|
918
|
+
config?: Record<string, unknown> | undefined;
|
|
919
|
+
}>, "many">>;
|
|
882
920
|
}, "strip", z.ZodTypeAny, {
|
|
883
921
|
model: string;
|
|
884
922
|
skills: {
|
|
@@ -952,6 +990,10 @@ export declare const OADSchema: z.ZodObject<{
|
|
|
952
990
|
defaultTimeoutMs: number;
|
|
953
991
|
defaultAction: "approve" | "deny";
|
|
954
992
|
} | undefined;
|
|
993
|
+
plugins?: {
|
|
994
|
+
name: string;
|
|
995
|
+
config?: Record<string, unknown> | undefined;
|
|
996
|
+
}[] | undefined;
|
|
955
997
|
}, {
|
|
956
998
|
auth?: {
|
|
957
999
|
enabled?: boolean | undefined;
|
|
@@ -1025,6 +1067,10 @@ export declare const OADSchema: z.ZodObject<{
|
|
|
1025
1067
|
defaultTimeoutMs?: number | undefined;
|
|
1026
1068
|
defaultAction?: "approve" | "deny" | undefined;
|
|
1027
1069
|
} | undefined;
|
|
1070
|
+
plugins?: {
|
|
1071
|
+
name: string;
|
|
1072
|
+
config?: Record<string, unknown> | undefined;
|
|
1073
|
+
}[] | undefined;
|
|
1028
1074
|
}>;
|
|
1029
1075
|
}, "strip", z.ZodTypeAny, {
|
|
1030
1076
|
apiVersion: "opc/v1";
|
|
@@ -1115,6 +1161,10 @@ export declare const OADSchema: z.ZodObject<{
|
|
|
1115
1161
|
defaultTimeoutMs: number;
|
|
1116
1162
|
defaultAction: "approve" | "deny";
|
|
1117
1163
|
} | undefined;
|
|
1164
|
+
plugins?: {
|
|
1165
|
+
name: string;
|
|
1166
|
+
config?: Record<string, unknown> | undefined;
|
|
1167
|
+
}[] | undefined;
|
|
1118
1168
|
};
|
|
1119
1169
|
}, {
|
|
1120
1170
|
apiVersion: "opc/v1";
|
|
@@ -1205,6 +1255,10 @@ export declare const OADSchema: z.ZodObject<{
|
|
|
1205
1255
|
defaultTimeoutMs?: number | undefined;
|
|
1206
1256
|
defaultAction?: "approve" | "deny" | undefined;
|
|
1207
1257
|
} | undefined;
|
|
1258
|
+
plugins?: {
|
|
1259
|
+
name: string;
|
|
1260
|
+
config?: Record<string, unknown> | undefined;
|
|
1261
|
+
}[] | undefined;
|
|
1208
1262
|
};
|
|
1209
1263
|
}>;
|
|
1210
1264
|
export type OADDocument = z.infer<typeof OADSchema>;
|
package/dist/schema/oad.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.OADSchema = exports.SpecSchema = exports.StreamingSchema = exports.RoomSchema = exports.MetadataSchema = exports.MarketplaceSchema = exports.ProviderSchema = exports.DTVSchema = exports.TrustLevel = exports.MemorySchema = exports.LongTermMemorySchema = exports.ChannelSchema = exports.AuthSchema = exports.HITLSchema = exports.WebhookSchema = exports.VoiceSchema = exports.WorkflowSchema = exports.WorkflowStepSchema = exports.SkillRefSchema = void 0;
|
|
3
|
+
exports.OADSchema = exports.SpecSchema = exports.StreamingSchema = exports.RoomSchema = exports.MetadataSchema = exports.MarketplaceSchema = exports.ProviderSchema = exports.DTVSchema = exports.TrustLevel = exports.MemorySchema = exports.LongTermMemorySchema = exports.ChannelSchema = exports.AuthSchema = exports.PluginRefSchema = exports.HITLSchema = exports.WebhookSchema = exports.VoiceSchema = exports.WorkflowSchema = exports.WorkflowStepSchema = exports.SkillRefSchema = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
// ─── OAD Schema v1 ───────────────────────────────────────────
|
|
6
6
|
exports.SkillRefSchema = zod_1.z.object({
|
|
@@ -43,6 +43,10 @@ exports.HITLSchema = zod_1.z.object({
|
|
|
43
43
|
defaultTimeoutMs: zod_1.z.number().default(60000),
|
|
44
44
|
defaultAction: zod_1.z.enum(['approve', 'deny']).default('deny'),
|
|
45
45
|
});
|
|
46
|
+
exports.PluginRefSchema = zod_1.z.object({
|
|
47
|
+
name: zod_1.z.string(),
|
|
48
|
+
config: zod_1.z.record(zod_1.z.unknown()).optional(),
|
|
49
|
+
});
|
|
46
50
|
exports.AuthSchema = zod_1.z.object({
|
|
47
51
|
enabled: zod_1.z.boolean().default(false),
|
|
48
52
|
apiKeys: zod_1.z.array(zod_1.z.string()).default([]),
|
|
@@ -115,6 +119,7 @@ exports.SpecSchema = zod_1.z.object({
|
|
|
115
119
|
webhook: exports.WebhookSchema.optional(),
|
|
116
120
|
hitl: exports.HITLSchema.optional(),
|
|
117
121
|
auth: exports.AuthSchema.optional(),
|
|
122
|
+
plugins: zod_1.z.array(exports.PluginRefSchema).optional(),
|
|
118
123
|
});
|
|
119
124
|
exports.OADSchema = zod_1.z.object({
|
|
120
125
|
apiVersion: zod_1.z.literal('opc/v1'),
|
package/package.json
CHANGED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { BaseChannel } from './index';
|
|
2
|
+
import type { Message } from '../core/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Discord Channel — v1.1.0
|
|
6
|
+
*
|
|
7
|
+
* Supports:
|
|
8
|
+
* - Discord Bot via Gateway (WebSocket) or HTTP interactions
|
|
9
|
+
* - Slash commands, message content intent
|
|
10
|
+
* - Thread-based conversations
|
|
11
|
+
* - Reactions, embeds
|
|
12
|
+
*
|
|
13
|
+
* Env vars:
|
|
14
|
+
* DISCORD_BOT_TOKEN — bot token
|
|
15
|
+
* DISCORD_APPLICATION_ID — application ID for slash commands
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface DiscordChannelConfig {
|
|
19
|
+
/** Bot token */
|
|
20
|
+
botToken?: string;
|
|
21
|
+
/** Application ID */
|
|
22
|
+
applicationId?: string;
|
|
23
|
+
/** Guild IDs to register slash commands (empty = global) */
|
|
24
|
+
guildIds?: string[];
|
|
25
|
+
/** Whether to use threads for conversations */
|
|
26
|
+
useThreads?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class DiscordChannel extends BaseChannel {
|
|
30
|
+
readonly type = 'discord';
|
|
31
|
+
private config: DiscordChannelConfig;
|
|
32
|
+
private ws: import('ws').WebSocket | null = null;
|
|
33
|
+
private heartbeatInterval: ReturnType<typeof setInterval> | null = null;
|
|
34
|
+
private sequenceNumber: number | null = null;
|
|
35
|
+
private sessionId: string | null = null;
|
|
36
|
+
private resumeUrl: string | null = null;
|
|
37
|
+
|
|
38
|
+
constructor(config: DiscordChannelConfig = {}) {
|
|
39
|
+
super();
|
|
40
|
+
this.config = {
|
|
41
|
+
botToken: config.botToken ?? process.env.DISCORD_BOT_TOKEN ?? '',
|
|
42
|
+
applicationId: config.applicationId ?? process.env.DISCORD_APPLICATION_ID ?? '',
|
|
43
|
+
guildIds: config.guildIds ?? [],
|
|
44
|
+
useThreads: config.useThreads ?? true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async start(): Promise<void> {
|
|
49
|
+
if (!this.config.botToken) {
|
|
50
|
+
console.warn('[DiscordChannel] No bot token. Set DISCORD_BOT_TOKEN.');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Get gateway URL
|
|
55
|
+
const gatewayResp = await fetch('https://discord.com/api/v10/gateway/bot', {
|
|
56
|
+
headers: { Authorization: `Bot ${this.config.botToken}` },
|
|
57
|
+
});
|
|
58
|
+
const gatewayData = await gatewayResp.json() as { url: string };
|
|
59
|
+
const wsUrl = `${gatewayData.url}?v=10&encoding=json`;
|
|
60
|
+
|
|
61
|
+
const { WebSocket } = await import('ws');
|
|
62
|
+
this.ws = new WebSocket(wsUrl);
|
|
63
|
+
|
|
64
|
+
this.ws.on('message', async (data: Buffer) => {
|
|
65
|
+
const payload = JSON.parse(data.toString());
|
|
66
|
+
this.sequenceNumber = payload.s ?? this.sequenceNumber;
|
|
67
|
+
|
|
68
|
+
switch (payload.op) {
|
|
69
|
+
case 10: // Hello
|
|
70
|
+
this.startHeartbeat(payload.d.heartbeat_interval);
|
|
71
|
+
this.identify();
|
|
72
|
+
break;
|
|
73
|
+
case 11: // Heartbeat ACK
|
|
74
|
+
break;
|
|
75
|
+
case 0: // Dispatch
|
|
76
|
+
if (payload.t === 'READY') {
|
|
77
|
+
this.sessionId = payload.d.session_id;
|
|
78
|
+
this.resumeUrl = payload.d.resume_gateway_url;
|
|
79
|
+
console.log(`[DiscordChannel] Connected as ${payload.d.user.username}`);
|
|
80
|
+
} else if (payload.t === 'MESSAGE_CREATE') {
|
|
81
|
+
await this.handleMessage(payload.d);
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
this.ws.on('close', (code: number) => {
|
|
88
|
+
console.log(`[DiscordChannel] WebSocket closed: ${code}`);
|
|
89
|
+
this.stopHeartbeat();
|
|
90
|
+
// Auto-reconnect after 5s for resumable codes
|
|
91
|
+
if (code !== 4004 && code !== 4014) {
|
|
92
|
+
setTimeout(() => this.start(), 5000);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
this.ws.on('error', (err: Error) => {
|
|
97
|
+
console.error('[DiscordChannel] WebSocket error:', err.message);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async stop(): Promise<void> {
|
|
102
|
+
this.stopHeartbeat();
|
|
103
|
+
if (this.ws) {
|
|
104
|
+
this.ws.close(1000);
|
|
105
|
+
this.ws = null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private identify(): void {
|
|
110
|
+
this.ws?.send(JSON.stringify({
|
|
111
|
+
op: 2,
|
|
112
|
+
d: {
|
|
113
|
+
token: this.config.botToken,
|
|
114
|
+
intents: (1 << 9) | (1 << 15), // GUILD_MESSAGES | MESSAGE_CONTENT
|
|
115
|
+
properties: {
|
|
116
|
+
os: process.platform,
|
|
117
|
+
browser: 'opc-agent',
|
|
118
|
+
device: 'opc-agent',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private startHeartbeat(intervalMs: number): void {
|
|
125
|
+
this.stopHeartbeat();
|
|
126
|
+
// Send first heartbeat with jitter
|
|
127
|
+
setTimeout(() => {
|
|
128
|
+
this.sendHeartbeat();
|
|
129
|
+
this.heartbeatInterval = setInterval(() => this.sendHeartbeat(), intervalMs);
|
|
130
|
+
}, intervalMs * Math.random());
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private stopHeartbeat(): void {
|
|
134
|
+
if (this.heartbeatInterval) {
|
|
135
|
+
clearInterval(this.heartbeatInterval);
|
|
136
|
+
this.heartbeatInterval = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private sendHeartbeat(): void {
|
|
141
|
+
this.ws?.send(JSON.stringify({ op: 1, d: this.sequenceNumber }));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private async handleMessage(d: Record<string, unknown>): Promise<void> {
|
|
145
|
+
// Ignore bot messages
|
|
146
|
+
const author = d.author as Record<string, unknown>;
|
|
147
|
+
if (author?.bot) return;
|
|
148
|
+
if (!d.content || !this.handler) return;
|
|
149
|
+
|
|
150
|
+
const msg: Message = {
|
|
151
|
+
id: `discord_${d.id}`,
|
|
152
|
+
role: 'user',
|
|
153
|
+
content: d.content as string,
|
|
154
|
+
timestamp: new Date(d.timestamp as string).getTime(),
|
|
155
|
+
metadata: {
|
|
156
|
+
sessionId: `discord_${d.channel_id}`,
|
|
157
|
+
chatId: d.channel_id as string,
|
|
158
|
+
userId: author.id as string,
|
|
159
|
+
platform: 'discord',
|
|
160
|
+
guildId: d.guild_id as string | undefined,
|
|
161
|
+
threadId: (d as Record<string, unknown>).thread?.toString(),
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const response = await this.handler(msg);
|
|
166
|
+
await this.sendMessage(d.channel_id as string, response.content);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async sendMessage(channelId: string, content: string): Promise<void> {
|
|
170
|
+
// Discord max message length is 2000
|
|
171
|
+
const chunks = this.splitMessage(content, 2000);
|
|
172
|
+
for (const chunk of chunks) {
|
|
173
|
+
await fetch(`https://discord.com/api/v10/channels/${channelId}/messages`, {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: {
|
|
176
|
+
'Content-Type': 'application/json',
|
|
177
|
+
Authorization: `Bot ${this.config.botToken}`,
|
|
178
|
+
},
|
|
179
|
+
body: JSON.stringify({ content: chunk }),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private splitMessage(text: string, maxLen: number): string[] {
|
|
185
|
+
if (text.length <= maxLen) return [text];
|
|
186
|
+
const parts: string[] = [];
|
|
187
|
+
for (let i = 0; i < text.length; i += maxLen) {
|
|
188
|
+
parts.push(text.slice(i, i + maxLen));
|
|
189
|
+
}
|
|
190
|
+
return parts;
|
|
191
|
+
}
|
|
192
|
+
}
|