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