@hedgehog2026/ciwei-ai 1.0.2

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/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
3
+ import { ciweiAIPlugin } from "./src/channel";
4
+ import { setCiweiAIRuntime } from "./src/runtime";
5
+
6
+ /**
7
+ * ciweiAI
8
+ */
9
+ const plugin: any = {
10
+ id: "ciweiAI",
11
+ name: "ciwei AI Channel",
12
+ description: "Custom WebSocket-based channel for Ciwei AI",
13
+ configSchema: emptyPluginConfigSchema(),
14
+
15
+ register(api: OpenClawPluginApi): void {
16
+ console.log("[ciweiAI] Registering plugin...");
17
+ setCiweiAIRuntime(api.runtime);
18
+
19
+ api.registerChannel({ plugin: ciweiAIPlugin });
20
+
21
+ // api.registerGatewayMethod("ciweiai.some.method", async (...) => { ... });
22
+ },
23
+ };
24
+
25
+ export default plugin;
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "ciweiAI",
3
+ "name": "ciweiAI",
4
+ "version": "1.0.0",
5
+ "description": "Custom WebSocket-based channel plugin for Ciwei AI app",
6
+ "entry": "./index.ts",
7
+ "type": "channel",
8
+ "channels": [
9
+ "ciweiAI"
10
+ ],
11
+ "configSchema": {
12
+ "type": "object",
13
+ "additionalProperties": false,
14
+ "properties": {}
15
+ }
16
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@hedgehog2026/ciwei-ai",
3
+ "version": "1.0.2",
4
+ "description": "ciwei AI WebSocket channel for OpenClaw",
5
+ "keywords": [
6
+ "bot",
7
+ "channel",
8
+ "clawdbot",
9
+ "openclaw",
10
+ "stream",
11
+ "ciweiAI"
12
+ ],
13
+ "repository": "github:hedgehog2026/ciwei-ai",
14
+ "main": "index.ts",
15
+ "type": "module",
16
+ "author": "OpenClaw Community",
17
+ "license": "MIT",
18
+ "dependencies": {
19
+ "ws": "^8.16.0",
20
+ "zod": "^3.23.0"
21
+ },
22
+ "peerDependencies": {
23
+ "openclaw": ">=2026.2.13"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^20.11.0",
27
+ "@types/ws": "^8.5.10",
28
+ "typescript": "^5.3.3"
29
+ },
30
+ "openclaw": {
31
+ "extensions": [
32
+ "./index.ts"
33
+ ],
34
+ "channels": [
35
+ "ciweiAI"
36
+ ],
37
+ "installDependencies": true,
38
+ "channel": {
39
+ "id": "ciweiAI",
40
+ "label": "ciwei AI",
41
+ "selectionLabel": "ciwei AI",
42
+ "docsPath": "/channels/ciweiai",
43
+ "blurb": "ciwei AI Custom Relay Channel.",
44
+ "order": 100
45
+ }
46
+ }
47
+ }
package/src/channel.ts ADDED
@@ -0,0 +1,389 @@
1
+ import { WebSocket } from "ws";
2
+ import { z } from "zod";
3
+ import { buildChannelConfigSchema } from "openclaw/plugin-sdk";
4
+ import {
5
+ ChannelPlugin,
6
+ OpenClawConfig,
7
+ ChannelGatewayContext,
8
+ ChannelAccountSnapshot,
9
+ ChannelStatusIssue
10
+ } from "openclaw/plugin-sdk";
11
+ import { getCiweiAIRuntime } from "./runtime";
12
+ import type {
13
+ CiweiAIResolvedAccount,
14
+ RelayInboundMessage,
15
+ RelayReplyMessage
16
+ } from "./types";
17
+
18
+
19
+ /**
20
+ * Ciwei AI Schema
21
+ * accountId , token
22
+ */
23
+ const CiweiAIConfigSchema = z.object({
24
+ token: z.string().describe("Relay server access token").optional(),
25
+ accountId: z.string().describe("Relay server account ID").optional(),
26
+ });
27
+
28
+ /**
29
+ * Unix
30
+ */
31
+ function getCurrentTimestamp(): number {
32
+ return Date.now();
33
+ }
34
+
35
+ /**
36
+ * Ciwei AI Channel Plugin
37
+ * accountId + token
38
+ *
39
+ * "ciweiAI": {
40
+ * "enabled": true,
41
+ * "accountId": "13333333333",
42
+ * "token": "your-token"
43
+ * }
44
+ */
45
+ export const ciweiAIPlugin: ChannelPlugin<CiweiAIResolvedAccount> = {
46
+ id: "ciweiAI",
47
+
48
+ meta: {
49
+ id: "ciweiAI",
50
+ label: "Ciwei AI",
51
+ selectionLabel: "Ciwei AI",
52
+ blurb: "Custom WebSocket relay channel for Ciwei AI",
53
+ docsPath: "/channels/ciweiai",
54
+ order: 100,
55
+ },
56
+
57
+ configSchema: buildChannelConfigSchema(CiweiAIConfigSchema),
58
+
59
+ capabilities: {
60
+ chatTypes: ["direct"],
61
+ media: false,
62
+ reactions: false,
63
+ threads: false,
64
+ blockStreaming: true,
65
+ },
66
+
67
+ config: {
68
+ listAccountIds: (cfg: OpenClawConfig): string[] => {
69
+ const channelConfig = cfg.channels?.ciweiAI;
70
+ if (!channelConfig) return [];
71
+
72
+ if (channelConfig.accountId) {
73
+ return [channelConfig.accountId];
74
+ }
75
+
76
+ if (channelConfig.accounts) {
77
+ if (Array.isArray(channelConfig.accounts)) {
78
+ return channelConfig.accounts.map((a: any) => a.accountId);
79
+ }
80
+ return Object.keys(channelConfig.accounts);
81
+ }
82
+
83
+ return ["default"];
84
+ },
85
+
86
+ resolveAccount: (cfg: OpenClawConfig, accountId?: string | null): CiweiAIResolvedAccount => {
87
+ const channelConfig = cfg.channels?.ciweiAI;
88
+
89
+ if (channelConfig?.accountId) {
90
+ const id = channelConfig.accountId;
91
+ const token = channelConfig.token;
92
+ return {
93
+ accountId: id,
94
+ config: { token },
95
+ enabled: true,
96
+ configured: Boolean(token),
97
+ };
98
+ }
99
+
100
+ const id = accountId || "default";
101
+ let accountInfo: any;
102
+
103
+ if (Array.isArray(channelConfig?.accounts)) {
104
+ accountInfo = channelConfig.accounts.find((a: any) => a.accountId === id);
105
+ } else {
106
+ accountInfo = channelConfig?.accounts?.[id];
107
+ }
108
+
109
+ let finalConfig: any = {};
110
+ if (typeof accountInfo === "string") {
111
+ finalConfig = { token: accountInfo };
112
+ } else if (accountInfo && typeof accountInfo === "object") {
113
+ finalConfig = accountInfo.config || accountInfo;
114
+ }
115
+
116
+ return {
117
+ accountId: id,
118
+ config: finalConfig,
119
+ enabled: accountInfo?.enabled !== false,
120
+ configured: Boolean(finalConfig.token),
121
+ };
122
+ },
123
+
124
+ defaultAccountId: (cfg: OpenClawConfig): string => {
125
+ const channelConfig = (cfg as any)?.channels?.ciweiAI;
126
+ return channelConfig?.accountId || "default";
127
+ },
128
+ },
129
+
130
+ gateway: {
131
+ startAccount: async (ctx: ChannelGatewayContext<CiweiAIResolvedAccount>) => {
132
+ const { account, cfg, log, abortSignal } = ctx;
133
+ const rt = getCiweiAIRuntime();
134
+ const accountId = String(account.accountId);
135
+ const token = account.config.token || "";
136
+ const relayUrl = `ws://47.110.64.126/relay?id=${accountId}&token=${token}&role=provider`;
137
+
138
+ let ws: WebSocket | null = null;
139
+ let isClosing = false;
140
+ let heartbeatInterval: NodeJS.Timeout | null = null;
141
+
142
+ // 用于阻塞 startAccount 的生命周期
143
+ let resolveStop: (value: void | PromiseLike<void>) => void;
144
+ const stopPromise = new Promise<void>((resolve) => {
145
+ resolveStop = resolve;
146
+ });
147
+
148
+ const stopClient = () => {
149
+ if (isClosing) return;
150
+ isClosing = true;
151
+
152
+ if (heartbeatInterval) clearInterval(heartbeatInterval);
153
+ log?.info?.(`[ciweiAI][${accountId}] Stopping gateway...`);
154
+
155
+ try {
156
+ ws?.close();
157
+ } catch (err: any) {
158
+ log?.warn?.(`[ciweiAI][${accountId}] Error during close: ${err.message}`);
159
+ }
160
+
161
+ ctx.setStatus({
162
+ ...ctx.getStatus(),
163
+ running: false,
164
+ lastStopAt: getCurrentTimestamp(),
165
+ });
166
+
167
+ resolveStop();
168
+ };
169
+
170
+ const connect = () => {
171
+ if (isClosing) return;
172
+
173
+ log?.info?.(`[ciweiAI][${accountId}] Connecting to relay: ${relayUrl}`);
174
+ ws = new WebSocket(relayUrl);
175
+
176
+ ws.on("open", () => {
177
+ log?.info?.(`[ciweiAI][${accountId}] Connected successfully.`);
178
+ ctx.setStatus({
179
+ ...ctx.getStatus(),
180
+ running: true,
181
+ lastStartAt: getCurrentTimestamp(),
182
+ lastEventAt: getCurrentTimestamp(),
183
+ lastError: null,
184
+ });
185
+
186
+ if (heartbeatInterval) clearInterval(heartbeatInterval);
187
+ heartbeatInterval = setInterval(() => {
188
+ if (ws?.readyState === WebSocket.OPEN) {
189
+ ws.ping();
190
+ }
191
+ }, 30000);
192
+ });
193
+
194
+ const sentLengthMap: Record<string, number> = {};
195
+
196
+ ws.on("message", async (data) => {
197
+ ctx.setStatus({
198
+ ...ctx.getStatus(),
199
+ lastEventAt: getCurrentTimestamp(),
200
+ });
201
+
202
+ try {
203
+ const appPayload: RelayInboundMessage = JSON.parse(data.toString());
204
+ const { from, text, chatId, id } = appPayload;
205
+
206
+ if (!text) return;
207
+
208
+ const context = rt.channel.reply.finalizeInboundContext({
209
+ Body: text,
210
+ From: from,
211
+ To: chatId,
212
+ SessionKey: `ws:${chatId}`,
213
+ AccountId: String(accountId),
214
+ Provider: "ciweiAI",
215
+ MessageSid: id,
216
+ });
217
+
218
+ // Force streaming response as per OpenClaw docs
219
+ const streamingCfg = JSON.parse(JSON.stringify(cfg));
220
+
221
+ // 1. Enable block streaming for this specific channel
222
+ streamingCfg.channels = streamingCfg.channels || {};
223
+ streamingCfg.channels.ciweiAI = streamingCfg.channels.ciweiAI || {};
224
+ streamingCfg.channels.ciweiAI.blockStreaming = true;
225
+ // Also enable preview streaming as fallback
226
+ streamingCfg.channels.ciweiAI.streaming = "block";
227
+
228
+ // 2. Configure agents to emit chunks as they arrive instead of bundling
229
+ streamingCfg.agents = streamingCfg.agents || {};
230
+ streamingCfg.agents.defaults = streamingCfg.agents.defaults || {};
231
+ streamingCfg.agents.defaults.blockStreamingDefault = "on";
232
+ streamingCfg.agents.defaults.blockStreamingBreak = "text_end";
233
+
234
+ await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
235
+ ctx: context,
236
+ cfg,
237
+ dispatcherOptions: {
238
+ deliver: async (payload: any) => {
239
+ if (ws?.readyState === WebSocket.OPEN) {
240
+ ws.send(JSON.stringify({
241
+ type: "reply",
242
+ to: from,
243
+ chatId: chatId,
244
+ replyTo: id,
245
+ isFinal: true
246
+ }));
247
+ }
248
+ delete sentLengthMap[chatId];
249
+ }
250
+ },
251
+ replyOptions: {
252
+ onPartialReply: (payload: any) => {
253
+ if (payload.text && ws?.readyState === WebSocket.OPEN) {
254
+ const prev = sentLengthMap[chatId] || 0;
255
+ const delta = payload.text.slice(prev);
256
+ sentLengthMap[chatId] = payload.text.length;
257
+ if (delta) {
258
+ ws.send(JSON.stringify({
259
+ type: "reply",
260
+ to: from,
261
+ chatId: chatId,
262
+ text: delta,
263
+ replyTo: id,
264
+ isPartial: true
265
+ }));
266
+ }
267
+ }
268
+ },
269
+ onReasoningStream: (payload: any) => {
270
+ if (payload.text && ws?.readyState === WebSocket.OPEN) {
271
+ const prev = sentLengthMap[chatId] || 0;
272
+ const delta = payload.text.slice(prev);
273
+ sentLengthMap[chatId] = payload.text.length;
274
+ if (delta) {
275
+ ws.send(JSON.stringify({
276
+ type: "reply",
277
+ to: from,
278
+ chatId: chatId,
279
+ text: delta,
280
+ replyTo: id,
281
+ isPartial: true
282
+ }));
283
+ }
284
+ }
285
+ }
286
+ }
287
+ });
288
+ } catch (err: any) {
289
+ log?.error?.(`[ciweiAI][${accountId}] Dispatch error: ${err.message}`);
290
+ }
291
+ });
292
+
293
+ ws.on("error", (err) => {
294
+ log?.error?.(`[ciweiAI][${accountId}] WebSocket error: ${err.message}`);
295
+ ctx.setStatus({
296
+ ...ctx.getStatus(),
297
+ lastError: `Connection error: ${err.message}`,
298
+ });
299
+ });
300
+
301
+ ws.on("close", (code, reason) => {
302
+ if (heartbeatInterval) clearInterval(heartbeatInterval);
303
+ if (!isClosing) {
304
+ log?.warn?.(`[ciweiAI][${accountId}] Connection dropped (code=${code}). Retrying in 5s...`);
305
+ ctx.setStatus({
306
+ ...ctx.getStatus(),
307
+ running: false,
308
+ });
309
+ setTimeout(connect, 5000);
310
+ }
311
+ });
312
+ };
313
+
314
+ connect();
315
+
316
+ abortSignal?.addEventListener("abort", () => {
317
+ log?.info?.(`[ciweiAI][${accountId}] Abort signal received`);
318
+ stopClient();
319
+ });
320
+
321
+ await stopPromise;
322
+
323
+ return {
324
+ stop: () => {
325
+ stopClient();
326
+ },
327
+ };
328
+ }
329
+ },
330
+
331
+ status: {
332
+ defaultRuntime: {
333
+ accountId: "default",
334
+ running: false,
335
+ lastEventAt: null,
336
+ lastStartAt: null,
337
+ lastStopAt: null,
338
+ lastError: null,
339
+ },
340
+ collectStatusIssues: (accounts: ChannelAccountSnapshot[]): ChannelStatusIssue[] => {
341
+ return accounts.flatMap((account) => {
342
+ if (!account.configured) {
343
+ return [
344
+ {
345
+ channel: "ciweiAI",
346
+ accountId: account.accountId,
347
+ kind: "config" as const,
348
+ message: "Account not configured (missing relay token)",
349
+ },
350
+ ];
351
+ }
352
+ return [];
353
+ });
354
+ },
355
+ buildChannelSummary: ({ snapshot }: { snapshot: ChannelAccountSnapshot }) => ({
356
+ configured: snapshot?.configured ?? false,
357
+ running: snapshot?.running ?? false,
358
+ lastStartAt: snapshot?.lastStartAt ?? null,
359
+ lastStopAt: snapshot?.lastStopAt ?? null,
360
+ lastError: snapshot?.lastError ?? null,
361
+ }),
362
+ probeAccount: async ({ account }: { account: CiweiAIResolvedAccount }) => {
363
+ if (!account.configured || !account.config?.token) {
364
+ return { ok: false, error: "Token not configured" };
365
+ }
366
+ return { ok: true, details: { relay: "47.110.64.126" } };
367
+ },
368
+ buildAccountSnapshot: ({ account, runtime, snapshot, probe }: {
369
+ account: CiweiAIResolvedAccount,
370
+ runtime?: ChannelAccountSnapshot,
371
+ snapshot?: ChannelAccountSnapshot,
372
+ probe?: any
373
+ }): ChannelAccountSnapshot => {
374
+ const running = runtime?.running ?? snapshot?.running ?? false;
375
+ return {
376
+ ...snapshot,
377
+ accountId: account.accountId,
378
+ enabled: account.enabled,
379
+ configured: account.configured,
380
+ running,
381
+ lastEventAt: runtime?.lastEventAt ?? snapshot?.lastEventAt ?? null,
382
+ lastStartAt: runtime?.lastStartAt ?? snapshot?.lastStartAt ?? null,
383
+ lastStopAt: runtime?.lastStopAt ?? snapshot?.lastStopAt ?? null,
384
+ lastError: runtime?.lastError ?? snapshot?.lastError ?? null,
385
+ probe,
386
+ };
387
+ },
388
+ },
389
+ };
package/src/runtime.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { PluginRuntime } from "openclaw/plugin-sdk";
2
+
3
+ let runtime: PluginRuntime | null = null;
4
+
5
+ export function setCiweiAIRuntime(next: PluginRuntime): void {
6
+ runtime = next;
7
+ }
8
+
9
+ export function getCiweiAIRuntime(): PluginRuntime {
10
+ if (!runtime) {
11
+ throw new Error("CiweiAI runtime not initialized");
12
+ }
13
+ return runtime;
14
+ }
package/src/types.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Ciwei AI
3
+ */
4
+ export interface CiweiAIResolvedAccount {
5
+ accountId: string;
6
+ config: {
7
+ token?: string;
8
+ };
9
+ enabled: boolean;
10
+ configured: boolean;
11
+ }
12
+
13
+ /**
14
+ * Inbound
15
+ */
16
+ export interface RelayInboundMessage {
17
+ type?: string;
18
+ from: string;
19
+ text: string;
20
+ chatId: string;
21
+ id: string;
22
+ }
23
+
24
+ /**
25
+ * Outbound
26
+ */
27
+ export interface RelayReplyMessage {
28
+ type: "reply";
29
+ to: string;
30
+ chatId: string;
31
+ text: string;
32
+ replyTo: string;
33
+ isPartial?: boolean;
34
+ isFinal?: boolean;
35
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "baseUrl": ".",
10
+ "types": ["node"],
11
+ "paths": {
12
+ "openclaw/*": ["node_modules/openclaw/*"]
13
+ }
14
+ },
15
+ "include": ["index.ts", "src/**/*.ts"]
16
+ }