@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, deliverToOpenClaw } = this.options;
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, deliverToOpenClaw } = this.options;
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
- const telemetry = container.register('telemetry', new TelemetryComponent(http, account.agentId));
33
- const deliverToOpenClaw = async (content, sessionId = 'default', modelId, attachments, messageId) => {
34
- if (messageId && processedMessageIds.has(messageId))
35
- return;
36
- if (messageId) {
37
- processedMessageIds.add(messageId);
38
- if (processedMessageIds.size > 1000) {
39
- const first = processedMessageIds.values().next().value;
40
- if (first !== undefined)
41
- processedMessageIds.delete(first);
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 bridge = container.get('bridge');
142
- const telemetry = container.get('telemetry');
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, { cfg: api.runtime.cfg, abortSignal: new AbortController().signal });
174
- const bridge = container.get('bridge');
175
- const telemetry = container.get('telemetry');
176
- if (bridge.client) {
177
- bridge.client.sendJson({
178
- type: "agent_event",
179
- channel: channelId,
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@efengx/openclaw-channel-dragon",
3
- "version": "0.4.3",
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",