yqf-feifei-openclaw-plugin 1.0.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.
@@ -0,0 +1,713 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var signalR = require('@microsoft/signalr');
6
+ var node_crypto = require('node:crypto');
7
+
8
+ function _interopNamespaceDefault(e) {
9
+ var n = Object.create(null);
10
+ if (e) {
11
+ Object.keys(e).forEach(function (k) {
12
+ if (k !== 'default') {
13
+ var d = Object.getOwnPropertyDescriptor(e, k);
14
+ Object.defineProperty(n, k, d.get ? d : {
15
+ enumerable: true,
16
+ get: function () { return e[k]; }
17
+ });
18
+ }
19
+ });
20
+ }
21
+ n.default = e;
22
+ return Object.freeze(n);
23
+ }
24
+
25
+ var signalR__namespace = /*#__PURE__*/_interopNamespaceDefault(signalR);
26
+
27
+ const CHANNEL_ID = "feifei";
28
+ const DEFAULT_ACCOUNT_ID = "default";
29
+ const DEFAULT_HUB_URL = "https://kefu.airtpb.cn/signalr/hubs/robot";
30
+ const DEFAULT_API_URL = "http://localhost:61201";
31
+
32
+ let _runtime = null;
33
+ function setFeiFeiRuntime(runtime) {
34
+ _runtime = runtime;
35
+ }
36
+ function getFeiFeiRuntime() {
37
+ if (!_runtime)
38
+ throw new Error("FeiFei runtime not initialized");
39
+ return _runtime;
40
+ }
41
+
42
+ /**
43
+ * FeiFei SignalR 连接模块
44
+ *
45
+ * 作为客户端连接到 FeiFei Server 的 RobotHub,
46
+ * 监听 OnReceiveMessageAsync 事件并 dispatch 到 OpenClaw。
47
+ */
48
+ // sessionId → sessionType,供 outbound.sendText 查找
49
+ const sessionRegistry = new Map();
50
+ function getSessionType(sessionId) {
51
+ return sessionRegistry.get(sessionId);
52
+ }
53
+ // ============================================================================
54
+ // Token 生成(HMAC-SHA256 签名,含 timestamp 防重放)
55
+ // ============================================================================
56
+ function buildToken$1(robotId, secret) {
57
+ const timestamp = Math.floor(Date.now() / 1000).toString();
58
+ const signature = node_crypto.createHmac("sha256", secret)
59
+ .update(`${robotId}:${timestamp}`)
60
+ .digest("hex");
61
+ return Buffer.from(`${robotId}:${timestamp}:${signature}`).toString("base64");
62
+ }
63
+ // ============================================================================
64
+ // Payload 文本提取
65
+ // ============================================================================
66
+ function extractBodyText(payload) {
67
+ switch (payload.type) {
68
+ case "Text":
69
+ return payload.text ?? "";
70
+ case "Markdown":
71
+ return payload.markdown ?? "";
72
+ case "Image": {
73
+ const img = payload;
74
+ return img.url ? `[图片] ${img.url}` : "[图片]";
75
+ }
76
+ case "File": {
77
+ const file = payload;
78
+ return file.url
79
+ ? `[文件: ${file.fileName ?? "unknown"}] ${file.url}`
80
+ : `[文件: ${file.fileName ?? "unknown"}]`;
81
+ }
82
+ default:
83
+ return `[${payload.type}]`;
84
+ }
85
+ }
86
+ // ============================================================================
87
+ // 单条消息处理
88
+ // ============================================================================
89
+ async function handleReceiveMessage(request, options, api) {
90
+ const { accountId, cfg, runtime } = options;
91
+ const core = getFeiFeiRuntime();
92
+ const sessionId = String(request.sessionId);
93
+ const userId = request.fromUserId ?? sessionId;
94
+ const body = extractBodyText(request.payload);
95
+ if (!body) {
96
+ runtime.log?.(`[feifei] skip empty message, sessionId=${sessionId}`);
97
+ return;
98
+ }
99
+ sessionRegistry.set(sessionId, request.sessionType);
100
+ runtime.log?.(`[feifei] ← OnReceiveMessage sessionId=${sessionId} user=${userId} text=${body.slice(0, 60)}`);
101
+ // 解析 OpenClaw 路由,用 sessionId 作为 peer id(一个 FeiFei 会话对应一个 OpenClaw 会话)
102
+ const route = core.channel.routing.resolveAgentRoute({
103
+ cfg,
104
+ channel: CHANNEL_ID,
105
+ accountId,
106
+ peer: { kind: "direct", id: sessionId },
107
+ });
108
+ const ctx = core.channel.reply.finalizeInboundContext({
109
+ Body: body,
110
+ RawBody: body,
111
+ CommandBody: body,
112
+ MessageSid: String(request.messageId),
113
+ From: `${CHANNEL_ID}:${userId}`,
114
+ To: `${CHANNEL_ID}:${sessionId}`,
115
+ SenderId: userId,
116
+ SessionKey: route.sessionKey,
117
+ AccountId: accountId,
118
+ ChatType: "direct",
119
+ ConversationLabel: `feifei:${request.sessionType.toLocaleLowerCase()}:${sessionId}`,
120
+ Timestamp: Date.now(),
121
+ Provider: CHANNEL_ID,
122
+ Surface: CHANNEL_ID,
123
+ OriginatingChannel: CHANNEL_ID,
124
+ OriginatingTo: `${CHANNEL_ID}:${sessionId}`,
125
+ CommandAuthorized: true,
126
+ });
127
+ await core.channel.reply
128
+ .dispatchReplyWithBufferedBlockDispatcher({
129
+ ctx,
130
+ cfg,
131
+ dispatcherOptions: {
132
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
133
+ deliver: async (payload, info) => {
134
+ runtime.log?.(`[feifei] → deliver kind=${info.kind} sessionType=${request.sessionType} sessionId=${request.sessionId} text=${(payload.text ?? "").slice(0, 60)}`);
135
+ const result = await api.sendMessage(request.sessionType, request.sessionId, payload);
136
+ runtime.log?.(`[feifei] → deliver ok messageId=${result.messageId}`);
137
+ },
138
+ onError: (err, info) => {
139
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
140
+ const apiErr = err;
141
+ const detail = apiErr?.status != null
142
+ ? ` status=${apiErr.status} body=${apiErr.response}`
143
+ : "";
144
+ runtime.error?.(`[feifei] deliver error kind=${info.kind}: ${String(err)}${detail}`);
145
+ },
146
+ },
147
+ })
148
+ .catch((err) => {
149
+ runtime.error?.(`[feifei] dispatch failed: ${String(err)}`);
150
+ });
151
+ }
152
+ // ============================================================================
153
+ // 主连接函数
154
+ // ============================================================================
155
+ async function monitorFeiFei(options, api) {
156
+ const { hubUrl, botId, secret, runtime, abortSignal } = options;
157
+ return new Promise((resolve, reject) => {
158
+ // accessTokenFactory 每次连接/重连时重新生成 token,避免长连接后 token 过期
159
+ // 服务端同时支持 query string(WebSocket)和 Authorization header(negotiate HTTP)
160
+ const connection = new signalR__namespace.HubConnectionBuilder()
161
+ .withUrl(hubUrl, {
162
+ accessTokenFactory: () => buildToken$1(botId, secret),
163
+ })
164
+ .withAutomaticReconnect({
165
+ nextRetryDelayInMilliseconds: (ctx) => {
166
+ if (ctx.previousRetryCount < 5)
167
+ return 2000;
168
+ if (ctx.previousRetryCount < 20)
169
+ return 10000;
170
+ return 30000;
171
+ },
172
+ })
173
+ .configureLogging(signalR__namespace.LogLevel.Warning)
174
+ .build();
175
+ // 监听用户消息
176
+ connection.on("OnReceiveMessage", (request) => {
177
+ handleReceiveMessage(request, options, api).catch((err) => {
178
+ runtime.error?.(`[feifei] handleReceiveMessage error: ${String(err)}`);
179
+ });
180
+ });
181
+ // 监听会话开始
182
+ connection.on("OnSessionStart", (request) => {
183
+ runtime.log?.(`[feifei] ← OnSessionStart: ${JSON.stringify(request)}`);
184
+ });
185
+ // 监听会话结束
186
+ connection.on("OnSessionEnd", (request) => {
187
+ runtime.log?.(`[feifei] ← OnSessionEnd: ${JSON.stringify(request)}`);
188
+ });
189
+ // 监听消息指令
190
+ connection.on("OnMessageCommand", (request) => {
191
+ runtime.log?.(`[feifei] ← OnMessageCommand: ${JSON.stringify(request)}`);
192
+ });
193
+ connection.onclose((err) => {
194
+ runtime.log?.(`[feifei] SignalR closed: ${err?.message ?? "clean"}`);
195
+ });
196
+ connection.onreconnecting((err) => {
197
+ runtime.log?.(`[feifei] SignalR reconnecting: ${err?.message ?? ""}`);
198
+ });
199
+ connection.onreconnected((connId) => {
200
+ runtime.log?.(`[feifei] SignalR reconnected connId=${connId}`);
201
+ });
202
+ // 收到 abort 信号时断开
203
+ abortSignal?.addEventListener("abort", async () => {
204
+ try {
205
+ await connection.stop();
206
+ }
207
+ catch { /* ignore */ }
208
+ resolve();
209
+ });
210
+ // 启动连接
211
+ connection
212
+ .start()
213
+ .then(() => {
214
+ runtime.log?.(`[feifei] SignalR connected → ${hubUrl} botId=${botId}`);
215
+ })
216
+ .catch((err) => {
217
+ runtime.error?.(`[feifei] SignalR start failed: ${String(err)}`);
218
+ reject(err);
219
+ });
220
+ });
221
+ }
222
+
223
+ //----------------------
224
+ // <auto-generated>
225
+ // Generated using the NSwag toolchain v14.7.0.0 (NJsonSchema v11.6.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org)
226
+ // </auto-generated>
227
+ //----------------------
228
+ /* eslint-disable */
229
+ // ReSharper disable InconsistentNaming
230
+ class ApiClient {
231
+ constructor(baseUrl, http) {
232
+ this.jsonParseReviver = undefined;
233
+ this.http = http ? http : { fetch: globalThis.fetch };
234
+ this.baseUrl = baseUrl ?? "/api";
235
+ }
236
+ /**
237
+ * 生成上传签名
238
+ */
239
+ assetGenerateUploadSign(input) {
240
+ let url_ = this.baseUrl + "/asset/generateUploadSign";
241
+ url_ = url_.replace(/[?&]$/, "");
242
+ const content_ = JSON.stringify(input);
243
+ let options_ = {
244
+ body: content_,
245
+ method: "POST",
246
+ headers: {
247
+ "Content-Type": "application/json",
248
+ "Accept": "application/json"
249
+ }
250
+ };
251
+ return this.http.fetch(url_, options_).then((_response) => {
252
+ return this.processAssetGenerateUploadSign(_response);
253
+ });
254
+ }
255
+ processAssetGenerateUploadSign(response) {
256
+ const status = response.status;
257
+ let _headers = {};
258
+ if (response.headers && response.headers.forEach) {
259
+ response.headers.forEach((v, k) => _headers[k] = v);
260
+ }
261
+ if (status === 200) {
262
+ return response.text().then((_responseText) => {
263
+ let result200 = null;
264
+ result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
265
+ return result200;
266
+ });
267
+ }
268
+ else if (status !== 200 && status !== 204) {
269
+ return response.text().then((_responseText) => {
270
+ return throwException("An unexpected server error occurred.", status, _responseText, _headers);
271
+ });
272
+ }
273
+ return Promise.resolve(null);
274
+ }
275
+ /**
276
+ * 发送消息
277
+ */
278
+ aiSessionSendMessage(input) {
279
+ let url_ = this.baseUrl + "/robots/aiSession/sendMessage";
280
+ url_ = url_.replace(/[?&]$/, "");
281
+ const content_ = JSON.stringify(input);
282
+ let options_ = {
283
+ body: content_,
284
+ method: "POST",
285
+ headers: {
286
+ "Content-Type": "application/json",
287
+ "Accept": "application/json"
288
+ }
289
+ };
290
+ return this.http.fetch(url_, options_).then((_response) => {
291
+ return this.processAiSessionSendMessage(_response);
292
+ });
293
+ }
294
+ processAiSessionSendMessage(response) {
295
+ const status = response.status;
296
+ let _headers = {};
297
+ if (response.headers && response.headers.forEach) {
298
+ response.headers.forEach((v, k) => _headers[k] = v);
299
+ }
300
+ if (status === 200) {
301
+ return response.text().then((_responseText) => {
302
+ let result200 = null;
303
+ result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
304
+ return result200;
305
+ });
306
+ }
307
+ else if (status !== 200 && status !== 204) {
308
+ return response.text().then((_responseText) => {
309
+ return throwException("An unexpected server error occurred.", status, _responseText, _headers);
310
+ });
311
+ }
312
+ return Promise.resolve(null);
313
+ }
314
+ /**
315
+ * 发送消息
316
+ */
317
+ botSessionSendMessage(input) {
318
+ let url_ = this.baseUrl + "/robots/botSession/sendMessage";
319
+ url_ = url_.replace(/[?&]$/, "");
320
+ const content_ = JSON.stringify(input);
321
+ let options_ = {
322
+ body: content_,
323
+ method: "POST",
324
+ headers: {
325
+ "Content-Type": "application/json",
326
+ "Accept": "application/json"
327
+ }
328
+ };
329
+ return this.http.fetch(url_, options_).then((_response) => {
330
+ return this.processBotSessionSendMessage(_response);
331
+ });
332
+ }
333
+ processBotSessionSendMessage(response) {
334
+ const status = response.status;
335
+ let _headers = {};
336
+ if (response.headers && response.headers.forEach) {
337
+ response.headers.forEach((v, k) => _headers[k] = v);
338
+ }
339
+ if (status === 200) {
340
+ return response.text().then((_responseText) => {
341
+ let result200 = null;
342
+ result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
343
+ return result200;
344
+ });
345
+ }
346
+ else if (status !== 200 && status !== 204) {
347
+ return response.text().then((_responseText) => {
348
+ return throwException("An unexpected server error occurred.", status, _responseText, _headers);
349
+ });
350
+ }
351
+ return Promise.resolve(null);
352
+ }
353
+ /**
354
+ * 查询机器人
355
+ */
356
+ robotFind(input) {
357
+ let url_ = this.baseUrl + "/robots/robot/find";
358
+ url_ = url_.replace(/[?&]$/, "");
359
+ const content_ = JSON.stringify(input);
360
+ let options_ = {
361
+ body: content_,
362
+ method: "POST",
363
+ headers: {
364
+ "Content-Type": "application/json",
365
+ "Accept": "application/json"
366
+ }
367
+ };
368
+ return this.http.fetch(url_, options_).then((_response) => {
369
+ return this.processRobotFind(_response);
370
+ });
371
+ }
372
+ processRobotFind(response) {
373
+ const status = response.status;
374
+ let _headers = {};
375
+ if (response.headers && response.headers.forEach) {
376
+ response.headers.forEach((v, k) => _headers[k] = v);
377
+ }
378
+ if (status === 200) {
379
+ return response.text().then((_responseText) => {
380
+ let result200 = null;
381
+ result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
382
+ return result200;
383
+ });
384
+ }
385
+ else if (status !== 200 && status !== 204) {
386
+ return response.text().then((_responseText) => {
387
+ return throwException("An unexpected server error occurred.", status, _responseText, _headers);
388
+ });
389
+ }
390
+ return Promise.resolve(null);
391
+ }
392
+ /**
393
+ * 发送消息
394
+ */
395
+ sessionSendMessage(input) {
396
+ let url_ = this.baseUrl + "/robots/session/sendMessage";
397
+ url_ = url_.replace(/[?&]$/, "");
398
+ const content_ = JSON.stringify(input);
399
+ let options_ = {
400
+ body: content_,
401
+ method: "POST",
402
+ headers: {
403
+ "Content-Type": "application/json",
404
+ "Accept": "application/json"
405
+ }
406
+ };
407
+ return this.http.fetch(url_, options_).then((_response) => {
408
+ return this.processSessionSendMessage(_response);
409
+ });
410
+ }
411
+ processSessionSendMessage(response) {
412
+ const status = response.status;
413
+ let _headers = {};
414
+ if (response.headers && response.headers.forEach) {
415
+ response.headers.forEach((v, k) => _headers[k] = v);
416
+ }
417
+ if (status === 200) {
418
+ return response.text().then((_responseText) => {
419
+ let result200 = null;
420
+ result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
421
+ return result200;
422
+ });
423
+ }
424
+ else if (status !== 200 && status !== 204) {
425
+ return response.text().then((_responseText) => {
426
+ return throwException("An unexpected server error occurred.", status, _responseText, _headers);
427
+ });
428
+ }
429
+ return Promise.resolve(null);
430
+ }
431
+ /**
432
+ * 获取用户
433
+ */
434
+ userGet(input) {
435
+ let url_ = this.baseUrl + "/robots/user/get";
436
+ url_ = url_.replace(/[?&]$/, "");
437
+ const content_ = JSON.stringify(input);
438
+ let options_ = {
439
+ body: content_,
440
+ method: "POST",
441
+ headers: {
442
+ "Content-Type": "application/json",
443
+ "Accept": "application/json"
444
+ }
445
+ };
446
+ return this.http.fetch(url_, options_).then((_response) => {
447
+ return this.processUserGet(_response);
448
+ });
449
+ }
450
+ processUserGet(response) {
451
+ const status = response.status;
452
+ let _headers = {};
453
+ if (response.headers && response.headers.forEach) {
454
+ response.headers.forEach((v, k) => _headers[k] = v);
455
+ }
456
+ if (status === 200) {
457
+ return response.text().then((_responseText) => {
458
+ let result200 = null;
459
+ result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
460
+ return result200;
461
+ });
462
+ }
463
+ else if (status !== 200 && status !== 204) {
464
+ return response.text().then((_responseText) => {
465
+ return throwException("An unexpected server error occurred.", status, _responseText, _headers);
466
+ });
467
+ }
468
+ return Promise.resolve(null);
469
+ }
470
+ }
471
+ class ApiException extends Error {
472
+ constructor(message, status, response, headers, result) {
473
+ super();
474
+ this.isApiException = true;
475
+ this.message = message;
476
+ this.status = status;
477
+ this.response = response;
478
+ this.headers = headers;
479
+ this.result = result;
480
+ }
481
+ static isApiException(obj) {
482
+ return obj.isApiException === true;
483
+ }
484
+ }
485
+ function throwException(message, status, response, headers, result) {
486
+ throw new ApiException(message, status, response, headers, null);
487
+ }
488
+
489
+ // ============================================================================
490
+ // 认证 Token(与 SignalR 保持一致)
491
+ // ============================================================================
492
+ function buildToken(botId, secret) {
493
+ const timestamp = Math.floor(Date.now() / 1000).toString();
494
+ const signature = node_crypto.createHmac("sha256", secret)
495
+ .update(`${botId}:${timestamp}`)
496
+ .digest("hex");
497
+ return Buffer.from(`${botId}:${timestamp}:${signature}`).toString("base64");
498
+ }
499
+ // ============================================================================
500
+ // ReplyPayload → MessagePayloadBase 转换
501
+ // ============================================================================
502
+ function toFeiFeiPayload(payload) {
503
+ // 优先图片
504
+ const imageUrl = payload.mediaUrl ?? payload.mediaUrls?.[0];
505
+ if (imageUrl) {
506
+ return { type: "Image", url: imageUrl };
507
+ }
508
+ // 文本(含 markdown,统一当文本发送)
509
+ const textPayload = {
510
+ type: "Markdown",
511
+ markdown: payload.text ?? "",
512
+ };
513
+ return textPayload;
514
+ }
515
+ function createFeiFeiApi(apiUrl, botId, secret) {
516
+ const client = new ApiClient(apiUrl, {
517
+ fetch: (url, init) => {
518
+ const urlWithRobotId = new URL(url);
519
+ urlWithRobotId.searchParams.set("robot_id", botId);
520
+ const headers = new Headers(init?.headers);
521
+ headers.set("Authorization", `Bearer ${buildToken(botId, secret)}`);
522
+ return globalThis.fetch(urlWithRobotId.toString(), { ...init, headers });
523
+ },
524
+ });
525
+ return {
526
+ async sendMessage(sessionType, sessionId, payload) {
527
+ const feifeiPayload = toFeiFeiPayload(payload);
528
+ switch (sessionType) {
529
+ case "Session":
530
+ return client.sessionSendMessage({ sessionId, payload: feifeiPayload });
531
+ case "AISession":
532
+ return client.aiSessionSendMessage({ sessionId, payload: feifeiPayload });
533
+ case "BotSession":
534
+ return client.botSessionSendMessage({ sessionId, payload: feifeiPayload });
535
+ default:
536
+ throw new Error(`Unsupported session type: ${sessionType}`);
537
+ }
538
+ },
539
+ };
540
+ }
541
+
542
+ let _api = null;
543
+ function resolveAccount(cfg) {
544
+ const ch = (cfg.channels?.[CHANNEL_ID] ?? {});
545
+ return {
546
+ accountId: DEFAULT_ACCOUNT_ID,
547
+ hubUrl: ch.hubUrl ?? DEFAULT_HUB_URL,
548
+ apiUrl: ch.apiUrl ?? DEFAULT_API_URL,
549
+ botId: ch.botId ?? "",
550
+ secret: ch.secret ?? "",
551
+ enabled: ch.enabled !== false,
552
+ };
553
+ }
554
+ const feifeiPlugin = {
555
+ id: CHANNEL_ID,
556
+ meta: {
557
+ id: CHANNEL_ID,
558
+ label: "飞飞 IM",
559
+ selectionLabel: "飞飞 IM (FeiFei)",
560
+ detailLabel: "飞飞 IM 插件",
561
+ docsPath: `/channels/${CHANNEL_ID}`,
562
+ docsLabel: CHANNEL_ID,
563
+ blurb: "飞飞 IM OpenClaw 插件",
564
+ quickstartAllowFrom: true,
565
+ },
566
+ pairing: {
567
+ idLabel: "userId",
568
+ normalizeAllowEntry: (e) => e.replace(new RegExp(`^${CHANNEL_ID}:`), "").trim(),
569
+ notifyApproval: async () => { },
570
+ },
571
+ capabilities: {
572
+ chatTypes: ["direct"],
573
+ reactions: false,
574
+ threads: false,
575
+ media: false,
576
+ nativeCommands: false,
577
+ blockStreaming: false,
578
+ },
579
+ reload: { configPrefixes: [`channels.${CHANNEL_ID}`] },
580
+ config: {
581
+ listAccountIds: () => [DEFAULT_ACCOUNT_ID],
582
+ resolveAccount: (cfg) => resolveAccount(cfg),
583
+ defaultAccountId: () => DEFAULT_ACCOUNT_ID,
584
+ setAccountEnabled: ({ cfg, enabled }) => {
585
+ const ch = (cfg.channels?.[CHANNEL_ID] ?? {});
586
+ return { ...cfg, channels: { ...cfg.channels, [CHANNEL_ID]: { ...ch, enabled } } };
587
+ },
588
+ deleteAccount: ({ cfg }) => {
589
+ const next = { ...(cfg.channels ?? {}) };
590
+ delete next[CHANNEL_ID];
591
+ return { ...cfg, channels: next };
592
+ },
593
+ isConfigured: (a) => Boolean(a.botId?.trim() && a.secret?.trim()),
594
+ describeAccount: (a) => ({
595
+ accountId: a.accountId,
596
+ enabled: a.enabled,
597
+ configured: Boolean(a.botId?.trim() && a.secret?.trim()),
598
+ hubUrl: a.hubUrl,
599
+ botId: a.botId,
600
+ }),
601
+ resolveAllowFrom: () => [],
602
+ formatAllowFrom: ({ allowFrom }) => allowFrom.map(String).filter(Boolean),
603
+ },
604
+ security: {
605
+ resolveDmPolicy: () => ({
606
+ policy: "open",
607
+ allowFrom: ["*"],
608
+ allowFromPath: `channels.${CHANNEL_ID}.`,
609
+ approveHint: `openclaw pairing approve ${CHANNEL_ID} <code>`,
610
+ }),
611
+ collectWarnings: () => [],
612
+ },
613
+ messaging: {
614
+ normalizeTarget: (t) => t.trim() || undefined,
615
+ targetResolver: { looksLikeId: (id) => Boolean(id?.trim()), hint: "<userId>" },
616
+ },
617
+ directory: {
618
+ self: async () => null,
619
+ listPeers: async () => [],
620
+ listGroups: async () => [],
621
+ },
622
+ outbound: {
623
+ deliveryMode: "gateway",
624
+ sendText: async ({ to, text }) => {
625
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
626
+ const runtime = getFeiFeiRuntime();
627
+ const sessionId = to.replace(new RegExp(`^${CHANNEL_ID}:`), "");
628
+ const sessionType = getSessionType(sessionId);
629
+ runtime.log?.(`[feifei] → proactive sessionId=${sessionId} text=${(text ?? "").slice(0, 60)}`);
630
+ let messageId = `p-${Date.now()}`;
631
+ if (_api && sessionType) {
632
+ const result = await _api.sendMessage(sessionType, Number(sessionId), { text });
633
+ messageId = String(result.messageId);
634
+ }
635
+ else {
636
+ runtime.error?.(`[feifei] proactive: sessionId=${sessionId} not found in registry`);
637
+ }
638
+ return { channel: CHANNEL_ID, messageId, chatId: sessionId };
639
+ },
640
+ sendFormattedMedia: async ({ to, text, mediaUrl }) => {
641
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
642
+ const runtime = getFeiFeiRuntime();
643
+ const sessionId = to.replace(new RegExp(`^${CHANNEL_ID}:`), "");
644
+ const sessionType = getSessionType(sessionId);
645
+ runtime.log?.(`[feifei] → proactive media sessionId=${sessionId} url=${mediaUrl ?? ""}`);
646
+ let messageId = `p-${Date.now()}`;
647
+ if (_api && sessionType) {
648
+ const result = await _api.sendMessage(sessionType, Number(sessionId), { text, mediaUrl });
649
+ messageId = String(result.messageId);
650
+ }
651
+ else {
652
+ runtime.error?.(`[feifei] proactive media: sessionId=${sessionId} not found in registry`);
653
+ }
654
+ return { channel: CHANNEL_ID, messageId, chatId: sessionId };
655
+ },
656
+ },
657
+ status: {
658
+ defaultRuntime: { accountId: DEFAULT_ACCOUNT_ID, running: false, lastStartAt: null, lastStopAt: null, lastError: null },
659
+ collectStatusIssues: () => [],
660
+ buildChannelSummary: ({ snapshot }) => ({
661
+ configured: true,
662
+ running: snapshot.running ?? false,
663
+ lastStartAt: snapshot.lastStartAt ?? null,
664
+ lastStopAt: snapshot.lastStopAt ?? null,
665
+ lastError: snapshot.lastError ?? null,
666
+ }),
667
+ probeAccount: async () => ({ ok: true, status: 200 }),
668
+ buildAccountSnapshot: ({ account, runtime }) => ({
669
+ accountId: account.accountId,
670
+ enabled: account.enabled,
671
+ configured: Boolean(account.botId?.trim() && account.secret?.trim()),
672
+ running: runtime?.running ?? false,
673
+ lastStartAt: runtime?.lastStartAt ?? null,
674
+ lastStopAt: runtime?.lastStopAt ?? null,
675
+ lastError: runtime?.lastError ?? null,
676
+ }),
677
+ },
678
+ gateway: {
679
+ startAccount: async (ctx) => {
680
+ const { account, cfg, abortSignal, runtime } = ctx;
681
+ _api = createFeiFeiApi(account.apiUrl, account.botId, account.secret);
682
+ await monitorFeiFei({
683
+ hubUrl: account.hubUrl,
684
+ botId: account.botId,
685
+ secret: account.secret,
686
+ accountId: account.accountId,
687
+ cfg,
688
+ abortSignal,
689
+ runtime,
690
+ }, _api);
691
+ },
692
+ logoutAccount: async () => ({ cleared: false, envToken: false, loggedOut: true }),
693
+ },
694
+ };
695
+
696
+ const plugin = {
697
+ id: "feifei-openclaw-plugin",
698
+ name: "飞飞 IM",
699
+ description: "飞飞 IM OpenClaw 插件",
700
+ configSchema: {
701
+ type: "object",
702
+ additionalProperties: false,
703
+ properties: {},
704
+ },
705
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
706
+ register(api) {
707
+ setFeiFeiRuntime(api.runtime);
708
+ api.registerChannel({ plugin: feifeiPlugin });
709
+ },
710
+ };
711
+
712
+ exports.default = plugin;
713
+ //# sourceMappingURL=index.cjs.js.map