@openclaw-cloud/agent-controller 0.1.2 → 0.1.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.
- package/CLAUDE.md +59 -0
- package/__tests__/board-handler.test.ts +6 -3
- package/dist/connection.js +18 -2
- package/dist/connection.js.map +1 -1
- package/package.json +1 -1
- package/src/connection.ts +42 -2
- package/src/handlers/board-handler.ts +5 -2
- package/src/handlers/chat.ts +79 -0
- package/src/index.ts +19 -3
- package/src/openclaw/gateway-adapter.ts +129 -0
- package/src/openclaw/gateway-client.ts +131 -0
- package/src/openclaw/index.ts +17 -0
- package/src/openclaw/types.ts +41 -0
- package/src/types.ts +4 -1
package/CLAUDE.md
CHANGED
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
# CLAUDE.md — Agent Controller Context
|
|
2
2
|
|
|
3
|
+
Review this plan thoroughly before making any code changes. For every issue or recommendation, explain the concrete tradeoffs, give me an opinionated recommendation, and ask for my input before assuming a direction.
|
|
4
|
+
|
|
5
|
+
My engineering preferences (use these to guide your recommendations):
|
|
6
|
+
|
|
7
|
+
• DRY is important—flag repetition aggressively.
|
|
8
|
+
• Well-tested code is non-negotiable; I'd rather have too many tests than too few.
|
|
9
|
+
• I want code that's "engineered enough" — not under-engineered (fragile, hacky) and not over-engineered (premature abstraction, unnecessary complexity).
|
|
10
|
+
• I err on the side of handling more edge cases, not fewer; thoughtfulness > speed.
|
|
11
|
+
• Bias toward explicit over clever.
|
|
12
|
+
|
|
13
|
+
1. Architecture review
|
|
14
|
+
Evaluate:
|
|
15
|
+
|
|
16
|
+
• Overall system design and component boundaries.
|
|
17
|
+
• Dependency graph and coupling concerns.
|
|
18
|
+
• Data flow patterns and potential bottlenecks.
|
|
19
|
+
• Scaling characteristics and single points of failure.
|
|
20
|
+
• Security architecture (auth, data access, API boundaries).
|
|
21
|
+
2. Code quality review
|
|
22
|
+
Evaluate:
|
|
23
|
+
|
|
24
|
+
• Code organization and module structure.
|
|
25
|
+
• DRY violations—be aggressive here.
|
|
26
|
+
• Error handling patterns and missing edge cases (call these out explicitly).
|
|
27
|
+
• Technical debt hotspots.
|
|
28
|
+
• Areas that are over-engineered or under-engineered relative to my preferences.
|
|
29
|
+
3. Test review
|
|
30
|
+
Evaluate:
|
|
31
|
+
|
|
32
|
+
• Test coverage gaps (unit, integration, e2e).
|
|
33
|
+
• Test quality and assertion strength.
|
|
34
|
+
• Missing edge case coverage—be thorough.
|
|
35
|
+
• Untested failure modes and error paths.
|
|
36
|
+
4. Performance review
|
|
37
|
+
Evaluate:
|
|
38
|
+
|
|
39
|
+
• N+1 queries and database access patterns.
|
|
40
|
+
• Memory-usage concerns.
|
|
41
|
+
• Caching opportunities.
|
|
42
|
+
• Slow or high-complexity code paths.
|
|
43
|
+
For each issue you find
|
|
44
|
+
For every specific issue (bug, smell, design concern, or risk):
|
|
45
|
+
|
|
46
|
+
• Describe the problem concretely, with file and line references.
|
|
47
|
+
• Present 2–3 options, including "do nothing" where that's reasonable.
|
|
48
|
+
• For each option, specify: implementation effort, risk, impact on other code, and maintenance burden.
|
|
49
|
+
• Give me your recommended option and why, mapped to my preferences above.
|
|
50
|
+
• Then explicitly ask whether I agree or want to choose a different direction before proceeding.
|
|
51
|
+
Workflow and interaction
|
|
52
|
+
|
|
53
|
+
• Do not assume my priorities on timeline or scale.
|
|
54
|
+
• After each section, pause and ask for my feedback before moving on.
|
|
55
|
+
BEFORE YOU START:
|
|
56
|
+
Ask if I want one of two options:
|
|
57
|
+
1/ BIG CHANGE: Work through this interactively, one section at a time (Architecture → Code Quality → Tests → Performance) with at most 4 top issues in each section.
|
|
58
|
+
2/ SMALL CHANGE: Work through interactively ONE question per review section
|
|
59
|
+
|
|
60
|
+
FOR EACH STAGE OF REVIEW: output the explanation and pros and cons of each stage's questions AND your opinionated recommendation and why, and then use AskUserQuestion. Also NUMBER issues and then give LETTERS for options and when using AskUserQuestion make sure each option clearly labels the issue NUMBER and option LETTER so the user doesn't get confused. Make the recommended option always the 1st option.
|
|
61
|
+
|
|
3
62
|
## Quick Reference
|
|
4
63
|
|
|
5
64
|
```bash
|
|
@@ -16,12 +16,14 @@ function mockApi(overrides: Partial<AgentApi> = {}): AgentApi {
|
|
|
16
16
|
|
|
17
17
|
function makeBoardInfo(opts: Partial<{
|
|
18
18
|
boardId: string;
|
|
19
|
+
workspaceId: string;
|
|
19
20
|
myColumnIds: string[];
|
|
20
21
|
columns: BoardInfo['board']['columns'];
|
|
21
22
|
}> = {}): BoardInfo {
|
|
22
23
|
return {
|
|
23
24
|
board: {
|
|
24
25
|
id: opts.boardId ?? 'board-1',
|
|
26
|
+
workspaceId: opts.workspaceId ?? 'ws-1',
|
|
25
27
|
myColumnIds: opts.myColumnIds ?? ['col-1'],
|
|
26
28
|
columns: opts.columns ?? [{
|
|
27
29
|
id: 'col-1',
|
|
@@ -70,13 +72,14 @@ describe('BoardHandler', () => {
|
|
|
70
72
|
});
|
|
71
73
|
|
|
72
74
|
describe('initialize', () => {
|
|
73
|
-
it('sets boardId and myColumnIds from API response', async () => {
|
|
74
|
-
const info = makeBoardInfo({ boardId: 'b-42', myColumnIds: ['c-1', 'c-2'] });
|
|
75
|
+
it('sets boardId, workspaceId and myColumnIds from API response', async () => {
|
|
76
|
+
const info = makeBoardInfo({ boardId: 'b-42', workspaceId: 'ws-99', myColumnIds: ['c-1', 'c-2'] });
|
|
75
77
|
(api.get as jest.Mock).mockResolvedValue(info);
|
|
76
78
|
|
|
77
79
|
const result = await handler.initialize();
|
|
78
80
|
|
|
79
|
-
|
|
81
|
+
// initialize() returns workspaceId for channel subscription
|
|
82
|
+
expect(result).toBe('ws-99');
|
|
80
83
|
expect(handler.getBoardId()).toBe('b-42');
|
|
81
84
|
expect(api.get).toHaveBeenCalledWith('/api/agent/board');
|
|
82
85
|
});
|
package/dist/connection.js
CHANGED
|
@@ -79,14 +79,18 @@ export function createConnection(opts) {
|
|
|
79
79
|
const commandChannel = `agent:${opts.agentId}`;
|
|
80
80
|
const commandSub = client.newSubscription(commandChannel);
|
|
81
81
|
commandSub.on('publication', async (ctx) => {
|
|
82
|
+
console.log(`[WS] Received message on ${commandChannel}:`, JSON.stringify(ctx.data));
|
|
82
83
|
const command = ctx.data;
|
|
83
|
-
if (!command || !command.type)
|
|
84
|
+
if (!command || !command.type) {
|
|
85
|
+
console.warn('[WS] Ignoring message without type:', JSON.stringify(ctx.data));
|
|
84
86
|
return;
|
|
87
|
+
}
|
|
85
88
|
const handler = handlers[command.type];
|
|
86
89
|
if (!handler) {
|
|
87
|
-
console.error(`Unknown command type: ${command.type}`);
|
|
90
|
+
console.error(`[WS] Unknown command type: ${command.type}`);
|
|
88
91
|
return;
|
|
89
92
|
}
|
|
93
|
+
console.log(`[WS] Handling command: type=${command.type} id=${command.id}`);
|
|
90
94
|
try {
|
|
91
95
|
const response = await handler(command);
|
|
92
96
|
await client.publish(commandChannel, response);
|
|
@@ -110,6 +114,18 @@ export function createConnection(opts) {
|
|
|
110
114
|
client.on('connecting', (ctx) => {
|
|
111
115
|
console.log(`Connecting: ${ctx.reason} (code ${ctx.code})`);
|
|
112
116
|
});
|
|
117
|
+
commandSub.on('subscribed', (ctx) => {
|
|
118
|
+
console.log(`[WS] Subscribed to ${commandChannel} (recovered=${ctx.wasRecovering})`);
|
|
119
|
+
});
|
|
120
|
+
commandSub.on('subscribing', (ctx) => {
|
|
121
|
+
console.log(`[WS] Subscribing to ${commandChannel}: ${ctx.reason} (code ${ctx.code})`);
|
|
122
|
+
});
|
|
123
|
+
commandSub.on('error', (ctx) => {
|
|
124
|
+
console.error(`[WS] Subscription error on ${commandChannel}:`, ctx.error);
|
|
125
|
+
});
|
|
126
|
+
commandSub.on('unsubscribed', (ctx) => {
|
|
127
|
+
console.log(`[WS] Unsubscribed from ${commandChannel}: ${ctx.reason} (code ${ctx.code})`);
|
|
128
|
+
});
|
|
113
129
|
commandSub.subscribe();
|
|
114
130
|
client.connect();
|
|
115
131
|
return { client, commandSub };
|
package/dist/connection.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/connection.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { handleConfig } from './handlers/config.js';
|
|
|
8
8
|
import { handlePair } from './handlers/pair.js';
|
|
9
9
|
import { handleStop } from './handlers/stop.js';
|
|
10
10
|
import { handleBackup } from './handlers/backup.js';
|
|
11
|
+
import { handleChatListSessions, handleChatHistory, handleChatSend } from './handlers/chat.js';
|
|
11
12
|
|
|
12
13
|
export interface ConnectionOptions {
|
|
13
14
|
url: string;
|
|
@@ -93,14 +94,37 @@ export function createConnection(opts: ConnectionOptions): {
|
|
|
93
94
|
const commandSub = client.newSubscription(commandChannel);
|
|
94
95
|
|
|
95
96
|
commandSub.on('publication', async (ctx) => {
|
|
97
|
+
console.log(`[WS] Received message on ${commandChannel}:`, JSON.stringify(ctx.data));
|
|
96
98
|
const command = ctx.data as AgentCommand;
|
|
97
|
-
if (!command || !command.type)
|
|
99
|
+
if (!command || !command.type) {
|
|
100
|
+
console.warn('[WS] Ignoring message without type:', JSON.stringify(ctx.data));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Chat commands — handle before generic dispatch
|
|
105
|
+
if (command.type === 'chat_list_sessions' || command.type === 'chat_history' || command.type === 'chat_send') {
|
|
106
|
+
const publishFn = async (data: unknown): Promise<void> => { await client.publish(commandChannel, data); };
|
|
107
|
+
console.log(`[WS] Handling chat command: type=${command.type} id=${command.id}`);
|
|
108
|
+
switch (command.type) {
|
|
109
|
+
case 'chat_list_sessions':
|
|
110
|
+
handleChatListSessions(command, publishFn).catch(console.error);
|
|
111
|
+
break;
|
|
112
|
+
case 'chat_history':
|
|
113
|
+
handleChatHistory(command, publishFn).catch(console.error);
|
|
114
|
+
break;
|
|
115
|
+
case 'chat_send':
|
|
116
|
+
handleChatSend(command, publishFn, opts.agentId).catch(console.error);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
98
121
|
|
|
99
122
|
const handler = handlers[command.type];
|
|
100
123
|
if (!handler) {
|
|
101
|
-
console.error(`Unknown command type: ${command.type}`);
|
|
124
|
+
console.error(`[WS] Unknown command type: ${command.type}`);
|
|
102
125
|
return;
|
|
103
126
|
}
|
|
127
|
+
console.log(`[WS] Handling command: type=${command.type} id=${command.id}`);
|
|
104
128
|
|
|
105
129
|
try {
|
|
106
130
|
const response = await handler(command);
|
|
@@ -128,6 +152,22 @@ export function createConnection(opts: ConnectionOptions): {
|
|
|
128
152
|
console.log(`Connecting: ${ctx.reason} (code ${ctx.code})`);
|
|
129
153
|
});
|
|
130
154
|
|
|
155
|
+
commandSub.on('subscribed', (ctx) => {
|
|
156
|
+
console.log(`[WS] Subscribed to ${commandChannel} (recovered=${ctx.wasRecovering})`);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
commandSub.on('subscribing', (ctx) => {
|
|
160
|
+
console.log(`[WS] Subscribing to ${commandChannel}: ${ctx.reason} (code ${ctx.code})`);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
commandSub.on('error', (ctx) => {
|
|
164
|
+
console.error(`[WS] Subscription error on ${commandChannel}:`, ctx.error);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
commandSub.on('unsubscribed', (ctx) => {
|
|
168
|
+
console.log(`[WS] Unsubscribed from ${commandChannel}: ${ctx.reason} (code ${ctx.code})`);
|
|
169
|
+
});
|
|
170
|
+
|
|
131
171
|
commandSub.subscribe();
|
|
132
172
|
client.connect();
|
|
133
173
|
|
|
@@ -8,6 +8,7 @@ export class BoardHandler {
|
|
|
8
8
|
private boardState: BoardState = { state: 'idle', cardId: null };
|
|
9
9
|
private myColumnIds: string[] = [];
|
|
10
10
|
private boardId: string | null = null;
|
|
11
|
+
private workspaceId: string | null = null;
|
|
11
12
|
private api: AgentApi;
|
|
12
13
|
|
|
13
14
|
constructor(api: AgentApi) {
|
|
@@ -18,12 +19,14 @@ export class BoardHandler {
|
|
|
18
19
|
try {
|
|
19
20
|
const data = await this.api.get('/api/agent/board') as BoardInfo;
|
|
20
21
|
this.boardId = data.board.id;
|
|
22
|
+
this.workspaceId = data.board.workspaceId;
|
|
21
23
|
this.myColumnIds = data.board.myColumnIds;
|
|
22
|
-
console.log(`Board initialized: ${this.boardId}, watching ${this.myColumnIds.length} column(s)`);
|
|
24
|
+
console.log(`Board initialized: ${this.boardId} (workspace: ${this.workspaceId}), watching ${this.myColumnIds.length} column(s)`);
|
|
23
25
|
|
|
24
26
|
// Backfill: check for existing unassigned cards in my columns
|
|
25
27
|
await this.checkQueue(data);
|
|
26
|
-
|
|
28
|
+
// Return workspaceId for channel subscription (board:<workspaceId>)
|
|
29
|
+
return this.workspaceId;
|
|
27
30
|
} catch (err) {
|
|
28
31
|
console.error('Board initialization failed:', err instanceof Error ? err.message : err);
|
|
29
32
|
return null;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { AgentCommand } from '../types.js';
|
|
2
|
+
import { getChatProvider } from '../openclaw/index.js';
|
|
3
|
+
|
|
4
|
+
export async function handleChatListSessions(
|
|
5
|
+
command: AgentCommand,
|
|
6
|
+
publish: (data: unknown) => Promise<void>,
|
|
7
|
+
): Promise<void> {
|
|
8
|
+
const provider = getChatProvider();
|
|
9
|
+
if (!provider) {
|
|
10
|
+
await publish({ type: 'chat_sessions_response', correlationId: command.id, sessions: [], error: 'Chat provider not initialized' });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const sessions = await provider.listSessions();
|
|
15
|
+
await publish({ type: 'chat_sessions_response', correlationId: command.id, sessions });
|
|
16
|
+
} catch (err) {
|
|
17
|
+
await publish({ type: 'chat_sessions_response', correlationId: command.id, sessions: [], error: err instanceof Error ? err.message : String(err) });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function handleChatHistory(
|
|
22
|
+
command: AgentCommand,
|
|
23
|
+
publish: (data: unknown) => Promise<void>,
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
const { sessionKey, limit } = command.payload as { sessionKey: string; limit?: number };
|
|
26
|
+
const provider = getChatProvider();
|
|
27
|
+
if (!provider) {
|
|
28
|
+
await publish({ type: 'chat_history_response', correlationId: command.id, messages: [], error: 'Chat provider not initialized' });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const messages = await provider.getHistory(sessionKey, limit ?? 200);
|
|
33
|
+
await publish({ type: 'chat_history_response', correlationId: command.id, messages });
|
|
34
|
+
} catch (err) {
|
|
35
|
+
await publish({ type: 'chat_history_response', correlationId: command.id, messages: [], error: err instanceof Error ? err.message : String(err) });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function handleChatSend(
|
|
40
|
+
command: AgentCommand,
|
|
41
|
+
publish: (data: unknown) => Promise<void>,
|
|
42
|
+
agentId: string,
|
|
43
|
+
): Promise<void> {
|
|
44
|
+
const { sessionKey, text, attachments } = command.payload as {
|
|
45
|
+
sessionKey: string;
|
|
46
|
+
text: string;
|
|
47
|
+
attachments?: Array<{ type: 'image'; mimeType: string; content: string }>;
|
|
48
|
+
};
|
|
49
|
+
const correlationId = command.id;
|
|
50
|
+
|
|
51
|
+
// Typing indicator
|
|
52
|
+
await publish({ type: 'chat_typing', agentId, state: true }).catch(() => {});
|
|
53
|
+
|
|
54
|
+
const provider = getChatProvider();
|
|
55
|
+
if (!provider) {
|
|
56
|
+
await publish({ type: 'chat_response', correlationId, sessionKey, text: '', error: 'Chat provider not initialized' });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
await provider.sendMessage({
|
|
62
|
+
sessionKey,
|
|
63
|
+
text,
|
|
64
|
+
idempotencyKey: correlationId,
|
|
65
|
+
attachments, // base64 inline — Centrifugo limit raised to 10MB
|
|
66
|
+
onDelta: async (accumulated) => {
|
|
67
|
+
await publish({ type: 'chat_delta', correlationId, sessionKey, text: accumulated }).catch(() => {});
|
|
68
|
+
},
|
|
69
|
+
onDone: async (finalText) => {
|
|
70
|
+
await publish({ type: 'chat_response', correlationId, sessionKey, text: finalText }).catch(() => {});
|
|
71
|
+
},
|
|
72
|
+
onError: async (error) => {
|
|
73
|
+
await publish({ type: 'chat_response', correlationId, sessionKey, text: '', error }).catch(() => {});
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
} catch (err) {
|
|
77
|
+
await publish({ type: 'chat_response', correlationId, sessionKey, text: '', error: err instanceof Error ? err.message : String(err) }).catch(() => {});
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createConnection } from './connection.js';
|
|
|
2
2
|
import { startHeartbeat } from './heartbeat.js';
|
|
3
3
|
import { createAgentApi } from './api.js';
|
|
4
4
|
import { BoardHandler } from './handlers/board-handler.js';
|
|
5
|
+
import { createChatProvider } from './openclaw/index.js';
|
|
5
6
|
import type { BoardEvent } from './types.js';
|
|
6
7
|
|
|
7
8
|
function requireEnv(name: string): string {
|
|
@@ -30,6 +31,21 @@ export function main(): void {
|
|
|
30
31
|
|
|
31
32
|
const { client } = createConnection({ url, token, agentId, backendUrl: backendUrl || undefined });
|
|
32
33
|
|
|
34
|
+
// Chat provider via OpenClaw Gateway WS
|
|
35
|
+
const gatewayWsUrl = process.env.OPENCLAW_GATEWAY_WS || 'ws://localhost:18789';
|
|
36
|
+
const gatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';
|
|
37
|
+
|
|
38
|
+
if (gatewayToken) {
|
|
39
|
+
const chatProvider = createChatProvider(gatewayWsUrl, gatewayToken);
|
|
40
|
+
chatProvider.connect().then(() => {
|
|
41
|
+
console.log('OpenClaw Gateway WS connected (chat provider ready)');
|
|
42
|
+
}).catch((e) => {
|
|
43
|
+
console.warn('Chat provider connect failed:', e instanceof Error ? e.message : e);
|
|
44
|
+
});
|
|
45
|
+
} else {
|
|
46
|
+
console.warn('OPENCLAW_GATEWAY_TOKEN not set, chat provider disabled');
|
|
47
|
+
}
|
|
48
|
+
|
|
33
49
|
// Board handler (optional — requires BACKEND_URL and AGENT_TOKEN)
|
|
34
50
|
let boardHandler: BoardHandler | null = null;
|
|
35
51
|
const apiBaseUrl = process.env.BACKEND_URL || backendUrl;
|
|
@@ -38,13 +54,13 @@ export function main(): void {
|
|
|
38
54
|
boardHandler = new BoardHandler(api);
|
|
39
55
|
|
|
40
56
|
// Initialize board handler after connection is established
|
|
41
|
-
boardHandler.initialize().then((
|
|
42
|
-
if (!
|
|
57
|
+
boardHandler.initialize().then((workspaceId) => {
|
|
58
|
+
if (!workspaceId) {
|
|
43
59
|
console.log('No board found for this agent, board handler dormant');
|
|
44
60
|
return;
|
|
45
61
|
}
|
|
46
62
|
|
|
47
|
-
const boardChannel = `board:${
|
|
63
|
+
const boardChannel = `board:${workspaceId}`;
|
|
48
64
|
const boardSub = client.newSubscription(boardChannel);
|
|
49
65
|
|
|
50
66
|
boardSub.on('publication', (ctx) => {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { GatewayClient } from './gateway-client.js';
|
|
2
|
+
import type { IChatProvider, ChatSession, ChatMessage, ChatAttachment } from './types.js';
|
|
3
|
+
|
|
4
|
+
export class OpenclawGatewayAdapter implements IChatProvider {
|
|
5
|
+
private client: GatewayClient;
|
|
6
|
+
private streamCallbacks = new Map<string, {
|
|
7
|
+
onDelta?: (text: string) => void;
|
|
8
|
+
onDone?: (text: string) => void;
|
|
9
|
+
onError?: (error: string) => void;
|
|
10
|
+
lastText: string;
|
|
11
|
+
}>();
|
|
12
|
+
|
|
13
|
+
constructor(url: string, token: string) {
|
|
14
|
+
this.client = new GatewayClient(url, token);
|
|
15
|
+
this.client.setEventHandler(this.handleEvent.bind(this));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async connect(): Promise<void> {
|
|
19
|
+
await this.client.connect();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
disconnect(): void {
|
|
23
|
+
this.client.disconnect();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
isConnected(): boolean {
|
|
27
|
+
return this.client.isConnected();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async listSessions(): Promise<ChatSession[]> {
|
|
31
|
+
const result = await this.client.request('sessions.list', {});
|
|
32
|
+
return (result?.sessions ?? []).map((s: any) => ({
|
|
33
|
+
key: s.key,
|
|
34
|
+
sessionId: s.sessionId ?? '',
|
|
35
|
+
kind: s.kind ?? 'direct',
|
|
36
|
+
updatedAt: s.updatedAt ?? 0,
|
|
37
|
+
model: s.model,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async getHistory(sessionKey: string, limit = 200): Promise<ChatMessage[]> {
|
|
42
|
+
const result = await this.client.request('chat.history', { sessionKey, limit });
|
|
43
|
+
return (result?.messages ?? [])
|
|
44
|
+
.filter((m: any) => m.role === 'user' || m.role === 'assistant')
|
|
45
|
+
.map((m: any) => {
|
|
46
|
+
const content = Array.isArray(m.content)
|
|
47
|
+
? m.content.filter((c: any) => c.type === 'text').map((c: any) => c.text).join('')
|
|
48
|
+
: String(m.content ?? '');
|
|
49
|
+
return {
|
|
50
|
+
role: m.role,
|
|
51
|
+
content,
|
|
52
|
+
timestamp: typeof m.timestamp === 'number'
|
|
53
|
+
? new Date(m.timestamp).toISOString()
|
|
54
|
+
: m.timestamp ?? new Date().toISOString(),
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async sendMessage(params: {
|
|
60
|
+
sessionKey: string;
|
|
61
|
+
text: string;
|
|
62
|
+
idempotencyKey: string;
|
|
63
|
+
attachments?: ChatAttachment[];
|
|
64
|
+
onDelta?: (text: string) => void;
|
|
65
|
+
onDone?: (text: string) => void;
|
|
66
|
+
onError?: (error: string) => void;
|
|
67
|
+
}): Promise<void> {
|
|
68
|
+
// Register stream callbacks BEFORE sending
|
|
69
|
+
this.streamCallbacks.set(params.idempotencyKey, {
|
|
70
|
+
onDelta: params.onDelta,
|
|
71
|
+
onDone: params.onDone,
|
|
72
|
+
onError: params.onError,
|
|
73
|
+
lastText: '',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const payload: any = {
|
|
77
|
+
sessionKey: params.sessionKey,
|
|
78
|
+
message: params.text,
|
|
79
|
+
deliver: false,
|
|
80
|
+
idempotencyKey: params.idempotencyKey,
|
|
81
|
+
};
|
|
82
|
+
if (params.attachments?.length) {
|
|
83
|
+
payload.attachments = params.attachments;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
await this.client.request('chat.send', payload);
|
|
88
|
+
// Request accepted — response comes via events
|
|
89
|
+
} catch (err) {
|
|
90
|
+
this.streamCallbacks.delete(params.idempotencyKey);
|
|
91
|
+
params.onError?.(err instanceof Error ? err.message : String(err));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async abort(sessionKey: string, runId?: string): Promise<void> {
|
|
96
|
+
await this.client.request('chat.abort', { sessionKey, runId }).catch(() => {});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private handleEvent(event: { event: string; payload: any }): void {
|
|
100
|
+
const data = event.payload;
|
|
101
|
+
if (!data || !data.sessionKey) return;
|
|
102
|
+
|
|
103
|
+
// Stream events have runId matching idempotencyKey
|
|
104
|
+
const runId = data.runId;
|
|
105
|
+
const cb = runId ? this.streamCallbacks.get(runId) : null;
|
|
106
|
+
if (!cb) return;
|
|
107
|
+
|
|
108
|
+
if (data.state === 'delta' && data.message != null) {
|
|
109
|
+
// message is accumulated text
|
|
110
|
+
const text = typeof data.message === 'string'
|
|
111
|
+
? data.message
|
|
112
|
+
: Array.isArray(data.message)
|
|
113
|
+
? data.message.filter((c: any) => c.type === 'text').map((c: any) => c.text).join('')
|
|
114
|
+
: '';
|
|
115
|
+
cb.lastText = text;
|
|
116
|
+
cb.onDelta?.(text);
|
|
117
|
+
} else if (data.state === 'final') {
|
|
118
|
+
this.streamCallbacks.delete(runId!);
|
|
119
|
+
cb.onDone?.(cb.lastText);
|
|
120
|
+
} else if (data.state === 'aborted') {
|
|
121
|
+
this.streamCallbacks.delete(runId!);
|
|
122
|
+
cb.onDone?.(cb.lastText); // still send whatever we have
|
|
123
|
+
} else if (data.state === 'error') {
|
|
124
|
+
this.streamCallbacks.delete(runId!);
|
|
125
|
+
const errMsg = typeof data.error === 'string' ? data.error : data.error?.message ?? 'Unknown error';
|
|
126
|
+
cb.onError?.(errMsg);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
|
|
4
|
+
interface PendingRequest {
|
|
5
|
+
resolve: (payload: any) => void;
|
|
6
|
+
reject: (err: Error) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type EventHandler = (event: { event: string; payload: any; seq?: number }) => void;
|
|
10
|
+
|
|
11
|
+
export class GatewayClient {
|
|
12
|
+
private ws: WebSocket | null = null;
|
|
13
|
+
private pending = new Map<string, PendingRequest>();
|
|
14
|
+
private onEvent: EventHandler | null = null;
|
|
15
|
+
private connected = false;
|
|
16
|
+
private token: string;
|
|
17
|
+
private url: string;
|
|
18
|
+
|
|
19
|
+
constructor(url: string, token: string) {
|
|
20
|
+
this.url = url;
|
|
21
|
+
this.token = token;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
setEventHandler(handler: EventHandler): void {
|
|
25
|
+
this.onEvent = handler;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
connect(): Promise<void> {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
this.ws = new WebSocket(this.url);
|
|
31
|
+
|
|
32
|
+
this.ws.on('open', () => {
|
|
33
|
+
// Wait for connect.challenge event, then authenticate
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
this.ws.on('message', (raw) => {
|
|
37
|
+
try {
|
|
38
|
+
const msg = JSON.parse(raw.toString());
|
|
39
|
+
this.handleMessage(msg);
|
|
40
|
+
} catch {}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
this.ws.on('error', (e) => {
|
|
44
|
+
if (!this.connected) reject(e);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
this.ws.on('close', () => {
|
|
48
|
+
this.connected = false;
|
|
49
|
+
this.flushPending(new Error('Connection closed'));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Wait for connect.challenge → authenticate → resolve
|
|
53
|
+
const origHandler = this.onEvent;
|
|
54
|
+
this.onEvent = async (ev) => {
|
|
55
|
+
if (ev.event === 'connect.challenge') {
|
|
56
|
+
try {
|
|
57
|
+
await this.request('connect', {
|
|
58
|
+
auth: { token: this.token },
|
|
59
|
+
scopes: ['operator.admin'],
|
|
60
|
+
caps: [],
|
|
61
|
+
});
|
|
62
|
+
this.connected = true;
|
|
63
|
+
this.onEvent = origHandler;
|
|
64
|
+
resolve();
|
|
65
|
+
} catch (e) {
|
|
66
|
+
reject(e);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
setTimeout(() => {
|
|
72
|
+
if (!this.connected) reject(new Error('Connect timeout'));
|
|
73
|
+
}, 10_000);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
disconnect(): void {
|
|
78
|
+
this.ws?.close();
|
|
79
|
+
this.ws = null;
|
|
80
|
+
this.connected = false;
|
|
81
|
+
this.flushPending(new Error('Disconnected'));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
isConnected(): boolean {
|
|
85
|
+
return this.connected && this.ws?.readyState === WebSocket.OPEN;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
request(method: string, params: any): Promise<any> {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
91
|
+
return reject(new Error('Not connected'));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const id = randomUUID();
|
|
95
|
+
this.pending.set(id, { resolve, reject });
|
|
96
|
+
this.ws.send(JSON.stringify({ type: 'req', id, method, params }));
|
|
97
|
+
|
|
98
|
+
// Timeout per request
|
|
99
|
+
setTimeout(() => {
|
|
100
|
+
if (this.pending.has(id)) {
|
|
101
|
+
this.pending.delete(id);
|
|
102
|
+
reject(new Error(`Request timeout: ${method}`));
|
|
103
|
+
}
|
|
104
|
+
}, 120_000);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private handleMessage(msg: any): void {
|
|
109
|
+
if (msg.type === 'event') {
|
|
110
|
+
this.onEvent?.(msg);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (msg.type === 'res') {
|
|
114
|
+
const req = this.pending.get(msg.id);
|
|
115
|
+
if (!req) return;
|
|
116
|
+
this.pending.delete(msg.id);
|
|
117
|
+
if (msg.ok) {
|
|
118
|
+
req.resolve(msg.payload);
|
|
119
|
+
} else {
|
|
120
|
+
req.reject(new Error(msg.error?.message ?? 'Request failed'));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private flushPending(err: Error): void {
|
|
126
|
+
for (const [, req] of this.pending) {
|
|
127
|
+
req.reject(err);
|
|
128
|
+
}
|
|
129
|
+
this.pending.clear();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { OpenclawGatewayAdapter } from './gateway-adapter.js';
|
|
2
|
+
export type { IChatProvider, ChatSession, ChatMessage, ChatAttachment } from './types.js';
|
|
3
|
+
|
|
4
|
+
import { OpenclawGatewayAdapter } from './gateway-adapter.js';
|
|
5
|
+
import type { IChatProvider } from './types.js';
|
|
6
|
+
|
|
7
|
+
let _provider: IChatProvider | null = null;
|
|
8
|
+
|
|
9
|
+
export function createChatProvider(url: string, token: string): IChatProvider & { connect(): Promise<void>; disconnect(): void } {
|
|
10
|
+
const adapter = new OpenclawGatewayAdapter(url, token);
|
|
11
|
+
_provider = adapter;
|
|
12
|
+
return adapter;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getChatProvider(): IChatProvider | null {
|
|
16
|
+
return _provider;
|
|
17
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface ChatSession {
|
|
2
|
+
key: string;
|
|
3
|
+
sessionId: string;
|
|
4
|
+
kind: string;
|
|
5
|
+
updatedAt: number;
|
|
6
|
+
model?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ChatMessage {
|
|
10
|
+
role: 'user' | 'assistant';
|
|
11
|
+
content: string; // normalized to plain text
|
|
12
|
+
timestamp: string; // ISO 8601
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ChatAttachment {
|
|
16
|
+
type: 'image';
|
|
17
|
+
mimeType: string;
|
|
18
|
+
content: string; // base64
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface IChatProvider {
|
|
22
|
+
listSessions(): Promise<ChatSession[]>;
|
|
23
|
+
getHistory(sessionKey: string, limit?: number): Promise<ChatMessage[]>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Send message. Returns after the agent finishes (full response).
|
|
27
|
+
* Calls onDelta with accumulated text during streaming.
|
|
28
|
+
* Calls onDone when agent response is complete.
|
|
29
|
+
*/
|
|
30
|
+
sendMessage(params: {
|
|
31
|
+
sessionKey: string;
|
|
32
|
+
text: string;
|
|
33
|
+
idempotencyKey: string;
|
|
34
|
+
attachments?: ChatAttachment[];
|
|
35
|
+
onDelta?: (accumulatedText: string) => void;
|
|
36
|
+
onDone?: (finalText: string) => void;
|
|
37
|
+
onError?: (error: string) => void;
|
|
38
|
+
}): Promise<void>;
|
|
39
|
+
|
|
40
|
+
abort(sessionKey: string, runId?: string): Promise<void>;
|
|
41
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* heartbeat:<agentId> — heartbeat publications from agent
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export type CommandType =
|
|
7
|
+
export type CommandType =
|
|
8
|
+
| 'exec' | 'restart' | 'deploy' | 'config' | 'pair' | 'stop' | 'backup'
|
|
9
|
+
| 'chat_list_sessions' | 'chat_history' | 'chat_send';
|
|
8
10
|
|
|
9
11
|
export interface AgentCommand {
|
|
10
12
|
id: string;
|
|
@@ -56,6 +58,7 @@ export interface BoardEvent {
|
|
|
56
58
|
export interface BoardInfo {
|
|
57
59
|
board: {
|
|
58
60
|
id: string;
|
|
61
|
+
workspaceId: string;
|
|
59
62
|
columns: Array<{
|
|
60
63
|
id: string;
|
|
61
64
|
name: string;
|