@buz-extensions/buz 1.0.0-beta.1

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/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # @buz-extensions/buz
2
+
3
+ buz channel plugin for OpenClaw. This plugin connects OpenClaw to buz through a gRPC bidirectional stream.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ openclaw plugins install @buz-extensions/buz
9
+ ```
10
+
11
+ ## Configuration
12
+
13
+ Configure the `buz` channel in your OpenClaw config.
14
+
15
+ ### Single account
16
+
17
+ ```yaml
18
+ channels:
19
+ buz:
20
+ enabled: true
21
+ accounts:
22
+ default:
23
+ enabled: true
24
+ serverAddress: grpc.buz.ai:443
25
+ secretKey: your-secret-key
26
+ ```
27
+
28
+ ### Multiple accounts
29
+
30
+ ```yaml
31
+ channels:
32
+ buz:
33
+ enabled: true
34
+ accounts:
35
+ accountA:
36
+ enabled: true
37
+ serverAddress: grpc.buz.ai:443
38
+ secretKey: secret-key-a
39
+ accountB:
40
+ enabled: true
41
+ serverAddress: grpc.buz.ai:443
42
+ secretKey: secret-key-b
43
+ ```
44
+
45
+ ## Features
46
+
47
+ - buz channel registration
48
+ - setup wizard support
49
+ - inbound message dispatch
50
+ - outbound text sending
51
+ - account-based runtime management
52
+ - gRPC reconnect and heartbeat
53
+
54
+ ## Notes
55
+
56
+ - Channel id: `buz`
57
+ - Requires OpenClaw runtime with plugin support
58
+ - The package includes `openclaw.plugin.json` for native plugin validation
package/index.ts ADDED
@@ -0,0 +1,199 @@
1
+ import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk/line";
2
+ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/line";
3
+ import { buildChannelConfigSchema } from "openclaw/plugin-sdk/line";
4
+ import { z } from "zod";
5
+
6
+ const BuzAccountSchema = z
7
+ .object({
8
+ enabled: z.boolean().optional(),
9
+ serverAddress: z.string().optional(),
10
+ secretKey: z.string().optional(),
11
+ })
12
+ .partial();
13
+
14
+ const BuzConfigSchema = z
15
+ .object({
16
+ enabled: z.boolean().optional(),
17
+ serverAddress: z.string().optional(),
18
+ secretKey: z.string().optional(),
19
+ accounts: z.record(z.string(), BuzAccountSchema.optional()).optional(),
20
+ })
21
+ .partial();
22
+
23
+ export const buzChannelPlugin = {
24
+ id: "buz",
25
+ meta: {
26
+ id: "buz",
27
+ label: "buz",
28
+ selectionLabel: "buz (gRPC)",
29
+ docsPath: "/channels/buz",
30
+ blurb: "Connect OpenClaw to buz via gRPC bidirectional stream.",
31
+ },
32
+ capabilities: {
33
+ chatTypes: ["direct", "group"],
34
+ reactions: false,
35
+ threads: true,
36
+ media: false,
37
+ },
38
+ configSchema: buildChannelConfigSchema(BuzConfigSchema),
39
+ config: {
40
+ listAccountIds: (cfg: any) => {
41
+ const accounts = cfg?.channels?.["buz"]?.accounts || {};
42
+ const ids = Object.keys(accounts);
43
+ return ids.length > 0 ? ids : ["default"];
44
+ },
45
+ resolveAccount: (cfg: any, accountId?: string | null) => {
46
+ const id = accountId || "default";
47
+ const channelConfig = cfg?.channels?.["buz"] ?? {};
48
+
49
+ const topLevelDefault =
50
+ id === "default"
51
+ ? {
52
+ enabled: channelConfig.enabled,
53
+ serverAddress: channelConfig.serverAddress,
54
+ secretKey: channelConfig.secretKey,
55
+ }
56
+ : undefined;
57
+
58
+ const accountConfig = channelConfig?.accounts?.[id] ?? topLevelDefault ?? {};
59
+ const configured = Boolean(accountConfig.serverAddress && accountConfig.secretKey);
60
+
61
+ return {
62
+ accountId: id,
63
+ name: `buz (${id})`,
64
+ enabled: accountConfig.enabled !== false,
65
+ serverAddress: accountConfig.serverAddress,
66
+ secretKey: accountConfig.secretKey,
67
+ configured,
68
+ config: accountConfig,
69
+ };
70
+ },
71
+ isConfigured: (account: any) => Boolean(account.configured),
72
+ describeAccount: (account: any) => ({
73
+ accountId: account.accountId,
74
+ name: account.name,
75
+ enabled: account.enabled,
76
+ configured: Boolean(account.configured),
77
+ }),
78
+ },
79
+ messaging: {
80
+ normalizeTarget: (target: string) => target,
81
+ targetResolver: {
82
+ looksLikeId: (_id: string) => true,
83
+ hint: "<targetId>",
84
+ },
85
+ resolveSessionTarget: ({ id }: any) => id,
86
+ },
87
+ setupWizard: {
88
+ steps: [
89
+ {
90
+ id: "serverAddress",
91
+ type: "text",
92
+ label: "Server Address",
93
+ placeholder: "e.g. grpc.buz.ai:443",
94
+ },
95
+ {
96
+ id: "secretKey",
97
+ type: "password",
98
+ label: "Secret Key",
99
+ placeholder: "Enter your IM Secret Key",
100
+ },
101
+ ],
102
+ },
103
+ setup: {
104
+ validateInput: async (params: any) => {
105
+ const { setupAdapter } = await import("./src/setup.js");
106
+ return setupAdapter.validateInput(params);
107
+ },
108
+ applyAccountConfig: async (params: any) => {
109
+ const { setupAdapter } = await import("./src/setup.js");
110
+ const result = await setupAdapter.applyAccountConfig(params);
111
+ return result;
112
+ },
113
+ },
114
+ outbound: {
115
+ deliveryMode: "direct",
116
+ chunker: null,
117
+ textChunkLimit: 4000,
118
+ sendText: async ({ to, text, accountId, replyToId, threadId, cfg }: any) => {
119
+ const { sendText } = await import("./src/outbound.js");
120
+ return await sendText({ to, text, accountId, replyToId, threadId, cfg });
121
+ },
122
+ sendMedia: async ({ to }: any) => {
123
+ return { messageId: "media-unsupported", chatId: to };
124
+ },
125
+ },
126
+ status: {
127
+ defaultRuntime: {
128
+ accountId: "default",
129
+ running: false,
130
+ connected: false,
131
+ lastStartAt: null,
132
+ lastStopAt: null,
133
+ lastError: null,
134
+ lastInboundAt: null,
135
+ lastOutboundAt: null,
136
+ },
137
+ buildChannelSummary: ({ snapshot }: any) => {
138
+ if (!snapshot?.configured)
139
+ return { text: "Not configured", kind: "muted", configured: false };
140
+ if (snapshot?.connected) return { text: "Connected", kind: "healthy", configured: true };
141
+ if (snapshot?.running) return { text: "Starting", kind: "warning", configured: true };
142
+ return { text: "Configured", kind: "warning", configured: true };
143
+ },
144
+ buildAccountSnapshot: ({ account, runtime }: any) => {
145
+ return {
146
+ accountId: account.accountId,
147
+ name: account.name,
148
+ enabled: account.enabled,
149
+ configured: Boolean(account.configured),
150
+ running: runtime?.running ?? false,
151
+ connected: runtime?.connected ?? false,
152
+ lastStartAt: runtime?.lastStartAt ?? null,
153
+ lastStopAt: runtime?.lastStopAt ?? null,
154
+ mode: "grpc",
155
+ ...(runtime?.lastError ? { lastError: runtime.lastError } : {}),
156
+ ...(runtime?.lastInboundAt ? { lastInboundAt: runtime.lastInboundAt } : {}),
157
+ ...(runtime?.lastOutboundAt ? { lastOutboundAt: runtime.lastOutboundAt } : {}),
158
+ };
159
+ },
160
+ },
161
+ gateway: {
162
+ startAccount: async (ctx: any) => {
163
+ const account = ctx.account;
164
+ const serverAddress = account.serverAddress;
165
+ const secretKey = account.secretKey;
166
+
167
+ if (!serverAddress || !secretKey) {
168
+ throw new Error("Missing serverAddress or secretKey for buz");
169
+ }
170
+
171
+ ctx.log?.info(`[${account.accountId}] starting buz gRPC provider to ${serverAddress}`);
172
+
173
+ const { startGateway } = await import("./src/gateway.js");
174
+ const result = await startGateway(ctx, serverAddress, secretKey);
175
+ return result;
176
+ },
177
+ stopAccount: async (ctx: any) => {
178
+ const { stopGateway } = await import("./src/gateway.js");
179
+ return stopGateway(ctx);
180
+ },
181
+ },
182
+ } as any;
183
+
184
+ export const setBuzRuntime = (_runtime: any) => {
185
+ // buz doesn't need special runtime setup yet
186
+ };
187
+
188
+ const plugin = {
189
+ id: "buz",
190
+ name: "buz Plugin",
191
+ description: "Connects OpenClaw to buz",
192
+ configSchema: emptyPluginConfigSchema(),
193
+ register(api: OpenClawPluginApi) {
194
+ setBuzRuntime(api.runtime);
195
+ api.registerChannel({ plugin: buzChannelPlugin as ChannelPlugin });
196
+ },
197
+ };
198
+
199
+ export default plugin;
@@ -0,0 +1,40 @@
1
+ {
2
+ "id": "buz",
3
+ "channels": ["buz"],
4
+ "name": "buz Plugin",
5
+ "description": "Connects OpenClaw to buz",
6
+ "enabledByDefault": true,
7
+ "configSchema": {
8
+ "type": "object",
9
+ "additionalProperties": false,
10
+ "properties": {
11
+ "enabled": {
12
+ "type": "boolean"
13
+ },
14
+ "serverAddress": {
15
+ "type": "string"
16
+ },
17
+ "secretKey": {
18
+ "type": "string"
19
+ },
20
+ "accounts": {
21
+ "type": "object",
22
+ "additionalProperties": {
23
+ "type": "object",
24
+ "additionalProperties": false,
25
+ "properties": {
26
+ "enabled": {
27
+ "type": "boolean"
28
+ },
29
+ "serverAddress": {
30
+ "type": "string"
31
+ },
32
+ "secretKey": {
33
+ "type": "string"
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@buz-extensions/buz",
3
+ "version": "1.0.0-beta.1",
4
+ "description": "OpenClaw buz channel plugin",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "openclaw",
9
+ "openclaw-plugin",
10
+ "openclaw-channel",
11
+ "buz",
12
+ "grpc"
13
+ ],
14
+ "files": [
15
+ "index.ts",
16
+ "setup-entry.ts",
17
+ "openclaw.plugin.json",
18
+ "src",
19
+ "proto",
20
+ "README.md"
21
+ ],
22
+ "exports": {
23
+ ".": "./index.ts",
24
+ "./setup-entry": "./setup-entry.ts",
25
+ "./openclaw.plugin.json": "./openclaw.plugin.json"
26
+ },
27
+ "dependencies": {
28
+ "@grpc/grpc-js": "^1.10.0",
29
+ "@grpc/proto-loader": "^0.7.10",
30
+ "zod": "^3.25.76"
31
+ },
32
+ "peerDependencies": {
33
+ "openclaw": "*"
34
+ },
35
+ "openclaw": {
36
+ "extensions": [
37
+ "./index.ts"
38
+ ],
39
+ "setupEntry": "./setup-entry.ts",
40
+ "channel": {
41
+ "id": "buz",
42
+ "label": "buz",
43
+ "selectionLabel": "buz (gRPC)",
44
+ "docsPath": "/channels/buz"
45
+ },
46
+ "install": {
47
+ "npmSpec": "@buz-extensions/buz",
48
+ "localPath": "extensions/buz_openclaw_channel",
49
+ "defaultChoice": "npm"
50
+ },
51
+ "bundle": {
52
+ "stageRuntimeDependencies": true
53
+ }
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
57
+ }
58
+ }
@@ -0,0 +1,70 @@
1
+ syntax = "proto3";
2
+ package buz.dc.ai.bridge;
3
+
4
+ // 核心桥接服务
5
+ service OpenClawBridgeService {
6
+ // 双向流式通道
7
+ rpc ConnectStream(stream ClientToBridgeMessage) returns (stream BridgeToClientMessage);
8
+ }
9
+
10
+ // OpenClaw (Client) 发送给 buz (Server) 的消息
11
+ message ClientToBridgeMessage {
12
+ oneof payload {
13
+ AuthRequest auth_req = 1; // 1. 鉴权请求 (建连后第一条必须是这个)
14
+ OutboundMessage outbound_msg = 2; // 2. OpenClaw 发出的回复消息
15
+ HeartbeatPing ping = 3; // 3. 客户端心跳
16
+ ActionStatusReport action_status = 4;// 4. [可选] 已读回执/正在输入状态
17
+ }
18
+ }
19
+
20
+ // buz (Server) 发送给 OpenClaw (Client) 的消息
21
+ message BridgeToClientMessage {
22
+ oneof payload {
23
+ AuthResponse auth_res = 1; // 1. 鉴权结果
24
+ InboundMessage inbound_msg = 2; // 2. IM 传递给 OpenClaw 的新消息
25
+ HeartbeatPong pong = 3; // 3. 服务端心跳响应
26
+ }
27
+ }
28
+
29
+ // --- 详细子结构 ---
30
+
31
+ message AuthRequest {
32
+ string secret_key = 1; // 用户在 OpenClaw 输入的接入凭证
33
+ string openclaw_id = 2; // [可选] 客户端唯一标识
34
+ }
35
+
36
+ message AuthResponse {
37
+ bool success = 1;
38
+ string error_message = 2;
39
+ string account_id = 3; // 绑定成功的 IM 账户 ID
40
+ }
41
+
42
+ message InboundMessage {
43
+ string message_id = 1; // IM 侧的全局消息 ID
44
+ string sender_id = 2; // 发送者 ID
45
+ string sender_name = 3; // 发送者昵称
46
+ string chat_type = 4; // 枚举: "direct"(单聊), "group"(群聊)
47
+ string group_id = 5; // 如果是群聊,群 ID
48
+ string content_text = 6; // 文本内容
49
+ // 可扩展媒体字段 (media_url 等)
50
+ }
51
+
52
+ message OutboundMessage {
53
+ string reply_to_id = 1; // 回复的目标消息 ID (Thread 追踪)
54
+ string target_id = 2; // 接收方 ID (用户 ID 或 群组 ID)
55
+ string chat_type = 3; // "direct" 或 "group"
56
+ string content_text = 4; // AI 生成的回复文本
57
+ // 可扩展媒体字段
58
+ }
59
+
60
+ message HeartbeatPing {
61
+ int64 timestamp = 1;
62
+ }
63
+
64
+ message HeartbeatPong {
65
+ int64 timestamp = 1;
66
+ }
67
+
68
+ message ActionStatusReport {
69
+ string status = 1;
70
+ }
package/setup-entry.ts ADDED
@@ -0,0 +1,5 @@
1
+ import plugin, { buzChannelPlugin, setBuzRuntime } from "./index.js";
2
+
3
+ export { buzChannelPlugin, setBuzRuntime } from "./index.js";
4
+
5
+ export default plugin;
package/src/gateway.ts ADDED
@@ -0,0 +1,320 @@
1
+ import { resolve } from "path";
2
+ import { dirname } from "path";
3
+ import { fileURLToPath } from "url";
4
+ import * as grpc from "@grpc/grpc-js";
5
+ import * as protoLoader from "@grpc/proto-loader";
6
+ import { handleInboundMessage } from "./inbound.js";
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+
11
+ export const activeStreams = new Map<string, grpc.ClientDuplexStream<any, any>>();
12
+ export const activeClients = new Map<string, any>();
13
+
14
+ function setStatus(ctx: any, patch: Record<string, unknown>) {
15
+ console.log(`[buz gateway] setStatus called:`, patch);
16
+ ctx.setStatus?.({
17
+ accountId: ctx.account?.accountId || "default",
18
+ ...patch,
19
+ });
20
+ }
21
+
22
+ export async function startGateway(ctx: any, serverAddress: string, secretKey: string) {
23
+ const PROTO_PATH = resolve(__dirname, "../proto/buz.proto");
24
+
25
+ let packageDefinition;
26
+ try {
27
+ packageDefinition = protoLoader.loadSync(PROTO_PATH, {
28
+ keepCase: true,
29
+ longs: String,
30
+ enums: String,
31
+ defaults: true,
32
+ oneofs: true,
33
+ });
34
+ console.log("[buz gateway] proto loaded successfully");
35
+ } catch (err: any) {
36
+ console.error("[buz gateway] failed to load proto:", err.message);
37
+ throw err;
38
+ }
39
+
40
+ const protoDescriptor = grpc.loadPackageDefinition(packageDefinition) as any;
41
+ const bridgePackage = protoDescriptor.buz.dc.ai.bridge;
42
+ console.log("[buz gateway] bridgePackage exists:", !!bridgePackage);
43
+ console.log(
44
+ "[buz gateway] OpenClawBridgeService exists:",
45
+ !!bridgePackage?.OpenClawBridgeService,
46
+ );
47
+
48
+ const useSsl = serverAddress.includes("443") || serverAddress.startsWith("https");
49
+ console.log("[buz gateway] using SSL:", useSsl);
50
+
51
+ const credentials = useSsl ? grpc.credentials.createSsl() : grpc.credentials.createInsecure();
52
+
53
+ const cleanAddress = serverAddress.replace(/^https?:\/\//, "");
54
+ console.log("[buz gateway] connecting to:", cleanAddress);
55
+
56
+ let client;
57
+ try {
58
+ client = new bridgePackage.OpenClawBridgeService(cleanAddress, credentials);
59
+ console.log("[buz gateway] gRPC client created");
60
+ } catch (err: any) {
61
+ console.error("[buz gateway] failed to create client:", err.message);
62
+ throw err;
63
+ }
64
+
65
+ const accountId = ctx.account.accountId;
66
+ console.log("[buz gateway] accountId:", accountId);
67
+ activeClients.set(accountId, client);
68
+ console.log("[buz gateway] client stored in activeClients, total:", activeClients.size);
69
+
70
+ let stream: grpc.ClientDuplexStream<any, any> | null = null;
71
+ let reconnectAttempts = 0;
72
+ let everConnected = false;
73
+ let isActive = true;
74
+ let pingInterval: NodeJS.Timeout | null = null;
75
+ let reconnectTimer: NodeJS.Timeout | null = null;
76
+
77
+ const cleanupStream = () => {
78
+ console.log("[buz gateway] cleanupStream called");
79
+ if (pingInterval) {
80
+ clearInterval(pingInterval);
81
+ pingInterval = null;
82
+ }
83
+ if (stream) {
84
+ activeStreams.delete(accountId);
85
+ stream.removeAllListeners();
86
+ stream = null;
87
+ }
88
+ };
89
+
90
+ const scheduleReconnect = (reason?: string) => {
91
+ console.log(`[buz gateway] scheduleReconnect called, reason: ${reason || "none"}`);
92
+ cleanupStream();
93
+ // Keep running: true during reconnect attempts
94
+ setStatus(ctx, {
95
+ connected: false,
96
+ running: true, // Still trying to run
97
+ ...(reason ? { lastError: reason } : {}),
98
+ });
99
+
100
+ if (!isActive || ctx.abortSignal?.aborted) {
101
+ console.log("[buz gateway] aborting reconnect (isActive=false or aborted)");
102
+ return;
103
+ }
104
+
105
+ const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
106
+ reconnectAttempts += 1;
107
+ console.log(`[buz gateway] reconnecting in ${delay}ms (attempt ${reconnectAttempts})`);
108
+ ctx.log?.warn?.(
109
+ `[${accountId}] buz gRPC disconnected${reason ? `: ${reason}` : ""}; reconnecting in ${delay}ms`,
110
+ );
111
+ reconnectTimer = setTimeout(() => {
112
+ reconnectTimer = null;
113
+ connect();
114
+ }, delay);
115
+ };
116
+
117
+ const connect = () => {
118
+ console.log("[buz gateway] connect() called");
119
+ if (!isActive || ctx.abortSignal?.aborted) {
120
+ console.log("[buz gateway] aborting connect (isActive=false or aborted)");
121
+ return;
122
+ }
123
+
124
+ cleanupStream();
125
+ setStatus(ctx, {
126
+ connected: false,
127
+ running: true, // Mark as running while connecting
128
+ lastError: everConnected ? null : "connecting",
129
+ });
130
+
131
+ // Create metadata with authorization header
132
+ const metadata = new grpc.Metadata();
133
+ metadata.add("authorization", `Bearer ${secretKey}`);
134
+ metadata.add("x-openclaw-id", accountId);
135
+ console.log("[buz gateway] metadata created with authorization header");
136
+
137
+ try {
138
+ stream = client.ConnectStream(metadata);
139
+ console.log("[buz gateway] stream created:", !!stream);
140
+ } catch (err: any) {
141
+ console.error("[buz gateway] failed to create gRPC stream:", err?.message || String(err));
142
+ ctx.log?.error?.(
143
+ `[${accountId}] failed to create gRPC stream: ${err?.message || String(err)}`,
144
+ );
145
+ scheduleReconnect(err?.message || String(err));
146
+ return;
147
+ }
148
+
149
+ if (!stream) {
150
+ console.error("[buz gateway] stream is null after creation");
151
+ ctx.log?.error?.(`[${accountId}] failed to create gRPC stream: stream is null`);
152
+ scheduleReconnect("stream is null");
153
+ return;
154
+ }
155
+
156
+ activeStreams.set(accountId, stream);
157
+ console.log("[buz gateway] stream stored, total active streams:", activeStreams.size);
158
+
159
+ // Send auth request via message body (for backward compatibility)
160
+ const authRequest = {
161
+ auth_req: {
162
+ secret_key: secretKey,
163
+ openclaw_id: accountId,
164
+ },
165
+ };
166
+
167
+ try {
168
+ stream.write(authRequest);
169
+ } catch (err: any) {
170
+ console.error("[buz gateway] failed to send auth request:", err?.message);
171
+ // Don't schedule reconnect here, let the stream error handler deal with it
172
+ }
173
+
174
+ stream.on("data", async (msg: any) => {
175
+ console.log("[buz gateway] stream received data:", Object.keys(msg));
176
+
177
+ if (msg.auth_res) {
178
+ console.log("[buz gateway] received auth_res:", msg.auth_res);
179
+ if (msg.auth_res.success) {
180
+ everConnected = true;
181
+ reconnectAttempts = 0;
182
+ // IMPORTANT: Must include running: true
183
+ setStatus(ctx, {
184
+ running: true,
185
+ connected: true,
186
+ lastError: null,
187
+ lastStartAt: Date.now(),
188
+ });
189
+ ctx.log?.info?.(`[${accountId}] buz gRPC authenticated successfully.`);
190
+ console.log("[buz gateway] authentication successful");
191
+
192
+ if (pingInterval) clearInterval(pingInterval);
193
+ pingInterval = setInterval(() => {
194
+ console.log("[buz gateway] sending heartbeat");
195
+ if (stream) {
196
+ try {
197
+ stream.write({
198
+ ping: { timestamp: Date.now() },
199
+ });
200
+ } catch (err: any) {
201
+ ctx.log?.warn?.(
202
+ `[${accountId}] failed to send buz heartbeat: ${err?.message || String(err)}`,
203
+ );
204
+ }
205
+ }
206
+ }, 30000);
207
+ console.log("[buz gateway] heartbeat interval set (30s)");
208
+ } else {
209
+ const reason = `Auth failed: ${msg.auth_res.error_message || "unknown error"}`;
210
+ console.error("[buz gateway] authentication failed:", reason);
211
+ ctx.log?.error?.(`[${accountId}] ${reason}`);
212
+ scheduleReconnect(reason);
213
+ }
214
+ return;
215
+ }
216
+
217
+ if (msg.inbound_msg) {
218
+ console.log("[buz gateway] received inbound message");
219
+ // Update lastInboundAt when receiving messages
220
+ setStatus(ctx, {
221
+ lastInboundAt: Date.now(),
222
+ running: true,
223
+ connected: true,
224
+ });
225
+ await handleInboundMessage(ctx, msg.inbound_msg);
226
+ return;
227
+ }
228
+
229
+ if (msg.pong) {
230
+ return;
231
+ }
232
+
233
+ console.log("[buz gateway] received unknown message type:", Object.keys(msg));
234
+ });
235
+
236
+ stream.on("end", () => {
237
+ console.log("[buz gateway] stream ended");
238
+ scheduleReconnect("stream ended");
239
+ });
240
+
241
+ stream.on("error", (err: any) => {
242
+ const reason = err?.message || String(err);
243
+ console.error("[buz gateway] stream error:", reason);
244
+ ctx.log?.error?.(`[${accountId}] gRPC stream error: ${reason}`);
245
+ scheduleReconnect(reason);
246
+ });
247
+
248
+ console.log("[buz gateway] stream event handlers registered");
249
+ };
250
+
251
+ console.log("[buz gateway] initializing connection...");
252
+ setStatus(ctx, {
253
+ running: true, // Mark as running from the start
254
+ connected: false,
255
+ lastError: null,
256
+ lastStartAt: Date.now(),
257
+ });
258
+ connect();
259
+ console.log("[buz gateway] connect() invoked");
260
+
261
+ ctx.abortSignal?.addEventListener("abort", () => {
262
+ console.log("[buz gateway] abort signal received");
263
+ isActive = false;
264
+ if (reconnectTimer) {
265
+ clearTimeout(reconnectTimer);
266
+ reconnectTimer = null;
267
+ }
268
+ cleanupStream();
269
+ const activeStream = activeStreams.get(accountId);
270
+ if (activeStream) {
271
+ activeStream.cancel();
272
+ activeStreams.delete(accountId);
273
+ }
274
+ const activeClient = activeClients.get(accountId);
275
+ if (activeClient) {
276
+ activeClient.close();
277
+ activeClients.delete(accountId);
278
+ }
279
+ setStatus(ctx, {
280
+ connected: false,
281
+ running: false,
282
+ lastStopAt: Date.now(),
283
+ });
284
+ });
285
+
286
+ return {
287
+ running: true,
288
+ connected: false,
289
+ lastStartAt: Date.now(),
290
+ lastError: null,
291
+ };
292
+ }
293
+
294
+ export async function stopGateway(ctx: any) {
295
+ console.log("[buz gateway] stopGateway called");
296
+ const accountId = ctx.account?.accountId || "default";
297
+ console.log("[buz gateway] stopping account:", accountId);
298
+
299
+ const stream = activeStreams.get(accountId);
300
+ if (stream) {
301
+ console.log("[buz gateway] cancelling stream");
302
+ stream.cancel();
303
+ activeStreams.delete(accountId);
304
+ }
305
+
306
+ const client = activeClients.get(accountId);
307
+ if (client) {
308
+ console.log("[buz gateway] closing client");
309
+ client.close();
310
+ activeClients.delete(accountId);
311
+ }
312
+
313
+ ctx.setStatus?.({
314
+ accountId,
315
+ connected: false,
316
+ running: false,
317
+ lastStopAt: Date.now(),
318
+ });
319
+ console.log("[buz gateway] stopGateway complete");
320
+ }
package/src/inbound.ts ADDED
@@ -0,0 +1,151 @@
1
+ import { recordInboundSession } from "openclaw/plugin-sdk";
2
+ import { sendText } from "./outbound.js";
3
+
4
+ function resolveDefaultAgentIdCompat(cfg: any): string {
5
+ const configured = cfg?.defaultAgentId ?? cfg?.agents?.default ?? cfg?.agent?.default;
6
+ if (typeof configured === "string" && configured.trim()) {
7
+ return configured.trim();
8
+ }
9
+ const agents = Array.isArray(cfg?.agents) ? cfg.agents : undefined;
10
+ const firstAgentId = agents?.find((entry: any) => typeof entry?.id === "string" && entry.id.trim())?.id;
11
+ return firstAgentId || "default";
12
+ }
13
+
14
+ function resolveDispatchReplyWithBufferedBlockDispatcher(ctx: any):
15
+ | ((params: any) => Promise<any>)
16
+ | null {
17
+ return (
18
+ ctx?.runtime?.channel?.reply?.dispatchReplyWithBufferedBlockDispatcher ??
19
+ ctx?.core?.channel?.reply?.dispatchReplyWithBufferedBlockDispatcher ??
20
+ null
21
+ );
22
+ }
23
+
24
+ export async function handleInboundMessage(ctx: any, inboundMsg: any) {
25
+ console.log("[buz inbound] =========================================");
26
+ console.log("[buz inbound] handleInboundMessage called");
27
+ console.log("[buz inbound] inboundMsg keys:", Object.keys(inboundMsg || {}));
28
+ console.log("[buz inbound] inboundMsg:", JSON.stringify(inboundMsg, null, 2));
29
+
30
+ const accountId = ctx.account.accountId;
31
+ const cfg = ctx.cfg;
32
+ const agentId = resolveDefaultAgentIdCompat(cfg);
33
+
34
+ console.log("[buz inbound] accountId:", accountId);
35
+ console.log("[buz inbound] agentId:", agentId);
36
+
37
+ const fromTarget =
38
+ inboundMsg.chat_type === "group"
39
+ ? `buz:group:${inboundMsg.group_id}:${inboundMsg.sender_id}`
40
+ : `buz:${inboundMsg.sender_id}`;
41
+
42
+ const toTarget =
43
+ inboundMsg.chat_type === "group"
44
+ ? `buz:group:${inboundMsg.group_id}`
45
+ : `buz:${inboundMsg.sender_id}`;
46
+
47
+ const conversationId =
48
+ inboundMsg.chat_type === "group" ? inboundMsg.group_id : inboundMsg.sender_id;
49
+
50
+ console.log("[buz inbound] fromTarget:", fromTarget);
51
+ console.log("[buz inbound] toTarget:", toTarget);
52
+ console.log("[buz inbound] conversationId:", conversationId);
53
+
54
+ const ctxPayload = {
55
+ MessageSid: inboundMsg.message_id || Date.now().toString(),
56
+ MessageSids: [inboundMsg.message_id || Date.now().toString()],
57
+ SessionKey: `buz:${accountId}:${conversationId}`,
58
+ ConversationId: conversationId,
59
+ From: fromTarget,
60
+ To: toTarget,
61
+ Body: inboundMsg.content_text || "",
62
+ BodyForAgent: inboundMsg.content_text || "",
63
+ BodyForCommands: inboundMsg.content_text || "",
64
+ Channel: "buz",
65
+ SenderName: inboundMsg.sender_name || inboundMsg.sender_id,
66
+ };
67
+
68
+ console.log("[buz inbound] ctxPayload:", JSON.stringify(ctxPayload, null, 2));
69
+
70
+ const storePath = cfg?.session?.storePath || ".openclaw/sessions";
71
+ console.log("[buz inbound] recording inbound session, storePath:", storePath);
72
+ console.log("[buz inbound] sessionKey:", ctxPayload.SessionKey);
73
+
74
+ try {
75
+ await recordInboundSession({
76
+ storePath,
77
+ sessionKey: ctxPayload.SessionKey,
78
+ ctx: ctxPayload,
79
+ updateLastRoute: {
80
+ sessionKey: ctxPayload.SessionKey,
81
+ channel: "buz",
82
+ to: toTarget,
83
+ accountId,
84
+ },
85
+ onRecordError: (err) => {
86
+ console.error("[buz inbound] failed to record session:", err);
87
+ },
88
+ });
89
+ console.log("[buz inbound] session recorded successfully");
90
+ } catch (err: any) {
91
+ console.error("[buz inbound] error recording session:", err.message);
92
+ }
93
+
94
+ const dispatchReplyWithBufferedBlockDispatcher =
95
+ resolveDispatchReplyWithBufferedBlockDispatcher(ctx);
96
+ if (!dispatchReplyWithBufferedBlockDispatcher) {
97
+ const error = new Error(
98
+ "OpenClaw reply runtime is unavailable on plugin context; missing channel reply dispatcher",
99
+ );
100
+ console.error("[buz inbound] failed to dispatch:", error.message);
101
+ ctx.log?.error?.(`[${accountId}] Failed to dispatch inbound message: ${error.message}`);
102
+ throw error;
103
+ }
104
+
105
+ try {
106
+ console.log("[buz inbound] dispatching inbound message...");
107
+ await dispatchReplyWithBufferedBlockDispatcher({
108
+ ctx: ctxPayload as any,
109
+ cfg,
110
+ dispatcherOptions: {
111
+ cfg,
112
+ agentId,
113
+ channel: "buz",
114
+ accountId,
115
+ deliver: async (payload: any, info: any) => {
116
+ console.log(
117
+ "[buz inbound] deliver called:",
118
+ info?.kind,
119
+ "text:",
120
+ payload?.text?.substring?.(0, 50),
121
+ );
122
+ if (!payload?.text) {
123
+ return;
124
+ }
125
+ await sendText({
126
+ to: toTarget,
127
+ text: payload.text,
128
+ accountId,
129
+ replyToId: inboundMsg.message_id,
130
+ });
131
+ console.log("[buz inbound] reply sent successfully via gRPC");
132
+ },
133
+ onError: (err: any, info: any) => {
134
+ console.error(`[buz inbound] ${info?.kind || "unknown"} reply failed:`, err);
135
+ },
136
+ },
137
+ replyOptions: {
138
+ disableBlockStreaming: true,
139
+ },
140
+ });
141
+ console.log("[buz inbound] message dispatched successfully");
142
+ ctx.log?.info?.(
143
+ `[${accountId}] Successfully dispatched inbound message from ${inboundMsg.sender_id}`,
144
+ );
145
+ } catch (err: any) {
146
+ console.error("[buz inbound] failed to dispatch:", err.message);
147
+ ctx.log?.error?.(`[${accountId}] Failed to dispatch inbound message: ${err.message}`);
148
+ throw err;
149
+ }
150
+ console.log("[buz inbound] =========================================");
151
+ }
@@ -0,0 +1,74 @@
1
+ import { activeStreams } from "./gateway.js";
2
+
3
+ export async function sendText(params: any) {
4
+ const { to, text, accountId, replyToId } = params;
5
+
6
+ console.log("[buz outbound] =========================================");
7
+ console.log("[buz outbound] sendText called");
8
+ console.log("[buz outbound] to:", to);
9
+ console.log("[buz outbound] text preview:", text?.substring(0, 100));
10
+ console.log("[buz outbound] text length:", text?.length);
11
+ console.log("[buz outbound] accountId:", accountId);
12
+ console.log("[buz outbound] replyToId:", replyToId);
13
+
14
+ const targetAccountId = accountId || "default";
15
+ console.log("[buz outbound] targetAccountId:", targetAccountId);
16
+
17
+ const stream = activeStreams.get(targetAccountId);
18
+ console.log("[buz outbound] activeStreams size:", activeStreams.size);
19
+ console.log("[buz outbound] stream found:", !!stream);
20
+
21
+ if (!stream) {
22
+ console.error("[buz outbound] ERROR: No active gRPC stream for account", targetAccountId);
23
+ console.log("[buz outbound] activeStreams keys:", Array.from(activeStreams.keys()));
24
+ throw new Error(`[buz] No active gRPC stream for account ${targetAccountId}`);
25
+ }
26
+
27
+ // to format might be: "buz:group:GROUP_ID" or "buz:USER_ID"
28
+ // based on inbound parsing or explicit routing
29
+ let chatType = "direct";
30
+ let targetId = to;
31
+
32
+ console.log("[buz outbound] parsing target:", to);
33
+
34
+ if (to.startsWith("buz:")) {
35
+ targetId = to.substring("buz:".length);
36
+ console.log("[buz outbound] removed 'buz:' prefix, targetId:", targetId);
37
+ }
38
+
39
+ if (targetId.startsWith("group:")) {
40
+ chatType = "group";
41
+ targetId = targetId.substring("group:".length);
42
+ console.log("[buz outbound] detected group chat, targetId:", targetId);
43
+ // if targetId has a suffix like :sender_id, remove it for group target
44
+ if (targetId.includes(":")) {
45
+ targetId = targetId.split(":")[0];
46
+ console.log("[buz outbound] removed sender suffix, final targetId:", targetId);
47
+ }
48
+ } else if (targetId.startsWith("user:")) {
49
+ targetId = targetId.substring("user:".length);
50
+ console.log("[buz outbound] removed 'user:' prefix, targetId:", targetId);
51
+ }
52
+
53
+ const outboundMsg = {
54
+ outbound_msg: {
55
+ reply_to_id: replyToId || "",
56
+ target_id: targetId,
57
+ chat_type: chatType,
58
+ content_text: text,
59
+ },
60
+ };
61
+
62
+ console.log("[buz outbound] outboundMsg:", JSON.stringify(outboundMsg, null, 2));
63
+
64
+ try {
65
+ stream.write(outboundMsg);
66
+ console.log("[buz outbound] gRPC stream write successful");
67
+ } catch (err: any) {
68
+ console.error("[buz outbound] ERROR writing to stream:", err.message);
69
+ throw err;
70
+ }
71
+
72
+ const result = { messageId: `msg-${Date.now()}`, chatId: to };
73
+ return result;
74
+ }
package/src/setup.ts ADDED
@@ -0,0 +1,99 @@
1
+ export const setupAdapter = {
2
+ validateInput: async (params: any) => {
3
+ console.log("[buz setup] validateInput called with params:", {
4
+ hasValues: !!params.values,
5
+ hasInput: !!params.input,
6
+ accountId: params.accountId,
7
+ keys: Object.keys(params.values || params.input || {}),
8
+ });
9
+
10
+ const input = params.values || params.input || {};
11
+ const { serverAddress, secretKey } = input;
12
+
13
+ console.log("[buz setup] extracted values:", {
14
+ hasServerAddress: !!serverAddress,
15
+ hasSecretKey: !!secretKey,
16
+ serverAddressLength: serverAddress?.length,
17
+ secretKeyLength: secretKey?.length,
18
+ });
19
+
20
+ if (!serverAddress) return { ok: false, error: "Server Address is required" };
21
+ if (!secretKey) return { ok: false, error: "Secret Key is required" };
22
+ return { ok: true, values: input };
23
+ },
24
+
25
+ applyAccountConfig: async (params: any) => {
26
+ const { cfg, accountId } = params;
27
+ // OpenClaw passes "input" instead of "values"
28
+ const input = params.input || params.values || {};
29
+ const resolvedAccountId = accountId || "default";
30
+
31
+ console.log("[buz setup] applyAccountConfig called:", {
32
+ accountId,
33
+ resolvedAccountId,
34
+ hasCfg: !!cfg,
35
+ hasInput: !!input,
36
+ inputKeys: Object.keys(input),
37
+ hasServerAddress: !!input.serverAddress,
38
+ hasSecretKey: !!input.secretKey,
39
+ existingChannelsKeys: Object.keys(cfg?.channels || {}),
40
+ });
41
+
42
+ // Deep clone to avoid mutations
43
+ const newCfg = JSON.parse(JSON.stringify(cfg || {}));
44
+
45
+ if (!newCfg.channels) {
46
+ newCfg.channels = {};
47
+ console.log("[buz setup] created new channels object");
48
+ }
49
+
50
+ const existingChannelCfg = newCfg.channels["buz"] || {};
51
+ console.log("[buz setup] existing channel config:", {
52
+ hasExisting: !!newCfg.channels["buz"],
53
+ existingKeys: Object.keys(existingChannelCfg),
54
+ existingAccountsKeys: Object.keys(existingChannelCfg.accounts || {}),
55
+ });
56
+
57
+ // Ensure the buz channel config exists with proper structure
58
+ newCfg.channels["buz"] = {
59
+ ...existingChannelCfg,
60
+ enabled: true,
61
+ accounts: {
62
+ ...(existingChannelCfg.accounts || {}),
63
+ },
64
+ };
65
+
66
+ // Set the account config
67
+ newCfg.channels["buz"].accounts[resolvedAccountId] = {
68
+ ...(newCfg.channels["buz"].accounts[resolvedAccountId] || {}),
69
+ serverAddress: input.serverAddress,
70
+ secretKey: input.secretKey,
71
+ enabled: true,
72
+ };
73
+
74
+ console.log("[buz setup] account config set:", {
75
+ accountId: resolvedAccountId,
76
+ hasServerAddress: !!newCfg.channels["buz"].accounts[resolvedAccountId].serverAddress,
77
+ hasSecretKey: !!newCfg.channels["buz"].accounts[resolvedAccountId].secretKey,
78
+ allAccountIds: Object.keys(newCfg.channels["buz"].accounts),
79
+ });
80
+
81
+ // Clean up top-level fields for default account (they should be in accounts.default)
82
+ if (resolvedAccountId === "default") {
83
+ delete newCfg.channels["buz"].serverAddress;
84
+ delete newCfg.channels["buz"].secretKey;
85
+ console.log("[buz setup] cleaned up top-level fields for default account");
86
+ }
87
+
88
+ console.log("[buz setup] final config structure:", {
89
+ channelsKeys: Object.keys(newCfg.channels),
90
+ buzKeys: Object.keys(newCfg.channels["buz"]),
91
+ buzEnabled: newCfg.channels["buz"].enabled,
92
+ accountIds: Object.keys(newCfg.channels["buz"].accounts),
93
+ defaultAccountKeys: Object.keys(newCfg.channels["buz"].accounts.default || {}),
94
+ });
95
+
96
+ // IMPORTANT: Return the entire cfg object, not just the channels part
97
+ return { cfg: newCfg, accountId: resolvedAccountId };
98
+ },
99
+ };