@efengx/openclaw-channel-dragon 0.4.3 → 0.4.4
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.
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { IComponent } from "../../core/IComponent.js";
|
|
2
|
+
import { BridgeComponent } from "../bridge/BridgeComponent.js";
|
|
3
|
+
import { TelemetryComponent } from "../telemetry/TelemetryComponent.js";
|
|
4
|
+
export interface ChannelOptions {
|
|
5
|
+
accountId: string;
|
|
6
|
+
agentId: string;
|
|
7
|
+
channelRuntime: any;
|
|
8
|
+
cfg: any;
|
|
9
|
+
logger?: any;
|
|
10
|
+
}
|
|
11
|
+
export declare class ChannelComponent implements IComponent {
|
|
12
|
+
private options;
|
|
13
|
+
private bridge;
|
|
14
|
+
private telemetry;
|
|
15
|
+
private processedMessageIds;
|
|
16
|
+
constructor(options: ChannelOptions, bridge: BridgeComponent, telemetry: TelemetryComponent);
|
|
17
|
+
start(): Promise<void>;
|
|
18
|
+
stop(): Promise<void>;
|
|
19
|
+
deliverToOpenClaw: (content: string, sessionId?: string, modelId?: string, attachments?: any[], messageId?: string | number) => Promise<void>;
|
|
20
|
+
handleOutboundText: (ctx: any) => Promise<{
|
|
21
|
+
ok: boolean;
|
|
22
|
+
messageId: string;
|
|
23
|
+
}>;
|
|
24
|
+
handleAgentEvent: (evt: any) => Promise<void>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const channelId = "dragon";
|
|
2
|
+
export class ChannelComponent {
|
|
3
|
+
options;
|
|
4
|
+
bridge;
|
|
5
|
+
telemetry;
|
|
6
|
+
processedMessageIds = new Set();
|
|
7
|
+
constructor(options, bridge, telemetry) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
this.bridge = bridge;
|
|
10
|
+
this.telemetry = telemetry;
|
|
11
|
+
}
|
|
12
|
+
async start() { }
|
|
13
|
+
async stop() { }
|
|
14
|
+
deliverToOpenClaw = async (content, sessionId = 'default', modelId, attachments, messageId) => {
|
|
15
|
+
if (messageId && this.processedMessageIds.has(messageId))
|
|
16
|
+
return;
|
|
17
|
+
if (messageId) {
|
|
18
|
+
this.processedMessageIds.add(messageId);
|
|
19
|
+
if (this.processedMessageIds.size > 1000) {
|
|
20
|
+
const first = this.processedMessageIds.values().next().value;
|
|
21
|
+
if (first !== undefined)
|
|
22
|
+
this.processedMessageIds.delete(first);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const replyDispatcher = this.options.channelRuntime?.reply?.dispatchReplyWithBufferedBlockDispatcher;
|
|
26
|
+
if (typeof replyDispatcher !== 'function')
|
|
27
|
+
return;
|
|
28
|
+
const { accountId, agentId, cfg, logger } = this.options;
|
|
29
|
+
const sessionKey = `dragon:${agentId}:direct:${sessionId}`;
|
|
30
|
+
await replyDispatcher({
|
|
31
|
+
ctx: {
|
|
32
|
+
Body: content,
|
|
33
|
+
From: sessionId === 'default' ? "workbench-user" : `workbench-user-${sessionId}`,
|
|
34
|
+
To: agentId,
|
|
35
|
+
ChatType: "direct",
|
|
36
|
+
Provider: channelId,
|
|
37
|
+
ChannelId: channelId,
|
|
38
|
+
AccountId: accountId,
|
|
39
|
+
SessionKey: sessionKey,
|
|
40
|
+
Timestamp: Date.now(),
|
|
41
|
+
Model: modelId,
|
|
42
|
+
Attachments: attachments,
|
|
43
|
+
},
|
|
44
|
+
cfg,
|
|
45
|
+
dispatcherOptions: {
|
|
46
|
+
deliver: async (payload) => {
|
|
47
|
+
const text = payload?.text || "";
|
|
48
|
+
if (!text && !payload?.tool_calls?.length)
|
|
49
|
+
return;
|
|
50
|
+
if (this.bridge.client) {
|
|
51
|
+
this.bridge.client.sendJson({
|
|
52
|
+
type: "outbound_text",
|
|
53
|
+
channel: channelId,
|
|
54
|
+
text,
|
|
55
|
+
tool_calls: payload?.tool_calls,
|
|
56
|
+
reasoning_content: payload?.reasoning_content,
|
|
57
|
+
route: { agentId, accountId, peer: { kind: "direct", id: sessionId }, sessionKey },
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
await this.telemetry.reportReply({
|
|
61
|
+
content: text,
|
|
62
|
+
sessionId,
|
|
63
|
+
tool_calls: payload?.tool_calls,
|
|
64
|
+
reasoning_content: payload?.reasoning_content,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
handleOutboundText = async (ctx) => {
|
|
71
|
+
const text = ctx?.text || "";
|
|
72
|
+
const { accountId, agentId } = this.options;
|
|
73
|
+
if (this.bridge.client) {
|
|
74
|
+
this.bridge.client.sendJson({
|
|
75
|
+
type: "outbound_text",
|
|
76
|
+
channel: channelId,
|
|
77
|
+
text,
|
|
78
|
+
route: { agentId, accountId, peer: ctx?.peer, sessionKey: ctx?.ctx?.SessionKey },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
await this.telemetry.reportReply({ content: text, sessionId: ctx?.peer?.id || 'default' });
|
|
82
|
+
return { ok: true, messageId: Date.now().toString() };
|
|
83
|
+
};
|
|
84
|
+
handleAgentEvent = async (evt) => {
|
|
85
|
+
const { agentId, accountId } = this.options;
|
|
86
|
+
if (this.bridge.client) {
|
|
87
|
+
this.bridge.client.sendJson({
|
|
88
|
+
type: "agent_event",
|
|
89
|
+
channel: channelId,
|
|
90
|
+
agentId,
|
|
91
|
+
runId: evt.runId,
|
|
92
|
+
stream: evt.stream,
|
|
93
|
+
data: evt.data,
|
|
94
|
+
sessionKey: evt.sessionKey
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
await this.telemetry.reportEvent(evt.stream, evt.data, evt.ts);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { IComponent } from "../../core/IComponent.js";
|
|
2
2
|
import { HttpComponent } from "../http/HttpComponent.js";
|
|
3
|
+
import { ChannelComponent } from "../channel/ChannelComponent.js";
|
|
3
4
|
export declare class PollingComponent implements IComponent {
|
|
4
5
|
private http;
|
|
6
|
+
private channel;
|
|
5
7
|
private options;
|
|
6
8
|
private pollInterval;
|
|
7
|
-
constructor(http: HttpComponent, options: {
|
|
9
|
+
constructor(http: HttpComponent, channel: ChannelComponent, options: {
|
|
8
10
|
agentId: string;
|
|
9
11
|
accountId: string;
|
|
10
12
|
abortSignal: AbortSignal;
|
|
11
13
|
logger?: any;
|
|
12
|
-
deliverToOpenClaw: (content: string, sessionId?: string, modelId?: string, attachments?: any[], messageId?: string | number) => Promise<void>;
|
|
13
14
|
});
|
|
14
15
|
start(): Promise<void>;
|
|
15
16
|
stop(): Promise<void>;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import * as http2 from "node:http2";
|
|
2
2
|
export class PollingComponent {
|
|
3
3
|
http;
|
|
4
|
+
channel;
|
|
4
5
|
options;
|
|
5
6
|
pollInterval = null;
|
|
6
|
-
constructor(http, options) {
|
|
7
|
+
constructor(http, channel, options) {
|
|
7
8
|
this.http = http;
|
|
9
|
+
this.channel = channel;
|
|
8
10
|
this.options = options;
|
|
9
11
|
}
|
|
10
12
|
async start() {
|
|
@@ -21,7 +23,7 @@ export class PollingComponent {
|
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
25
|
async consumePendingMessages() {
|
|
24
|
-
const { agentId, logger
|
|
26
|
+
const { agentId, logger } = this.options;
|
|
25
27
|
try {
|
|
26
28
|
const res = await this.http.fetch(`/api/agents/${agentId}/messages/poll`);
|
|
27
29
|
if (res.ok) {
|
|
@@ -30,7 +32,7 @@ export class PollingComponent {
|
|
|
30
32
|
if (messages.length > 0) {
|
|
31
33
|
logger?.info?.(`dragon channel: [RECOVERY] Consuming ${messages.length} pending messages via polling.`);
|
|
32
34
|
for (const m of messages) {
|
|
33
|
-
await deliverToOpenClaw(String(m.content || ''), String(m.sessionId || 'default'), m.modelId, m.attachments, m.id);
|
|
35
|
+
await this.channel.deliverToOpenClaw(String(m.content || ''), String(m.sessionId || 'default'), m.modelId, m.attachments, m.id);
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
}
|
|
@@ -58,7 +60,7 @@ export class PollingComponent {
|
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
async connectOnce() {
|
|
61
|
-
const { agentId, logger, abortSignal
|
|
63
|
+
const { agentId, logger, abortSignal } = this.options;
|
|
62
64
|
const ssePath = `/api/agents/events`;
|
|
63
65
|
void this.consumePendingMessages();
|
|
64
66
|
const handleSseText = async (chunkText, bufRef) => {
|
|
@@ -79,7 +81,7 @@ export class PollingComponent {
|
|
|
79
81
|
try {
|
|
80
82
|
const evt = JSON.parse(dataStr);
|
|
81
83
|
if (evt?.type === 'WORKBENCH_MESSAGE' && evt.agentId === agentId) {
|
|
82
|
-
await deliverToOpenClaw(String(evt?.payload?.content || ''), String(evt?.payload?.sessionId || 'default'), evt?.payload?.modelId, evt?.payload?.attachments, evt?.payload?.id);
|
|
84
|
+
await this.channel.deliverToOpenClaw(String(evt?.payload?.content || ''), String(evt?.payload?.sessionId || 'default'), evt?.payload?.modelId, evt?.payload?.attachments, evt?.payload?.id);
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
catch (e) { }
|
|
@@ -90,10 +92,6 @@ export class PollingComponent {
|
|
|
90
92
|
headers: { Accept: 'text/event-stream' },
|
|
91
93
|
});
|
|
92
94
|
if (res.status === 404) {
|
|
93
|
-
// HTTP/2 fallback logic (simplified for brevity here, original has full impl)
|
|
94
|
-
// Note: connectHttp2 should ideally also use HttpComponent logic, but it uses raw node http2
|
|
95
|
-
// We'll pass the auth token to it manually if needed.
|
|
96
|
-
// await this.connectHttp2(ssePath, handleSseText);
|
|
97
95
|
return;
|
|
98
96
|
}
|
|
99
97
|
if (!res.ok)
|
package/dist/index.js
CHANGED
|
@@ -6,21 +6,23 @@ import { BridgeComponent } from "./components/bridge/BridgeComponent.js";
|
|
|
6
6
|
import { PollingComponent } from "./components/sync/PollingComponent.js";
|
|
7
7
|
import { TelemetryComponent } from "./components/telemetry/TelemetryComponent.js";
|
|
8
8
|
import { HttpComponent } from "./components/http/HttpComponent.js";
|
|
9
|
+
import { ChannelComponent } from "./components/channel/ChannelComponent.js";
|
|
9
10
|
const channelId = "dragon";
|
|
10
11
|
let cachedRuntime;
|
|
11
12
|
const containers = new Map();
|
|
12
|
-
const processedMessageIds = new Set();
|
|
13
13
|
async function getOrCreateContainer(account, ctx) {
|
|
14
14
|
const key = `${account.agentId}:${account.orchestratorUrl}`;
|
|
15
15
|
if (containers.has(key))
|
|
16
16
|
return containers.get(key);
|
|
17
17
|
const logger = ctx?.logger ?? ctx?.log ?? cachedRuntime?.logger ?? cachedRuntime?.log;
|
|
18
18
|
const container = new ServiceContainer();
|
|
19
|
+
// 1. Core Infrastructure
|
|
19
20
|
const http = container.register('http', new HttpComponent({
|
|
20
21
|
baseURL: account.orchestratorUrl,
|
|
21
22
|
authToken: account.orchestratorAuthToken || process.env.DRAGON_ORCHESTRATOR_AUTH_TOKEN || process.env.DRAGON_GATEWAY_TOKEN,
|
|
22
23
|
logger
|
|
23
24
|
}));
|
|
25
|
+
const telemetry = container.register('telemetry', new TelemetryComponent(http, account.agentId));
|
|
24
26
|
const bridge = container.register('bridge', new BridgeComponent({
|
|
25
27
|
port: parseInt(account.bridgePort || "18799", 10),
|
|
26
28
|
token: account.bridgeToken || "",
|
|
@@ -29,68 +31,20 @@ async function getOrCreateContainer(account, ctx) {
|
|
|
29
31
|
gatewayToken: account.gatewayToken || "",
|
|
30
32
|
logger
|
|
31
33
|
}));
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
const replyDispatcher = ctx.channelRuntime?.reply?.dispatchReplyWithBufferedBlockDispatcher;
|
|
45
|
-
if (typeof replyDispatcher !== 'function')
|
|
46
|
-
return;
|
|
47
|
-
const sessionKey = `dragon:${account.agentId}:direct:${sessionId}`;
|
|
48
|
-
await replyDispatcher({
|
|
49
|
-
ctx: {
|
|
50
|
-
Body: content,
|
|
51
|
-
From: sessionId === 'default' ? "workbench-user" : `workbench-user-${sessionId}`,
|
|
52
|
-
To: account.agentId,
|
|
53
|
-
ChatType: "direct",
|
|
54
|
-
Provider: channelId,
|
|
55
|
-
ChannelId: channelId,
|
|
56
|
-
AccountId: account.accountId,
|
|
57
|
-
SessionKey: sessionKey,
|
|
58
|
-
Timestamp: Date.now(),
|
|
59
|
-
Model: modelId,
|
|
60
|
-
Attachments: attachments,
|
|
61
|
-
},
|
|
62
|
-
cfg: ctx.cfg,
|
|
63
|
-
dispatcherOptions: {
|
|
64
|
-
deliver: async (payload) => {
|
|
65
|
-
const text = payload?.text || "";
|
|
66
|
-
if (!text && !payload?.tool_calls?.length)
|
|
67
|
-
return;
|
|
68
|
-
if (bridge.client) {
|
|
69
|
-
bridge.client.sendJson({
|
|
70
|
-
type: "outbound_text",
|
|
71
|
-
channel: channelId,
|
|
72
|
-
text,
|
|
73
|
-
tool_calls: payload?.tool_calls,
|
|
74
|
-
reasoning_content: payload?.reasoning_content,
|
|
75
|
-
route: { agentId: account.agentId, accountId: account.accountId, peer: { kind: "direct", id: sessionId }, sessionKey },
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
await telemetry.reportReply({
|
|
79
|
-
content: text,
|
|
80
|
-
sessionId,
|
|
81
|
-
tool_calls: payload?.tool_calls,
|
|
82
|
-
reasoning_content: payload?.reasoning_content,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
};
|
|
88
|
-
container.register('polling', new PollingComponent(http, {
|
|
34
|
+
// 2. Channel Logic (Centralized)
|
|
35
|
+
const channel = container.register('channel', new ChannelComponent({
|
|
36
|
+
accountId: account.accountId,
|
|
37
|
+
agentId: account.agentId,
|
|
38
|
+
channelRuntime: ctx.channelRuntime,
|
|
39
|
+
cfg: ctx.cfg,
|
|
40
|
+
logger
|
|
41
|
+
}, bridge, telemetry));
|
|
42
|
+
// 3. Polling Infrastructure
|
|
43
|
+
container.register('polling', new PollingComponent(http, channel, {
|
|
89
44
|
agentId: account.agentId,
|
|
90
45
|
accountId: account.accountId,
|
|
91
46
|
abortSignal: ctx.abortSignal,
|
|
92
|
-
logger
|
|
93
|
-
deliverToOpenClaw
|
|
47
|
+
logger
|
|
94
48
|
}));
|
|
95
49
|
await container.startAll();
|
|
96
50
|
containers.set(key, container);
|
|
@@ -135,21 +89,10 @@ const plugin = createChatChannelPlugin({
|
|
|
135
89
|
attachedResults: createRawChannelSendResultAdapter({
|
|
136
90
|
channel: channelId,
|
|
137
91
|
async sendText(ctx) {
|
|
138
|
-
const text = ctx?.text || "";
|
|
139
92
|
const account = base.config.resolveAccount(ctx.cfg, ctx.accountId);
|
|
140
93
|
const container = await getOrCreateContainer(account, ctx);
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
if (bridge.client) {
|
|
144
|
-
bridge.client.sendJson({
|
|
145
|
-
type: "outbound_text",
|
|
146
|
-
channel: channelId,
|
|
147
|
-
text,
|
|
148
|
-
route: { agentId: account.agentId, accountId: account.accountId, peer: ctx?.peer, sessionKey: ctx?.ctx?.SessionKey },
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
await telemetry.reportReply({ content: text, sessionId: ctx?.peer?.id || 'default' });
|
|
152
|
-
return { ok: true, messageId: Date.now().toString() };
|
|
94
|
+
const channel = container.get('channel');
|
|
95
|
+
return await channel.handleOutboundText(ctx);
|
|
153
96
|
},
|
|
154
97
|
}),
|
|
155
98
|
},
|
|
@@ -167,24 +110,15 @@ const entry = defineChannelPluginEntry({
|
|
|
167
110
|
const sessionKey = evt.sessionKey;
|
|
168
111
|
if (!sessionKey || !sessionKey.startsWith(`${channelId}:`))
|
|
169
112
|
return;
|
|
170
|
-
const agentId = sessionKey.split(':')[1] || "";
|
|
171
113
|
const accountId = sessionKey.split(':')[2] || "default";
|
|
172
114
|
const account = base.config.resolveAccount(api.runtime.cfg, accountId);
|
|
173
|
-
const container = await getOrCreateContainer(account, {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
agentId,
|
|
181
|
-
runId: evt.runId,
|
|
182
|
-
stream: evt.stream,
|
|
183
|
-
data: evt.data,
|
|
184
|
-
sessionKey
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
await telemetry.reportEvent(evt.stream, evt.data, evt.ts);
|
|
115
|
+
const container = await getOrCreateContainer(account, {
|
|
116
|
+
cfg: api.runtime.cfg,
|
|
117
|
+
abortSignal: new AbortController().signal,
|
|
118
|
+
channelRuntime: api.runtime.channelRuntime // Ensure runtime is passed
|
|
119
|
+
});
|
|
120
|
+
const channel = container.get('channel');
|
|
121
|
+
await channel.handleAgentEvent(evt);
|
|
188
122
|
});
|
|
189
123
|
}
|
|
190
124
|
}
|