@bajoseek/openclaw-bajoseek 0.1.0

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
+
4
+ import { bajoseekPlugin } from "./src/channel.js";
5
+ import { setBajoseekRuntime } from "./src/runtime.js";
6
+
7
+ const plugin = {
8
+ id: "bajoseek",
9
+ name: "Bajoseek",
10
+ description: "Bajoseek channel plugin",
11
+ configSchema: emptyPluginConfigSchema(),
12
+ register(api: OpenClawPluginApi) {
13
+ setBajoseekRuntime(api.runtime);
14
+ api.registerChannel({ plugin: bajoseekPlugin });
15
+ },
16
+ };
17
+
18
+ export default plugin;
19
+
20
+ export { bajoseekPlugin } from "./src/channel.js";
21
+ export { setBajoseekRuntime, getBajoseekRuntime } from "./src/runtime.js";
22
+ export * from "./src/types.js";
23
+ export * from "./src/config.js";
24
+ export * from "./src/gateway.js";
25
+ export * from "./src/outbound.js";
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "bajoseek",
3
+ "channels": ["bajoseek"],
4
+ "providerAuthEnvVars": {
5
+ "bajoseek": ["BAJOSEEK_BOT_ID", "BAJOSEEK_TOKEN"]
6
+ },
7
+ "configSchema": {
8
+ "type": "object",
9
+ "additionalProperties": false,
10
+ "properties": {}
11
+ }
12
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@bajoseek/openclaw-bajoseek",
3
+ "version": "0.1.0",
4
+ "description": "Bajoseek channel plugin for OpenClaw — connect AI assistants to Bajoseek via WebSocket",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/bajoseek/openclaw-bajoseek"
12
+ },
13
+ "keywords": [
14
+ "openclaw",
15
+ "bajoseek",
16
+ "channel-plugin",
17
+ "websocket",
18
+ "ai",
19
+ "chatbot"
20
+ ],
21
+ "files": [
22
+ "dist",
23
+ "src",
24
+ "index.ts",
25
+ "tsconfig.json",
26
+ "openclaw.plugin.json"
27
+ ],
28
+ "openclaw": {
29
+ "id": "bajoseek",
30
+ "extensions": [
31
+ "./index.ts"
32
+ ]
33
+ },
34
+ "dependencies": {
35
+ "ws": "^8.18.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^20.0.0",
39
+ "@types/ws": "^8.5.0",
40
+ "typescript": "^5.9.3"
41
+ },
42
+ "peerDependencies": {
43
+ "openclaw": "*"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "scripts": {
49
+ "build": "tsc || true",
50
+ "dev": "tsc --watch"
51
+ }
52
+ }
package/src/channel.ts ADDED
@@ -0,0 +1,248 @@
1
+ import {
2
+ type ChannelPlugin,
3
+ type OpenClawConfig,
4
+ applyAccountNameToChannelSection,
5
+ deleteAccountFromConfigSection,
6
+ setAccountEnabledInConfigSection,
7
+ } from "openclaw/plugin-sdk";
8
+
9
+ import type { ResolvedBajoseekAccount } from "./types.js";
10
+ import { DEFAULT_ACCOUNT_ID, listBajoseekAccountIds, resolveBajoseekAccount, applyBajoseekAccountConfig, resolveDefaultBajoseekAccountId } from "./config.js";
11
+ import { sendText } from "./outbound.js";
12
+ import { startGateway } from "./gateway.js";
13
+ import { bajoseekOnboardingAdapter } from "./onboarding.js";
14
+
15
+ /**
16
+ * 简单的文本分块函数
17
+ */
18
+ function chunkText(text: string, limit: number): string[] {
19
+ if (text.length <= limit) return [text];
20
+
21
+ const chunks: string[] = [];
22
+ let remaining = text;
23
+
24
+ while (remaining.length > 0) {
25
+ if (remaining.length <= limit) {
26
+ chunks.push(remaining);
27
+ break;
28
+ }
29
+
30
+ // 尝试在换行处分割
31
+ let splitAt = remaining.lastIndexOf("\n", limit);
32
+ if (splitAt <= 0 || splitAt < limit * 0.5) {
33
+ splitAt = remaining.lastIndexOf(" ", limit);
34
+ }
35
+ if (splitAt <= 0 || splitAt < limit * 0.5) {
36
+ splitAt = limit;
37
+ }
38
+
39
+ chunks.push(remaining.slice(0, splitAt));
40
+ remaining = remaining.slice(splitAt).trimStart();
41
+ }
42
+
43
+ return chunks;
44
+ }
45
+
46
+ export const bajoseekPlugin: ChannelPlugin<ResolvedBajoseekAccount> = {
47
+ id: "bajoseek",
48
+ meta: {
49
+ id: "bajoseek",
50
+ label: "Bajoseek",
51
+ selectionLabel: "Bajoseek",
52
+ docsPath: "/docs/channels/bajoseek",
53
+ blurb: "Connect to Bajoseek App via WebSocket",
54
+ order: 60,
55
+ },
56
+ capabilities: {
57
+ chatTypes: ["direct"],
58
+ media: false,
59
+ reactions: false,
60
+ threads: false,
61
+ blockStreaming: true,
62
+ },
63
+ agentPrompt: {
64
+ messageToolHints: () => [
65
+ "- Bajoseek is a chat channel. Always respond with text directly in the conversation.",
66
+ "- Do NOT write content to files or use file-writing tools to produce your response. Send all content as chat messages.",
67
+ "- If the user asks you to write or create content (stories, articles, code, etc.), output the full content directly in your reply text, do not save it to a file.",
68
+ "- Bajoseek does not support media attachments, reactions, or threads.",
69
+ ],
70
+ },
71
+ reload: { configPrefixes: ["channels.bajoseek"] },
72
+ onboarding: bajoseekOnboardingAdapter,
73
+
74
+ config: {
75
+ listAccountIds: (cfg) => listBajoseekAccountIds(cfg),
76
+ resolveAccount: (cfg, accountId) => resolveBajoseekAccount(cfg, accountId),
77
+ defaultAccountId: (cfg) => resolveDefaultBajoseekAccountId(cfg),
78
+ setAccountEnabled: ({ cfg, accountId, enabled }) =>
79
+ setAccountEnabledInConfigSection({
80
+ cfg,
81
+ sectionKey: "bajoseek",
82
+ accountId,
83
+ enabled,
84
+ allowTopLevel: true,
85
+ }),
86
+ deleteAccount: ({ cfg, accountId }) =>
87
+ deleteAccountFromConfigSection({
88
+ cfg,
89
+ sectionKey: "bajoseek",
90
+ accountId,
91
+ clearBaseFields: ["botId", "token", "tokenFile", "name"],
92
+ }),
93
+ isConfigured: (account) => Boolean(account?.botId && account?.token),
94
+ describeAccount: (account) => ({
95
+ accountId: account?.accountId ?? DEFAULT_ACCOUNT_ID,
96
+ name: account?.name,
97
+ enabled: account?.enabled ?? false,
98
+ configured: Boolean(account?.botId && account?.token),
99
+ tokenSource: account?.tokenSource,
100
+ }),
101
+ resolveAllowFrom: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId?: string | null }) => {
102
+ const account = resolveBajoseekAccount(cfg, accountId);
103
+ const allowFrom = account.config?.allowFrom ?? [];
104
+ return allowFrom.map((entry: string | number) => String(entry));
105
+ },
106
+ formatAllowFrom: ({ allowFrom }: { allowFrom: Array<string | number> }) =>
107
+ allowFrom
108
+ .map((entry: string | number) => String(entry).trim())
109
+ .filter(Boolean)
110
+ .map((entry: string) => entry.replace(/^bajoseek:/i, "")),
111
+ },
112
+ setup: {
113
+ resolveAccountId: ({ accountId }) => accountId?.trim().toLowerCase() || DEFAULT_ACCOUNT_ID,
114
+ applyAccountName: ({ cfg, accountId, name }) =>
115
+ applyAccountNameToChannelSection({
116
+ cfg,
117
+ channelKey: "bajoseek",
118
+ accountId,
119
+ name,
120
+ }),
121
+ validateInput: ({ input }) => {
122
+ if (!input.botToken && !input.useEnv) {
123
+ return "Bajoseek requires --bot-token <botId> --token <token> [--url <wsUrl>], or --use-env";
124
+ }
125
+ if (!input.token && !input.useEnv) {
126
+ return "Bajoseek requires --token <token>";
127
+ }
128
+ return null;
129
+ },
130
+ applyAccountConfig: ({ cfg, accountId, input }) => {
131
+ return applyBajoseekAccountConfig(cfg, accountId, {
132
+ botId: input.botToken,
133
+ token: input.token,
134
+ tokenFile: input.tokenFile,
135
+ name: input.name,
136
+ wsUrl: input.url,
137
+ });
138
+ },
139
+ },
140
+ messaging: {
141
+ /**
142
+ * 规范化目标地址
143
+ * 支持格式:
144
+ * - bajoseek:user:userId → 私聊
145
+ * - user:userId → 私聊
146
+ * - 纯 userId
147
+ */
148
+ normalizeTarget: (target: string) => {
149
+ const id = target.replace(/^bajoseek:/i, "");
150
+
151
+ if (id.startsWith("user:")) {
152
+ return { ok: true, to: `bajoseek:${id}` };
153
+ }
154
+
155
+ // 纯 userId(非空字符串)
156
+ if (id && !id.includes(":")) {
157
+ return { ok: true, to: `bajoseek:user:${id}` };
158
+ }
159
+
160
+ return { ok: false, error: "无法识别的目标格式" };
161
+ },
162
+ targetResolver: {
163
+ looksLikeId: (id: string): boolean => {
164
+ if (/^bajoseek:user:/i.test(id)) return true;
165
+ if (/^user:/i.test(id)) return true;
166
+ // 简单的 userId 判断
167
+ return /^[a-zA-Z0-9_-]{1,64}$/.test(id);
168
+ },
169
+ hint: "Bajoseek 目标格式: bajoseek:user:userId",
170
+ },
171
+ },
172
+ outbound: {
173
+ deliveryMode: "direct",
174
+ chunker: chunkText,
175
+ chunkerMode: "markdown",
176
+ textChunkLimit: 4000,
177
+ sendText: async ({ to, text, accountId, replyToId }) => {
178
+ const result = await sendText({ to, text, accountId, replyToId });
179
+ return {
180
+ channel: "bajoseek" as const,
181
+ messageId: result.messageId ?? "",
182
+ error: result.error ? new Error(result.error) : undefined,
183
+ };
184
+ },
185
+ },
186
+ gateway: {
187
+ startAccount: async (ctx) => {
188
+ const { account, abortSignal, log, cfg } = ctx;
189
+
190
+ log?.info(`[bajoseek:${account.accountId}] Starting gateway — botId=${account.botId}, enabled=${account.enabled}, name=${account.name ?? "unnamed"}`);
191
+
192
+ await startGateway({
193
+ account,
194
+ abortSignal,
195
+ cfg,
196
+ log,
197
+ onReady: () => {
198
+ log?.info(`[bajoseek:${account.accountId}] Gateway ready`);
199
+ ctx.setStatus({
200
+ ...ctx.getStatus(),
201
+ running: true,
202
+ connected: true,
203
+ lastConnectedAt: Date.now(),
204
+ });
205
+ },
206
+ onError: (error) => {
207
+ log?.error(`[bajoseek:${account.accountId}] Gateway error: ${error.message}`);
208
+ ctx.setStatus({
209
+ ...ctx.getStatus(),
210
+ lastError: error.message,
211
+ });
212
+ },
213
+ });
214
+ },
215
+ },
216
+ status: {
217
+ defaultRuntime: {
218
+ accountId: DEFAULT_ACCOUNT_ID,
219
+ running: false,
220
+ connected: false,
221
+ lastConnectedAt: null,
222
+ lastError: null,
223
+ lastInboundAt: null,
224
+ lastOutboundAt: null,
225
+ },
226
+ buildChannelSummary: ({ snapshot }: { snapshot: Record<string, unknown> }) => ({
227
+ configured: snapshot.configured ?? false,
228
+ tokenSource: snapshot.tokenSource ?? "none",
229
+ running: snapshot.running ?? false,
230
+ connected: snapshot.connected ?? false,
231
+ lastConnectedAt: snapshot.lastConnectedAt ?? null,
232
+ lastError: snapshot.lastError ?? null,
233
+ }),
234
+ buildAccountSnapshot: ({ account, runtime }: { account: ResolvedBajoseekAccount; cfg: OpenClawConfig; runtime?: Record<string, unknown> }) => ({
235
+ accountId: account.accountId,
236
+ name: account.name,
237
+ enabled: account.enabled,
238
+ configured: Boolean(account.botId && account.token),
239
+ tokenSource: account.tokenSource,
240
+ running: (runtime?.running as boolean) ?? false,
241
+ connected: (runtime?.connected as boolean) ?? false,
242
+ lastConnectedAt: (runtime?.lastConnectedAt as number | null) ?? null,
243
+ lastError: (runtime?.lastError as string | null) ?? null,
244
+ lastInboundAt: (runtime?.lastInboundAt as number | null) ?? null,
245
+ lastOutboundAt: (runtime?.lastOutboundAt as number | null) ?? null,
246
+ }),
247
+ },
248
+ };
package/src/config.ts ADDED
@@ -0,0 +1,183 @@
1
+ import type { ResolvedBajoseekAccount, BajoseekAccountConfig } from "./types.js";
2
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
3
+ import * as fs from "node:fs";
4
+
5
+ export const DEFAULT_ACCOUNT_ID = "default";
6
+ const DEFAULT_WS_URL = "wss://ws.bajoseek.com";
7
+
8
+ interface BajoseekChannelConfig extends BajoseekAccountConfig {
9
+ accounts?: Record<string, BajoseekAccountConfig>;
10
+ }
11
+
12
+ /**
13
+ * 列出所有 Bajoseek 账户 ID
14
+ */
15
+ export function listBajoseekAccountIds(cfg: OpenClawConfig): string[] {
16
+ const ids = new Set<string>();
17
+ const bajoseek = cfg.channels?.bajoseek as BajoseekChannelConfig | undefined;
18
+
19
+ if (bajoseek?.botId) {
20
+ ids.add(DEFAULT_ACCOUNT_ID);
21
+ }
22
+
23
+ if (bajoseek?.accounts) {
24
+ for (const accountId of Object.keys(bajoseek.accounts)) {
25
+ if (bajoseek.accounts[accountId]?.botId) {
26
+ ids.add(accountId);
27
+ }
28
+ }
29
+ }
30
+
31
+ return Array.from(ids);
32
+ }
33
+
34
+ /**
35
+ * 获取默认账户 ID
36
+ */
37
+ export function resolveDefaultBajoseekAccountId(cfg: OpenClawConfig): string {
38
+ const bajoseek = cfg.channels?.bajoseek as BajoseekChannelConfig | undefined;
39
+ if (bajoseek?.botId) {
40
+ return DEFAULT_ACCOUNT_ID;
41
+ }
42
+ if (bajoseek?.accounts) {
43
+ const ids = Object.keys(bajoseek.accounts);
44
+ if (ids.length > 0) {
45
+ return ids[0];
46
+ }
47
+ }
48
+ return DEFAULT_ACCOUNT_ID;
49
+ }
50
+
51
+ /**
52
+ * 解析 Bajoseek 账户配置
53
+ * 支持三级回退:config → file → env
54
+ */
55
+ export function resolveBajoseekAccount(
56
+ cfg: OpenClawConfig,
57
+ accountId?: string | null
58
+ ): ResolvedBajoseekAccount {
59
+ const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
60
+ const bajoseek = cfg.channels?.bajoseek as BajoseekChannelConfig | undefined;
61
+
62
+ let accountConfig: BajoseekAccountConfig = {};
63
+ let botId = "";
64
+ let token = "";
65
+ let tokenSource: "config" | "file" | "env" | "none" = "none";
66
+
67
+ if (resolvedAccountId === DEFAULT_ACCOUNT_ID) {
68
+ accountConfig = {
69
+ enabled: bajoseek?.enabled,
70
+ name: bajoseek?.name,
71
+ botId: bajoseek?.botId,
72
+ token: bajoseek?.token,
73
+ tokenFile: bajoseek?.tokenFile,
74
+ wsUrl: bajoseek?.wsUrl,
75
+ allowFrom: bajoseek?.allowFrom,
76
+ blockStreaming: (bajoseek as Record<string, unknown>)?.blockStreaming as boolean | undefined,
77
+ };
78
+ botId = (bajoseek?.botId ?? "").trim();
79
+ } else {
80
+ const account = bajoseek?.accounts?.[resolvedAccountId];
81
+ accountConfig = account ?? {};
82
+ botId = (account?.botId ?? "").trim();
83
+ }
84
+
85
+ // 解析 token:config → file → env
86
+ if (accountConfig.token) {
87
+ token = accountConfig.token;
88
+ tokenSource = "config";
89
+ } else if (accountConfig.tokenFile) {
90
+ try {
91
+ token = fs.readFileSync(accountConfig.tokenFile, "utf-8").trim();
92
+ tokenSource = "file";
93
+ } catch {
94
+ // file read failed, fall through
95
+ }
96
+ } else if (process.env.BAJOSEEK_TOKEN && resolvedAccountId === DEFAULT_ACCOUNT_ID) {
97
+ token = process.env.BAJOSEEK_TOKEN;
98
+ tokenSource = "env";
99
+ }
100
+
101
+ // botId 也可以从环境变量读取
102
+ if (!botId && process.env.BAJOSEEK_BOT_ID && resolvedAccountId === DEFAULT_ACCOUNT_ID) {
103
+ botId = process.env.BAJOSEEK_BOT_ID.trim();
104
+ }
105
+
106
+ // wsUrl:account config → channel config → default
107
+ const wsUrl = accountConfig.wsUrl || bajoseek?.wsUrl || DEFAULT_WS_URL;
108
+
109
+ return {
110
+ accountId: resolvedAccountId,
111
+ name: accountConfig.name,
112
+ enabled: accountConfig.enabled !== false,
113
+ botId,
114
+ token,
115
+ tokenSource,
116
+ wsUrl,
117
+ config: accountConfig,
118
+ };
119
+ }
120
+
121
+ /**
122
+ * 应用账户配置
123
+ */
124
+ export function applyBajoseekAccountConfig(
125
+ cfg: OpenClawConfig,
126
+ accountId: string,
127
+ input: { botId?: string; token?: string; tokenFile?: string; name?: string; wsUrl?: string }
128
+ ): OpenClawConfig {
129
+ const next = { ...cfg };
130
+
131
+ if (accountId === DEFAULT_ACCOUNT_ID) {
132
+ const existingConfig = (next.channels?.bajoseek as BajoseekChannelConfig) || {};
133
+ const allowFrom = existingConfig.allowFrom ?? ["*"];
134
+
135
+ next.channels = {
136
+ ...next.channels,
137
+ bajoseek: {
138
+ ...(next.channels?.bajoseek as Record<string, unknown> || {}),
139
+ enabled: true,
140
+ blockStreaming: true,
141
+ allowFrom,
142
+ ...(input.botId ? { botId: input.botId } : {}),
143
+ ...(input.token
144
+ ? { token: input.token }
145
+ : input.tokenFile
146
+ ? { tokenFile: input.tokenFile }
147
+ : {}),
148
+ ...(input.name ? { name: input.name } : {}),
149
+ ...(input.wsUrl ? { wsUrl: input.wsUrl } : {}),
150
+ },
151
+ };
152
+ } else {
153
+ const existingAccountConfig = (next.channels?.bajoseek as BajoseekChannelConfig)?.accounts?.[accountId] || {};
154
+ const allowFrom = existingAccountConfig.allowFrom ?? ["*"];
155
+
156
+ next.channels = {
157
+ ...next.channels,
158
+ bajoseek: {
159
+ ...(next.channels?.bajoseek as Record<string, unknown> || {}),
160
+ enabled: true,
161
+ accounts: {
162
+ ...((next.channels?.bajoseek as BajoseekChannelConfig)?.accounts || {}),
163
+ [accountId]: {
164
+ ...((next.channels?.bajoseek as BajoseekChannelConfig)?.accounts?.[accountId] || {}),
165
+ enabled: true,
166
+ blockStreaming: true,
167
+ allowFrom,
168
+ ...(input.botId ? { botId: input.botId } : {}),
169
+ ...(input.token
170
+ ? { token: input.token }
171
+ : input.tokenFile
172
+ ? { tokenFile: input.tokenFile }
173
+ : {}),
174
+ ...(input.name ? { name: input.name } : {}),
175
+ ...(input.wsUrl ? { wsUrl: input.wsUrl } : {}),
176
+ },
177
+ },
178
+ },
179
+ };
180
+ }
181
+
182
+ return next;
183
+ }