@sumeai/sumeclaw 1.0.28 → 1.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,78 @@
1
+ import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
2
+ import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
3
+
4
+ import {
5
+ handleSumeclawInbound,
6
+ isDuplicateMessage,
7
+ type SumeclawInboundContext,
8
+ } from "./src/inbound-handler.js";
9
+ import { resolveAccount, type ResolvedAccount } from "./src/types.js";
10
+ import { sumeclawPlugin } from "./src/plugin.js";
11
+ import { SumeclawWebSocketClient } from "./src/websocket-client.js";
12
+
13
+ // 扩展插件以添加 runtime 钩子
14
+ sumeclawPlugin.runtime = {
15
+ async onActivate(context) {
16
+ const logger = context.logger;
17
+ logger.info("[sumeclaw] Plugin activated", { channel: "sumeclaw" });
18
+
19
+ const account = resolveAccount(context.cfg);
20
+ const wsClient = new SumeclawWebSocketClient(account, context.runtime, logger);
21
+
22
+ wsClient.connect(async (rawMessage) => {
23
+ if (isDuplicateMessage(rawMessage.id)) {
24
+ logger.verbose("[sumeclaw] Duplicate message dropped", {
25
+ channel: "sumeclaw",
26
+ messageId: rawMessage.id,
27
+ });
28
+ return;
29
+ }
30
+
31
+ const inboundContext: SumeclawInboundContext = {
32
+ cfg: context.cfg,
33
+ runtime: context.runtime,
34
+ logger,
35
+ accountId: account.accountId,
36
+ message: rawMessage,
37
+ };
38
+
39
+ await handleSumeclawInbound(inboundContext);
40
+ });
41
+
42
+ (context.runtime as any).sumeclawWsClient = wsClient;
43
+ },
44
+ async onDeactivate(context) {
45
+ const logger = context.logger;
46
+ logger.info("[sumeclaw] Plugin deactivated", { channel: "sumeclaw" });
47
+
48
+ const wsClient = (context.runtime as any).sumeclawWsClient;
49
+ if (wsClient) {
50
+ wsClient.disconnect();
51
+ }
52
+ },
53
+ };
54
+
55
+ export default defineChannelPluginEntry({
56
+ id: "sumeclaw",
57
+ name: "友虾名片",
58
+ description: "将 OpenClaw 连接到友虾名片平台,通过 AI 名片为客户提供服务",
59
+ plugin: sumeclawPlugin,
60
+ registerCliMetadata(api) {
61
+ api.registerCli(
62
+ ({ program }) => {
63
+ program
64
+ .command("sumeclaw")
65
+ .description("友虾名片管理");
66
+ },
67
+ {
68
+ descriptors: [
69
+ {
70
+ name: "sumeclaw",
71
+ description: "友虾名片管理",
72
+ hasSubcommands: false,
73
+ },
74
+ ],
75
+ },
76
+ );
77
+ },
78
+ });
@@ -1,28 +1,11 @@
1
- {
2
- "$schema": "https://docs.openclaw.ai/schemas/openclaw.plugin.schema.json",
3
- "id": "sumeclaw",
4
- "kind": "channel",
5
- "channels": ["sumeclaw"],
6
- "label": "友虾名片",
7
- "blurb": "将 OpenClaw 连接到友虾名片平台,通过 AI 名片为客户提供服务。",
8
- "configSchema": {
9
- "type": "object",
10
- "additionalProperties": false,
11
- "properties": {
12
- "registrationToken": {
13
- "type": "string",
14
- "description": "从友虾小程序获取的注册令牌"
15
- },
16
- "platformUrl": {
17
- "type": "string",
18
- "description": "友虾名片平台 WebSocket 地址,默认为 wss://api.gixin.cc"
19
- },
20
- "enabled": {
21
- "type": "boolean",
22
- "description": "是否启用此通道",
23
- "default": true
24
- }
25
- },
26
- "required": []
27
- }
28
- }
1
+ {
2
+ "id": "sumeclaw",
3
+ "name": "sumeclaw",
4
+ "description": "将 OpenClaw 连接到友虾名片平台,通过 AI 名片为客户提供服务。",
5
+ "contracts": {
6
+ "channels": ["sumeclaw"]
7
+ },
8
+ "activation": {
9
+ "onStartup": true
10
+ }
11
+ }
package/package.json CHANGED
@@ -1,43 +1,22 @@
1
- {
2
- "name": "@sumeai/sumeclaw",
3
- "version": "1.0.28",
4
- "description": "友虾名片 OpenClaw Channel 插件 — 将 OpenClaw 连接到友虾名片平台,通过 AI 名片为客户提供服务",
5
- "type": "commonjs",
6
- "main": "dist/index.js",
7
- "scripts": {
8
- "build": "tsc",
9
- "prepublishOnly": "npm run build"
10
- },
11
- "openclaw": {
12
- "extensions": [
13
- "dist/index.js"
14
- ],
15
- "channel": {
16
- "id": "sumeclaw",
17
- "label": "友虾名片",
18
- "blurb": "将 OpenClaw 连接到友虾名片平台,通过 AI 名片为客户提供服务。"
19
- }
20
- },
21
- "dependencies": {
22
- "ws": "^8.18.0"
23
- },
24
- "devDependencies": {
25
- "typescript": "^5.7.0",
26
- "@types/ws": "^8.5.0"
27
- },
28
- "engines": {
29
- "openclaw": ">=2026.3.22"
30
- },
31
- "keywords": [
32
- "openclaw",
33
- "channel",
34
- "minicard",
35
- "友虾名片",
36
- "ai-card"
37
- ],
38
- "author": "sumeai",
39
- "license": "MIT",
40
- "publishConfig": {
41
- "access": "public"
42
- }
43
- }
1
+ {
2
+ "name": "@sumeai/sumeclaw",
3
+ "version": "1.1.0",
4
+ "description": "友虾名片 OpenClaw Channel 插件 — 将 OpenClaw 连接到友虾名片平台,通过 AI 名片为客户提供服务",
5
+ "type": "module",
6
+ "openclaw": {
7
+ "extensions": ["./index.ts"],
8
+ "setupEntry": "./setup-entry.ts",
9
+ "channel": {
10
+ "id": "sumeclaw",
11
+ "label": "友虾名片",
12
+ "blurb": "将 OpenClaw 连接到友虾名片平台,通过 AI 名片为客户提供服务"
13
+ },
14
+ "compat": {
15
+ "pluginApi": ">=2026.3.24-beta.2",
16
+ "minGatewayVersion": ">=2026.3.24-beta.2"
17
+ },
18
+ "install": {
19
+ "npmSpec": "@sumeai/sumeclaw"
20
+ }
21
+ }
22
+ }
package/readme.md ADDED
@@ -0,0 +1,133 @@
1
+
2
+
3
+ # 文件结构
4
+ @sumeai/sumeclaw/
5
+ ├── index.ts # 完整插件入口点(包含 runtime 钩子)
6
+ ├── setup-entry.ts # 轻量级设置入口
7
+ ├── src/
8
+ │ ├── types.ts # 类型定义和 resolveAccount
9
+ │ ├── plugin.ts # 插件配置(createChatChannelPlugin)
10
+ │ ├── outbound-adapter.ts # Outbound 适配器
11
+ │ ├── websocket-client.ts # WebSocket 客户端类
12
+ │ └── inbound-handler.ts # Inbound 消息处理
13
+ ├── package.json
14
+ ├── readme.md
15
+ └── openclaw.plugin.json
16
+
17
+
18
+
19
+
20
+
21
+ # 文件说明
22
+
23
+
24
+
25
+ ## 《index.ts》
26
+
27
+
28
+
29
+ - `index.ts` 现在只负责插件注册和生命周期管理,符合 OpenClaw 插件的最佳实践
30
+
31
+
32
+
33
+ ## 《setup-entry.ts》
34
+
35
+ ### Setup Entry 的作用
36
+
37
+ `setup-entry.ts` 是一个轻量级的入口点,OpenClaw 在以下情况下会加载它而不是完整的 `index.ts`:
38
+
39
+ - 频道被禁用但需要设置/引导界面
40
+ - 频道已启用但未配置
41
+ - 启用了延迟加载(`deferConfiguredChannelFullLoadUntilAfterListen`)
42
+
43
+ ### `defineSetupPluginEntry`
44
+
45
+ 这个辅助函数只返回 `{ plugin }`,不包含运行时或 CLI 连线。这确保了 setup entry 保持轻量级。
46
+
47
+ ### Setup Entry 必须包含
48
+
49
+ - 频道插件对象(通过 `defineSetupPluginEntry`)
50
+ - 网关监听前需要的任何 HTTP 路由
51
+ - 启动期间需要的任何 gateway 方法
52
+
53
+ ### Setup Entry 不应包含
54
+
55
+ - CLI 注册
56
+ - 后台服务
57
+ - 重的运行时导入(crypto、SDKs)
58
+ - 仅在启动后需要的 gateway 方法
59
+
60
+
61
+
62
+
63
+
64
+
65
+
66
+ ## 《outbound-adapter.ts》
67
+
68
+ ### Outbound 适配器结构
69
+
70
+ `createChatChannelPlugin` 的 `outbound` 参数支持两种形式:
71
+
72
+ 1. **`attachedResults`** - 包含 `sendText`、`sendMedia`、`sendPoll` 方法,返回结果元数据(如 `messageId`)
73
+ 2. **`base`** - 包含其他 outbound 配置,如 `deliveryMode`、`chunker`、`sanitizeText` 等
74
+
75
+ ### sendText 方法签名
76
+
77
+ `sendText` 接收的 context 参数包含:
78
+
79
+ - `cfg` - OpenClaw 配置
80
+ - `to` - 目标用户/频道 ID
81
+ - `text` - 要发送的文本
82
+ - `accountId` - 账户 ID
83
+ - `replyToId` - 回复的消息 ID
84
+ - `threadId` - 线程 ID
85
+
86
+ ### sendMedia 方法签名
87
+
88
+ `sendMedia` 接收额外的参数:
89
+
90
+ - `mediaUrl` - 媒体文件 URL
91
+ - `mediaAccess` - 媒体访问权限
92
+ - `mediaLocalRoots` - 本地媒体根目录
93
+ - `mediaReadFile` - 读取本地文件的函数
94
+
95
+ ### 参考实现
96
+
97
+ - **Slack** 使用 `createAttachedChannelResultAdapter` 包装 send 方法
98
+ - **WhatsApp** 在 sendMedia 中处理媒体上传和发送
99
+ - **Telegram** 定义了 `deliveryCapabilities` 来声明支持的功能
100
+
101
+
102
+
103
+
104
+
105
+
106
+
107
+ ## 《websocket-client.ts》
108
+
109
+ ### WebSocket 客户端特性
110
+
111
+ 1. **连接管理** - 使用 `ws` 包创建 WebSocket 连接,支持握手超时配置
112
+ 2. **事件捕获** - 使用 `captureWsEvent` 记录 WebSocket 事件,便于调试和监控
113
+ 3. **自动重连** - 连接断开后自动重连,默认 5 秒延迟
114
+ 4. **认证处理** - 连接成功后发送认证消息(需要根据友虾名片 API 实现)
115
+ 5. **消息处理** - 接收消息并通过回调函数转发
116
+
117
+ ### 集成到插件生命周期
118
+
119
+ - **`onActivate`** - 插件激活时创建 WebSocket 连接并存储到运行时上下文
120
+ - **`onDeactivate`** - 插件停用时断开 WebSocket 连接
121
+ - **Outbound** - 从运行时上下文获取 WebSocket 客户端实例发送消息
122
+
123
+
124
+
125
+ ## 《inbound-handler.ts》
126
+
127
+ ### Inbound 处理流程
128
+
129
+ 1. **消息解析** - `parseSumeclawMessage` 将平台原始消息转换为标准格式
130
+ 2. **会话路由** - 使用 `buildAgentSessionKey` 和 `resolveThreadSessionKeys` 构建会话密钥
131
+ 3. **Envelope 格式化** - 使用 `formatInboundEnvelope` 构建 OpenClaw 标准消息 envelope
132
+ 4. **上下文完成** - 使用 `finalizeInboundContext` 完成 inbound 上下文
133
+ 5. **消息去重** - 使用内存缓存防止重复处理消息
package/setup-entry.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
2
+ import { sumeclawPlugin } from "./src/plugin.js";
3
+
4
+ export default defineSetupPluginEntry(sumeclawPlugin);
@@ -0,0 +1,191 @@
1
+ import {
2
+ formatInboundEnvelope,
3
+ resolveEnvelopeFormatOptions,
4
+ } from "openclaw/plugin-sdk/channel-inbound";
5
+ import { resolveChannelContextVisibilityMode } from "openclaw/plugin-sdk/context-visibility-runtime";
6
+ import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime";
7
+ import { buildAgentSessionKey, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
8
+ import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
9
+ import { evaluateSupplementalContextVisibility } from "openclaw/plugin-sdk/security-runtime";
10
+ import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-runtime";
11
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
12
+ import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
13
+
14
+ // 友虾名片平台消息类型
15
+ export type SumeclawMessage = {
16
+ id: string;
17
+ from: string;
18
+ to: string;
19
+ text: string;
20
+ timestamp: number;
21
+ type: "text" | "media" | "system";
22
+ mediaUrl?: string;
23
+ replyToId?: string;
24
+ threadId?: string;
25
+ };
26
+
27
+ // Inbound 处理上下文
28
+ export type SumeclawInboundContext = {
29
+ cfg: OpenClawConfig;
30
+ runtime: RuntimeEnv;
31
+ logger: any;
32
+ accountId: string | null;
33
+ message: SumeclawMessage;
34
+ };
35
+
36
+ // 解析友虾名片消息到 OpenClaw 格式
37
+ export function parseSumeclawMessage(raw: any): SumeclawMessage | null {
38
+ try {
39
+ // 根据友虾名片 API 实际格式解析
40
+ return {
41
+ id: raw.id || String(Date.now()),
42
+ from: raw.from || raw.senderId,
43
+ to: raw.to || raw.receiverId,
44
+ text: raw.text || raw.content || "",
45
+ timestamp: raw.timestamp || Date.now(),
46
+ type: raw.type || "text",
47
+ mediaUrl: raw.mediaUrl,
48
+ replyToId: raw.replyToId,
49
+ threadId: raw.threadId,
50
+ };
51
+ } catch (error) {
52
+ console.error("[sumeclaw] Failed to parse message", error);
53
+ return null;
54
+ }
55
+ }
56
+
57
+ // 构建 inbound envelope
58
+ export async function buildSumeclawInboundEnvelope(
59
+ context: SumeclawInboundContext,
60
+ ) {
61
+ const { cfg, runtime, logger, accountId, message } = context;
62
+
63
+ // 解析会话密钥
64
+ const baseSessionKey = buildAgentSessionKey({
65
+ agentId: "main", // 默认使用 main agent,可以根据配置路由
66
+ channel: "sumeclaw",
67
+ peer: message.from,
68
+ accountId: accountId || undefined,
69
+ });
70
+
71
+ // 解析线程密钥(如果有)
72
+ const threadKeys = message.threadId
73
+ ? resolveThreadSessionKeys({
74
+ baseSessionKey,
75
+ threadId: String(message.threadId),
76
+ channel: "sumeclaw",
77
+ })
78
+ : undefined;
79
+
80
+ const sessionKey = threadKeys?.threadSessionKey || baseSessionKey;
81
+
82
+ // 构建发送者标签
83
+ const senderLabel = `User ${message.from}`;
84
+
85
+ // 构建消息文本
86
+ const messageText = message.text || "";
87
+
88
+ // 格式化 inbound envelope
89
+ const envelope = formatInboundEnvelope({
90
+ channel: "sumeclaw",
91
+ senderLabel,
92
+ text: messageText,
93
+ commandBody: messageText,
94
+ bodyForAgent: messageText,
95
+ timestamp: message.timestamp,
96
+ messageId: message.id,
97
+ replyToId: message.replyToId,
98
+ threadId: message.threadId,
99
+ sessionKey,
100
+ history: [], // 可以添加历史消息
101
+ media: message.mediaUrl
102
+ ? [{ url: message.mediaUrl, type: "image" }]
103
+ : undefined,
104
+ });
105
+
106
+ return {
107
+ envelope,
108
+ sessionKey,
109
+ baseSessionKey,
110
+ threadKeys,
111
+ };
112
+ }
113
+
114
+ // 处理 inbound 消息并转发到 OpenClaw
115
+ export async function handleSumeclawInbound(
116
+ context: SumeclawInboundContext,
117
+ ) {
118
+ const { cfg, runtime, logger, message } = context;
119
+
120
+ logger.info("[sumeclaw] Processing inbound message", {
121
+ channel: "sumeclaw",
122
+ messageId: message.id,
123
+ from: message.from,
124
+ text: message.text?.substring(0, 100),
125
+ });
126
+
127
+ // 解析消息
128
+ const parsedMessage = parseSumeclawMessage(message);
129
+ if (!parsedMessage) {
130
+ logger.error("[sumeclaw] Failed to parse message", {
131
+ channel: "sumeclaw",
132
+ rawMessage: message,
133
+ });
134
+ return;
135
+ }
136
+
137
+ // 构建 inbound envelope
138
+ const { envelope, sessionKey } = await buildSumeclawInboundEnvelope(context);
139
+
140
+ // 完成 inbound 上下文
141
+ const inboundContext = finalizeInboundContext({
142
+ cfg,
143
+ runtime,
144
+ channel: "sumeclaw",
145
+ sessionKey,
146
+ envelope,
147
+ source: {
148
+ kind: "channel",
149
+ channel: "sumeclaw",
150
+ accountId: context.accountId || undefined,
151
+ peer: message.from,
152
+ },
153
+ });
154
+
155
+ // 记录 inbound 消息
156
+ logger.info("[sumeclaw] Inbound message ready for dispatch", {
157
+ channel: "sumeclaw",
158
+ sessionKey,
159
+ messageId: message.id,
160
+ });
161
+
162
+ // TODO: 将消息转发到 OpenClaw 的 inbound 处理系统
163
+ // 这需要使用 OpenClaw 的 inbound dispatch API
164
+ // 参考 Discord 的实现:extensions/discord/src/monitor/message-handler.ts
165
+
166
+ return inboundContext;
167
+ }
168
+
169
+ // 消息去重缓存
170
+ const messageDedupeCache = new Map<string, number>();
171
+
172
+ // 检查消息是否重复
173
+ export function isDuplicateMessage(messageId: string, ttlMs: number = 60000): boolean {
174
+ const now = Date.now();
175
+ const lastSeen = messageDedupeCache.get(messageId);
176
+
177
+ if (lastSeen && (now - lastSeen) < ttlMs) {
178
+ return true;
179
+ }
180
+
181
+ messageDedupeCache.set(messageId, now);
182
+
183
+ // 清理过期缓存
184
+ for (const [id, timestamp] of messageDedupeCache.entries()) {
185
+ if (now - timestamp > ttlMs) {
186
+ messageDedupeCache.delete(id);
187
+ }
188
+ }
189
+
190
+ return false;
191
+ }
@@ -0,0 +1,94 @@
1
+ import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/channel-send-result";
2
+ import { createAttachedChannelResultAdapter } from "openclaw/plugin-sdk/channel-send-result";
3
+ import type { ResolvedAccount } from "./types.js";
4
+ import type { SumeclawWebSocketClient } from "./websocket-client.js";
5
+
6
+ async function sendSumeclawMessage(params: {
7
+ to: string;
8
+ text: string;
9
+ token: string;
10
+ wsUrl: string;
11
+ replyToId?: string | null;
12
+ threadId?: string | number | null;
13
+ wsClient?: SumeclawWebSocketClient;
14
+ }): Promise<{ messageId: string }> {
15
+ if (params.wsClient && params.wsClient.isReady()) {
16
+ const message = {
17
+ type: "message",
18
+ to: params.to,
19
+ text: params.text,
20
+ replyToId: params.replyToId,
21
+ threadId: params.threadId,
22
+ };
23
+ params.wsClient.send(message);
24
+ return { messageId: `msg-${Date.now()}` };
25
+ }
26
+
27
+ console.log("[sumeclaw] Sending message via HTTP fallback", {
28
+ to: params.to,
29
+ text: params.text
30
+ });
31
+ return { messageId: `msg-${Date.now()}` };
32
+ }
33
+
34
+ async function sendSumeclawMedia(params: {
35
+ to: string;
36
+ text: string;
37
+ mediaUrl: string;
38
+ token: string;
39
+ wsUrl: string;
40
+ replyToId?: string | null;
41
+ threadId?: string | number | null;
42
+ wsClient?: SumeclawWebSocketClient;
43
+ }): Promise<{ messageId: string }> {
44
+ console.log("[sumeclaw] Sending media", { to: params, text: params.text, mediaUrl: params.mediaUrl });
45
+ return { messageId: `media-${Date.now()}` };
46
+ }
47
+
48
+ export const sumeclawOutbound: ChannelOutboundAdapter = {
49
+ deliveryMode: "direct",
50
+ textChunkLimit: 4000,
51
+ chunkerMode: "text",
52
+ sanitizeText: ({ text }) => text,
53
+ ...createAttachedChannelResultAdapter({
54
+ channel: "sumeclaw",
55
+ sendText: async ({ cfg, to, text, accountId, replyToId, threadId, runtime }) => {
56
+ const account = (cfg.channels as Record<string, any>)?.["sumeclaw"];
57
+ const wsClient = (runtime as any).sumeclawWsClient;
58
+ const result = await sendSumeclawMessage({
59
+ to,
60
+ text,
61
+ token: account?.token,
62
+ wsUrl: account?.wsUrl ?? "wss://api.gixin.cc",
63
+ replyToId,
64
+ threadId,
65
+ wsClient,
66
+ });
67
+ return { messageId: result.messageId };
68
+ },
69
+ sendMedia: async ({
70
+ cfg,
71
+ to,
72
+ text,
73
+ mediaUrl,
74
+ accountId,
75
+ replyToId,
76
+ threadId,
77
+ runtime,
78
+ }) => {
79
+ const account = (cfg.channels as Record<string, any>)?.["sumeclaw"];
80
+ const wsClient = (runtime as any).sumeclawWsClient;
81
+ const result = await sendSumeclawMedia({
82
+ to,
83
+ text,
84
+ mediaUrl,
85
+ token: account?.token,
86
+ wsUrl: account?.wsUrl ?? "wss://api.gixin.cc",
87
+ replyToId,
88
+ threadId,
89
+ wsClient,
90
+ });
91
+ return { messageId: result.messageId };
92
+ },
93
+ }),
94
+ };
package/src/plugin.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { createChatChannelPlugin, createChannelPluginBase } from "openclaw/plugin-sdk/channel-core";
2
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/channel-core";
3
+ import { resolveAccount, type ResolvedAccount } from "./types.js";
4
+ import { sumeclawOutbound } from "./outbound-adapter.js";
5
+
6
+ export const sumeclawPlugin = createChatChannelPlugin<ResolvedAccount>({
7
+ base: createChannelPluginBase({
8
+ id: "sumeclaw",
9
+ setup: {
10
+ resolveAccount,
11
+ inspectAccount(cfg, accountId) {
12
+ const section = (cfg.channels as Record<string, any>)?.["sumeclaw"];
13
+ return {
14
+ enabled: Boolean(section?.token),
15
+ configured: Boolean(section?.token),
16
+ tokenStatus: section?.token ? "available" : "missing",
17
+ };
18
+ },
19
+ },
20
+ }),
21
+ security: {
22
+ dm: {
23
+ channelKey: "sumeclaw",
24
+ resolvePolicy: (account) => account.dmPolicy,
25
+ resolveAllowFrom: (account) => account.allowFrom,
26
+ defaultPolicy: "allowlist",
27
+ },
28
+ },
29
+ threading: { topLevelReplyToMode: "reply" },
30
+ outbound: sumeclawOutbound,
31
+ });
package/src/types.ts ADDED
@@ -0,0 +1,26 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
2
+
3
+ export type ResolvedAccount = {
4
+ accountId: string | null;
5
+ token: string;
6
+ wsUrl: string;
7
+ allowFrom: string[];
8
+ dmPolicy: string | undefined;
9
+ };
10
+
11
+ export function resolveAccount(
12
+ cfg: OpenClawConfig,
13
+ accountId?: string | null,
14
+ ): ResolvedAccount {
15
+ const section = (cfg.channels as Record<string, any>)?.["sumeclaw"];
16
+ const token = section?.token;
17
+ const wsUrl = section?.wsUrl ?? "wss://api.gixin.cc";
18
+ if (!token) throw new Error("sumeclaw: token is required");
19
+ return {
20
+ accountId: accountId ?? null,
21
+ token,
22
+ wsUrl,
23
+ allowFrom: section?.allowFrom ?? [],
24
+ dmPolicy: section?.dmSecurity,
25
+ };
26
+ }
@@ -0,0 +1,174 @@
1
+ import { captureWsEvent } from "openclaw/plugin-sdk/proxy-capture";
2
+ import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
3
+ import * as ws from "ws";
4
+
5
+ import type { ResolvedAccount } from "./types.js";
6
+
7
+ export class SumeclawWebSocketClient {
8
+ private ws: ws.WebSocket | null = null;
9
+ private reconnectTimer: NodeJS.Timeout | null = null;
10
+ private logger: any;
11
+ private runtime: RuntimeEnv;
12
+ private account: ResolvedAccount;
13
+ private onMessageCallback?: (data: any) => void;
14
+ private isConnected: boolean = false;
15
+
16
+ constructor(account: ResolvedAccount, runtime: RuntimeEnv, logger: any) {
17
+ this.account = account;
18
+ this.runtime = runtime;
19
+ this.logger = logger;
20
+ }
21
+
22
+ connect(onMessage?: (data: any) => void) {
23
+ this.onMessageCallback = onMessage;
24
+ this.logger.info("[sumeclaw] Connecting to WebSocket", {
25
+ channel: "sumeclaw",
26
+ wsUrl: this.account.wsUrl,
27
+ });
28
+
29
+ try {
30
+ this.ws = new ws.WebSocket(this.account.wsUrl, {
31
+ handshakeTimeout: 30000,
32
+ });
33
+
34
+ const wsFlowId = `${Date.now()}-${Math.random()}`;
35
+
36
+ this.ws.on("open", () => {
37
+ this.isConnected = true;
38
+ this.logger.info("[sumeclaw] WebSocket connected", {
39
+ channel: "sumeclaw",
40
+ });
41
+ captureWsEvent({
42
+ url: this.account.wsUrl,
43
+ direction: "local",
44
+ kind: "ws-open",
45
+ flowId: wsFlowId,
46
+ meta: { subsystem: "sumeclaw-gateway" },
47
+ });
48
+
49
+ this.sendAuth();
50
+ });
51
+
52
+ this.ws.on("message", (data: ws.Data) => {
53
+ captureWsEvent({
54
+ url: this.account.wsUrl,
55
+ direction: "inbound",
56
+ kind: "ws-frame",
57
+ flowId: wsFlowId,
58
+ payload: Buffer.isBuffer(data) ? data : Buffer.from(String(data)),
59
+ meta: { subsystem: "sumeclaw-gateway" },
60
+ });
61
+
62
+ if (this.onMessageCallback) {
63
+ try {
64
+ const message = JSON.parse(data.toString());
65
+ this.onMessageCallback(message);
66
+ } catch (error) {
67
+ this.logger.error("[sumeclaw] Failed to parse message", {
68
+ channel: "sumeclaw",
69
+ error: String(error),
70
+ });
71
+ }
72
+ }
73
+ });
74
+
75
+ this.ws.on("close", (code: number, reason: Buffer) => {
76
+ this.isConnected = false;
77
+ this.logger.warn("[sumeclaw] WebSocket disconnected", {
78
+ channel: "sumeclaw",
79
+ code,
80
+ reason: reason.toString(),
81
+ });
82
+ captureWsEvent({
83
+ url: this.account.wsUrl,
84
+ direction: "local",
85
+ kind: "ws-close",
86
+ flowId: wsFlowId,
87
+ closeCode: code,
88
+ payload: reason,
89
+ meta: { subsystem: "sumeclaw-gateway" },
90
+ });
91
+
92
+ this.scheduleReconnect();
93
+ });
94
+
95
+ this.ws.on("error", (error: Error) => {
96
+ this.logger.error("[sumeclaw] WebSocket error", {
97
+ channel: "sumeclaw",
98
+ error: error.message,
99
+ });
100
+ captureWsEvent({
101
+ url: this.account.wsUrl,
102
+ direction: "local",
103
+ kind: "error",
104
+ flowId: wsFlowId,
105
+ errorText: error.message,
106
+ meta: { subsystem: "sumeclaw-gateway" },
107
+ });
108
+ });
109
+ } catch (error) {
110
+ this.logger.error("[sumeclaw] Failed to create WebSocket", {
111
+ channel: "sumeclaw",
112
+ error: String(error),
113
+ });
114
+ this.scheduleReconnect();
115
+ }
116
+ }
117
+
118
+ private sendAuth() {
119
+ if (this.ws && this.ws.readyState === ws.WebSocket.OPEN) {
120
+ const authMessage = {
121
+ type: "auth",
122
+ token: this.account.token,
123
+ };
124
+ this.ws.send(JSON.stringify(authMessage));
125
+ this.logger.info("[sumeclaw] Auth message sent", { channel: "sumeclaw" });
126
+ }
127
+ }
128
+
129
+ send(data: any) {
130
+ if (this.ws && this.ws.readyState === ws.WebSocket.OPEN) {
131
+ this.ws.send(JSON.stringify(data));
132
+ } else {
133
+ this.logger.warn("[sumeclaw] Cannot send message, WebSocket not connected", {
134
+ channel: "sumeclaw",
135
+ });
136
+ }
137
+ }
138
+
139
+ private scheduleReconnect() {
140
+ if (this.reconnectTimer) {
141
+ return;
142
+ }
143
+
144
+ const delay = 5000;
145
+ this.logger.info("[sumeclaw] Scheduling reconnect", {
146
+ channel: "sumeclaw",
147
+ delay,
148
+ });
149
+
150
+ this.reconnectTimer = setTimeout(() => {
151
+ this.reconnectTimer = null;
152
+ this.connect(this.onMessageCallback);
153
+ }, delay);
154
+ }
155
+
156
+ disconnect() {
157
+ if (this.reconnectTimer) {
158
+ clearTimeout(this.reconnectTimer);
159
+ this.reconnectTimer = null;
160
+ }
161
+
162
+ if (this.ws) {
163
+ this.ws.close();
164
+ this.ws = null;
165
+ }
166
+
167
+ this.isConnected = false;
168
+ this.logger.info("[sumeclaw] WebSocket disconnected", { channel: "sumeclaw" });
169
+ }
170
+
171
+ isReady(): boolean {
172
+ return this.isConnected && this.ws?.readyState === ws.WebSocket.OPEN;
173
+ }
174
+ }
package/dist/index.d.ts DELETED
@@ -1,12 +0,0 @@
1
- /**
2
- * @sumeai/sumeclaw — 稳定版(强制 channel 识别)
3
- */
4
- declare const plugin: {
5
- type: string;
6
- id: string;
7
- name: string;
8
- description: string;
9
- onReady(ctx: any): Promise<void>;
10
- };
11
- export default plugin;
12
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAGA;;GAEG;AAIH,QAAA,MAAM,MAAM;;;;;iBAQS,GAAG;CAyCvB,CAAC;AAIF,eAAe,MAAM,CAAC"}
package/dist/index.js DELETED
@@ -1,50 +0,0 @@
1
- "use strict";
2
- /**
3
- * @sumeai/sumeclaw — 稳定版(强制 channel 识别)
4
- */
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const gateway_js_1 = require("./src/gateway.js");
7
- const plugin = {
8
- // 🔥 核心:手动声明 channel 类型(解决 missing id)
9
- type: "channel",
10
- id: "sumeclaw",
11
- name: "友虾名片",
12
- description: "友虾名片 Channel",
13
- async onReady(ctx) {
14
- const { api, cfg } = ctx;
15
- console.log("[sumeclaw] 🚀 插件启动");
16
- // 🔍 调试用(可以先保留)
17
- console.log("[sumeclaw] cfg =", JSON.stringify(cfg, null, 2));
18
- // ✅ 兼容不同 OpenClaw 版本
19
- const accounts = cfg?.channels?.sumeclaw?.accounts ||
20
- cfg?.channel?.accounts || // ⚠️ 有些版本是扁平的
21
- {};
22
- if (!accounts || Object.keys(accounts).length === 0) {
23
- console.warn("[sumeclaw] ⚠️ 没有配置 accounts");
24
- return;
25
- }
26
- for (const accountId of Object.keys(accounts)) {
27
- const account = accounts[accountId];
28
- if (!account?.registrationToken) {
29
- console.error(`[sumeclaw] ❌ account ${accountId} 缺少 registrationToken`);
30
- continue;
31
- }
32
- console.log(`[sumeclaw] 🔗 正在连接 account: ${accountId}`);
33
- try {
34
- (0, gateway_js_1.connectGateway)({
35
- accountId,
36
- platformUrl: account.platformUrl,
37
- registrationToken: account.registrationToken,
38
- api
39
- });
40
- }
41
- catch (err) {
42
- console.error("[sumeclaw] ❌ 连接失败:", err);
43
- }
44
- }
45
- }
46
- };
47
- // ✅ 两种导出同时兼容
48
- exports.default = plugin;
49
- module.exports = plugin;
50
- //# sourceMappingURL=index.js.map
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":";AAGA;;GAEG;;AAEH,iDAAkD;AAElD,MAAM,MAAM,GAAG;IACb,uCAAuC;IACvC,IAAI,EAAE,SAAS;IAEf,EAAE,EAAE,UAAU;IACd,IAAI,EAAE,MAAM;IACZ,WAAW,EAAE,cAAc;IAE3B,KAAK,CAAC,OAAO,CAAC,GAAQ;QACpB,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,GAAG,CAAC;QAEzB,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;QAElC,gBAAgB;QAChB,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QAE9D,qBAAqB;QACrB,MAAM,QAAQ,GACZ,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ;YACjC,GAAG,EAAE,OAAO,EAAE,QAAQ,IAAM,cAAc;YAC1C,EAAE,CAAC;QAEL,IAAI,CAAC,QAAQ,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpD,OAAO,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;YAC5C,OAAO;QACT,CAAC;QAED,KAAK,MAAM,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC9C,MAAM,OAAO,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC;YAEpC,IAAI,CAAC,OAAO,EAAE,iBAAiB,EAAE,CAAC;gBAChC,OAAO,CAAC,KAAK,CAAC,wBAAwB,SAAS,uBAAuB,CAAC,CAAC;gBACxE,SAAS;YACX,CAAC;YAED,OAAO,CAAC,GAAG,CAAC,+BAA+B,SAAS,EAAE,CAAC,CAAC;YAExD,IAAI,CAAC;gBACH,IAAA,2BAAc,EAAC;oBACb,SAAS;oBACT,WAAW,EAAE,OAAO,CAAC,WAAW;oBAChC,iBAAiB,EAAE,OAAO,CAAC,iBAAiB;oBAC5C,GAAG;iBACJ,CAAC,CAAC;YACL,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,CAAC,KAAK,CAAC,oBAAoB,EAAE,GAAG,CAAC,CAAC;YAC3C,CAAC;QACH,CAAC;IACH,CAAC;CACF,CAAC;AAGF,aAAa;AACb,kBAAe,MAAM,CAAC;AACtB,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC"}
@@ -1,3 +0,0 @@
1
- declare const _default: any;
2
- export default _default;
3
- //# sourceMappingURL=setup-entry.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"setup-entry.d.ts","sourceRoot":"","sources":["../setup-entry.ts"],"names":[],"mappings":";AASA,wBAAsD"}
@@ -1,12 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- /**
4
- * 友虾名片 Channel 插件 — 轻量设置入口
5
- *
6
- * 此入口在 openclaw plugins install 阶段加载,
7
- * 用于展示配置向导和验证 Token 有效性。
8
- */
9
- const channel_core_1 = require("openclaw/plugin-sdk/channel-core");
10
- const channel_js_1 = require("./src/channel.js");
11
- exports.default = (0, channel_core_1.defineSetupPluginEntry)(channel_js_1.sumeclawPlugin);
12
- //# sourceMappingURL=setup-entry.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"setup-entry.js","sourceRoot":"","sources":["../setup-entry.ts"],"names":[],"mappings":";;AAAA;;;;;GAKG;AACH,mEAA0E;AAC1E,iDAAkD;AAElD,kBAAe,IAAA,qCAAsB,EAAC,2BAAc,CAAC,CAAC"}
@@ -1,7 +0,0 @@
1
- export interface SumelawAccount {
2
- registrationToken?: string;
3
- platformUrl?: string;
4
- enabled?: boolean;
5
- }
6
- export declare const sumeclawPlugin: any;
7
- //# sourceMappingURL=channel.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../src/channel.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,cAAc;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AA4CD,eAAO,MAAM,cAAc,KAkFzB,CAAC"}
@@ -1,117 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.sumeclawPlugin = void 0;
4
- /**
5
- * 友虾名片 ChannelPlugin 核心定义
6
- *
7
- * 基于新版 plugin-sdk(2026.3.22+)的 createChatChannelPlugin API。
8
- */
9
- const channel_core_1 = require("openclaw/plugin-sdk/channel-core");
10
- const gateway_js_1 = require("./gateway.js");
11
- /**
12
- * 安装/配置向导 — 交互式提示用户输入 registrationToken
13
- */
14
- const channelKey = "sumeclaw";
15
- const setupWizard = {
16
- channel: channelKey,
17
- /** 根据当前配置显示连接状态 */
18
- status: {
19
- configuredLabel: "已连接友虾名片",
20
- unconfiguredLabel: "尚未配置",
21
- resolveConfigured: ({ cfg }) => {
22
- const accounts = cfg.channels?.[channelKey]?.accounts ?? {};
23
- return Object.values(accounts).some((acct) => acct?.registrationToken);
24
- },
25
- },
26
- /** 交互式凭据收集 — 引导用户输入 registrationToken */
27
- credentials: [
28
- {
29
- inputKey: "registrationToken",
30
- credentialLabel: "友虾名片注册令牌",
31
- preferredEnvVar: "SUMECLAW_REGISTRATION_TOKEN",
32
- envPrompt: "是否使用环境变量 SUMECLAW_REGISTRATION_TOKEN?",
33
- keepPrompt: "保留当前 registrationToken?",
34
- inputPrompt: "请输入从友虾小程序获取的 registrationToken:",
35
- inspect: ({ cfg, accountId }) => {
36
- const token = cfg.channels?.[channelKey]?.accounts?.[accountId ?? "default"]?.registrationToken;
37
- return {
38
- accountConfigured: Boolean(token),
39
- hasConfiguredValue: Boolean(token),
40
- };
41
- },
42
- },
43
- ],
44
- };
45
- exports.sumeclawPlugin = (0, channel_core_1.createChatChannelPlugin)({
46
- base: (0, channel_core_1.createChannelPluginBase)({
47
- id: channelKey,
48
- setupWizard,
49
- setup: {
50
- /**
51
- * 从用户配置中解析账户信息
52
- */
53
- resolveAccount(cfg, accountId) {
54
- const accounts = cfg.channels?.[channelKey]?.accounts ?? {};
55
- return (accounts[accountId ?? "default"] ?? {});
56
- },
57
- /**
58
- * 检查账户配置状态
59
- */
60
- inspectAccount(cfg, accountId) {
61
- const account = cfg.channels?.[channelKey]?.accounts?.[accountId ?? "default"];
62
- return {
63
- enabled: !!account?.registrationToken,
64
- configured: !!account?.registrationToken,
65
- };
66
- },
67
- },
68
- }),
69
- /**
70
- * 渠道配置适配器 (ChannelConfigAdapter)
71
- */
72
- config: {
73
- listAccountIds(cfg) {
74
- const accounts = cfg.channels?.[channelKey]?.accounts ?? {};
75
- return Object.keys(accounts);
76
- },
77
- resolveAccount(cfg, accountId) {
78
- const accounts = cfg.channels?.[channelKey]?.accounts ?? {};
79
- return (accounts[accountId ?? "default"] ?? {});
80
- },
81
- },
82
- /**
83
- * DM 安全策略
84
- *
85
- * 友虾名片场景下,所有通过名片访问的用户都允许对话。
86
- * 名片主可在小程序端管理黑名单(后续扩展)。
87
- */
88
- security: {
89
- dm: {
90
- channelKey: "sumeclaw",
91
- resolvePolicy: () => "open",
92
- defaultPolicy: "open",
93
- },
94
- },
95
- /**
96
- * 出站消息处理
97
- *
98
- * 当 OpenClaw Agent 生成回复后,通过 WebSocket 推回友虾名片平台。
99
- */
100
- outbound: {
101
- attachedResults: {
102
- sendText: async ({ text, to, accountId }) => {
103
- const ws = (0, gateway_js_1.getWebSocket)(accountId ?? "default");
104
- if (!ws || ws.readyState !== 1) {
105
- throw new Error("WebSocket 未连接到友虾名片平台");
106
- }
107
- ws.send(JSON.stringify({
108
- type: "agent_reply",
109
- userId: to,
110
- text,
111
- }));
112
- return {};
113
- },
114
- },
115
- },
116
- });
117
- //# sourceMappingURL=channel.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"channel.js","sourceRoot":"","sources":["../../src/channel.ts"],"names":[],"mappings":";;;AAAA;;;;GAIG;AACH,mEAG0C;AAE1C,6CAA4C;AAQ5C;;GAEG;AACH,MAAM,UAAU,GAAG,UAAU,CAAC;AAE9B,MAAM,WAAW,GAAuB;IACtC,OAAO,EAAE,UAAU;IAEnB,mBAAmB;IACnB,MAAM,EAAE;QACN,eAAe,EAAE,SAAS;QAC1B,iBAAiB,EAAE,MAAM;QACzB,iBAAiB,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;YAC7B,MAAM,QAAQ,GAAI,GAAG,CAAC,QAAgB,EAAE,CAAC,UAAU,CAAC,EAAE,QAAQ,IAAI,EAAE,CAAC;YACrE,OAAO,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CACjC,CAAC,IAAS,EAAE,EAAE,CAAC,IAAI,EAAE,iBAAiB,CACvC,CAAC;QACJ,CAAC;KACF;IAED,yCAAyC;IACzC,WAAW,EAAE;QACX;YACE,QAAQ,EAAE,mBAAmB;YAC7B,eAAe,EAAE,UAAU;YAC3B,eAAe,EAAE,6BAA6B;YAC9C,SAAS,EAAE,uCAAuC;YAClD,UAAU,EAAE,yBAAyB;YACrC,WAAW,EAAE,iCAAiC;YAC9C,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE,EAAE;gBAC9B,MAAM,KAAK,GAAI,GAAG,CAAC,QAAgB,EAAE,CAAC,UAAU,CAAC,EAAE,QAAQ,EAAE,CAC3D,SAAS,IAAI,SAAS,CACvB,EAAE,iBAAiB,CAAC;gBACrB,OAAO;oBACL,iBAAiB,EAAE,OAAO,CAAC,KAAK,CAAC;oBACjC,kBAAkB,EAAE,OAAO,CAAC,KAAK,CAAC;iBACnC,CAAC;YACJ,CAAC;SACF;KACF;CACF,CAAC;AAEW,QAAA,cAAc,GAAG,IAAA,sCAAuB,EAAiB;IACpE,IAAI,EAAE,IAAA,sCAAuB,EAAC;QAC5B,EAAE,EAAE,UAAU;QACd,WAAW;QACX,KAAK,EAAE;YACL;;eAEG;YACH,cAAc,CAAC,GAAG,EAAE,SAAS;gBAC3B,MAAM,QAAQ,GAAI,GAAG,CAAC,QAAgB,EAAE,CAAC,UAAU,CAAC,EAAE,QAAQ,IAAI,EAAE,CAAC;gBACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,IAAI,SAAS,CAAC,IAAI,EAAE,CAAmB,CAAC;YACpE,CAAC;YAED;;eAEG;YACH,cAAc,CAAC,GAAG,EAAE,SAAS;gBAC3B,MAAM,OAAO,GAAI,GAAG,CAAC,QAAgB,EAAE,CAAC,UAAU,CAAC,EAAE,QAAQ,EAAE,CAC7D,SAAS,IAAI,SAAS,CACvB,CAAC;gBACF,OAAO;oBACL,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,iBAAiB;oBACrC,UAAU,EAAE,CAAC,CAAC,OAAO,EAAE,iBAAiB;iBACzC,CAAC;YACJ,CAAC;SACF;KACF,CAAC;IAEF;;OAEG;IACH,MAAM,EAAE;QACN,cAAc,CAAC,GAAG;YAChB,MAAM,QAAQ,GAAI,GAAG,CAAC,QAAgB,EAAE,CAAC,UAAU,CAAC,EAAE,QAAQ,IAAI,EAAE,CAAC;YACrE,OAAO,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC/B,CAAC;QAED,cAAc,CAAC,GAAG,EAAE,SAAS;YAC3B,MAAM,QAAQ,GAAI,GAAG,CAAC,QAAgB,EAAE,CAAC,UAAU,CAAC,EAAE,QAAQ,IAAI,EAAE,CAAC;YACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,IAAI,SAAS,CAAC,IAAI,EAAE,CAAmB,CAAC;QACpE,CAAC;KACF;IAED;;;;;OAKG;IACH,QAAQ,EAAE;QACR,EAAE,EAAE;YACF,UAAU,EAAE,UAAU;YACtB,aAAa,EAAE,GAAG,EAAE,CAAC,MAAM;YAC3B,aAAa,EAAE,MAAM;SACtB;KACF;IAED;;;;OAIG;IACH,QAAQ,EAAE;QACR,eAAe,EAAE;YACf,QAAQ,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE;gBAC1C,MAAM,EAAE,GAAG,IAAA,yBAAY,EAAC,SAAS,IAAI,SAAS,CAAC,CAAC;gBAChD,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,UAAU,KAAK,CAAC,EAAE,CAAC;oBAC/B,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;gBAC1C,CAAC;gBAED,EAAE,CAAC,IAAI,CACL,IAAI,CAAC,SAAS,CAAC;oBACb,IAAI,EAAE,aAAa;oBACnB,MAAM,EAAE,EAAE;oBACV,IAAI;iBACL,CAAC,CACH,CAAC;gBAEF,OAAO,EAAE,CAAC;YACZ,CAAC;SACF;KACF;CACF,CAAC,CAAC"}
@@ -1,26 +0,0 @@
1
- /**
2
- * 友虾名片 OpenClaw Channel 插件 — Gateway 层
3
- *
4
- * WebSocket 连接管理:
5
- * - 出站连接到 wss://api.gixin.cc/api/channel/{registrationToken}
6
- * - 心跳保活(30s 间隔)
7
- * - 断线自动重连(指数退避,最大 60s)
8
- * - 平台下发的消息注入 OpenClaw Agent
9
- * - Agent 回复通过 WS 推回平台
10
- */
11
- import WebSocket from "ws";
12
- export declare function getWebSocket(accountId: string): WebSocket | undefined;
13
- export interface MinicardAccount {
14
- registrationToken?: string;
15
- platformUrl?: string;
16
- enabled?: boolean;
17
- }
18
- type ConnectOptions = {
19
- accountId: string;
20
- platformUrl: string;
21
- registrationToken: string;
22
- api: any;
23
- };
24
- export declare function connectGateway(opts: ConnectOptions): void;
25
- export {};
26
- //# sourceMappingURL=gateway.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../../src/gateway.ts"],"names":[],"mappings":"AACA;;;;;;;;;GASG;AAEH,OAAO,SAAS,MAAM,IAAI,CAAC;AA4B3B,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAErE;AAID,MAAM,WAAW,eAAe;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAKD,KAAK,cAAc,GAAG;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,GAAG,EAAE,GAAG,CAAC;CACV,CAAC;AAEF,wBAAgB,cAAc,CAAC,IAAI,EAAE,cAAc,QA0GlD"}
@@ -1,121 +0,0 @@
1
- "use strict";
2
- /**
3
- * 友虾名片 OpenClaw Channel 插件 — Gateway 层
4
- *
5
- * WebSocket 连接管理:
6
- * - 出站连接到 wss://api.gixin.cc/api/channel/{registrationToken}
7
- * - 心跳保活(30s 间隔)
8
- * - 断线自动重连(指数退避,最大 60s)
9
- * - 平台下发的消息注入 OpenClaw Agent
10
- * - Agent 回复通过 WS 推回平台
11
- */
12
- var __importDefault = (this && this.__importDefault) || function (mod) {
13
- return (mod && mod.__esModule) ? mod : { "default": mod };
14
- };
15
- Object.defineProperty(exports, "__esModule", { value: true });
16
- exports.getWebSocket = getWebSocket;
17
- exports.connectGateway = connectGateway;
18
- const ws_1 = __importDefault(require("ws"));
19
- /**
20
- * 全局 WS 管理
21
- */
22
- const wsMap = {};
23
- // const __filename = fileURLToPath(import.meta.url);
24
- // const __dirname = dirname(__filename);
25
- // const pkg = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8"));
26
- // ─── 配置常量 ────────────────────────────────────────────────────────────────
27
- const DEFAULT_PLATFORM_URL = "wss://api.gixin.cc";
28
- const HEARTBEAT_MS = 30_000;
29
- const MAX_RECONNECT_MS = 60_000;
30
- const BASE_BACKOFF_MS = 1_000;
31
- // ─── 连接池 ──────────────────────────────────────────────────────────────────
32
- /** key: accountId → WebSocket */
33
- const connections = new Map();
34
- function getWebSocket(accountId) {
35
- return connections.get(accountId);
36
- }
37
- function connectGateway(opts) {
38
- const { accountId, platformUrl, registrationToken, api } = opts;
39
- let ws = null;
40
- let heartbeatTimer = null;
41
- let reconnectTimer = null;
42
- const HEARTBEAT_INTERVAL = 30_000;
43
- const RECONNECT_DELAY = 5_000;
44
- function start() {
45
- console.log(`[sumeclaw] 🌐 connecting -> ${platformUrl} (${accountId})`);
46
- ws = new ws_1.default(platformUrl);
47
- ws.on("open", () => {
48
- console.log(`[sumeclaw] ✅ connected: ${accountId}`);
49
- // 🔐 注册
50
- ws?.send(JSON.stringify({
51
- type: "register",
52
- token: registrationToken
53
- }));
54
- // ❤️ 心跳
55
- heartbeatTimer = setInterval(() => {
56
- if (ws?.readyState === ws_1.default.OPEN) {
57
- ws.send(JSON.stringify({ type: "ping" }));
58
- }
59
- }, HEARTBEAT_INTERVAL);
60
- });
61
- ws.on("message", (data) => {
62
- try {
63
- const msg = JSON.parse(data.toString());
64
- // 🔍 调试
65
- console.log("[sumeclaw] 📩 message:", msg);
66
- handleMessage(msg);
67
- }
68
- catch (err) {
69
- console.error("[sumeclaw] ❌ parse message error", err);
70
- }
71
- });
72
- ws.on("close", () => {
73
- console.warn(`[sumeclaw] ⚠️ disconnected: ${accountId}`);
74
- cleanup();
75
- scheduleReconnect();
76
- });
77
- ws.on("error", (err) => {
78
- console.error(`[sumeclaw] ❌ ws error: ${accountId}`, err);
79
- ws?.close();
80
- });
81
- }
82
- function handleMessage(msg) {
83
- switch (msg.type) {
84
- case "pong":
85
- // 心跳回应
86
- break;
87
- case "message":
88
- // 👉 这里接入 OpenClaw
89
- console.log("[sumeclaw] 💬 收到消息:", msg);
90
- // 示例:推给 agent
91
- api?.emit?.("message", {
92
- accountId,
93
- text: msg.text,
94
- raw: msg
95
- });
96
- break;
97
- case "registered":
98
- console.log(`[sumeclaw] 🔐 注册成功: ${accountId}`);
99
- break;
100
- default:
101
- console.log("[sumeclaw] ❓ 未处理消息:", msg);
102
- }
103
- }
104
- function cleanup() {
105
- if (heartbeatTimer) {
106
- clearInterval(heartbeatTimer);
107
- heartbeatTimer = null;
108
- }
109
- }
110
- function scheduleReconnect() {
111
- if (reconnectTimer)
112
- return;
113
- console.log(`[sumeclaw] 🔁 reconnecting in ${RECONNECT_DELAY / 1000}s`);
114
- reconnectTimer = setTimeout(() => {
115
- reconnectTimer = null;
116
- start();
117
- }, RECONNECT_DELAY);
118
- }
119
- start();
120
- }
121
- //# sourceMappingURL=gateway.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"gateway.js","sourceRoot":"","sources":["../../src/gateway.ts"],"names":[],"mappings":"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"}