lightclawbot 1.0.6 → 1.0.8

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.
Files changed (61) hide show
  1. package/README.md +47 -213
  2. package/dist/src/config.d.ts +8 -8
  3. package/dist/src/config.d.ts.map +1 -1
  4. package/dist/src/config.js +9 -9
  5. package/dist/src/config.js.map +1 -1
  6. package/dist/src/dedup.d.ts +6 -1
  7. package/dist/src/dedup.d.ts.map +1 -1
  8. package/dist/src/dedup.js +27 -14
  9. package/dist/src/dedup.js.map +1 -1
  10. package/dist/src/download-tool.d.ts +1 -1
  11. package/dist/src/download-tool.js +9 -9
  12. package/dist/src/download-tool.js.map +1 -1
  13. package/dist/src/file-storage.d.ts +9 -9
  14. package/dist/src/file-storage.d.ts.map +1 -1
  15. package/dist/src/file-storage.js +10 -10
  16. package/dist/src/file-storage.js.map +1 -1
  17. package/dist/src/format-urls.d.ts +5 -5
  18. package/dist/src/format-urls.js +8 -8
  19. package/dist/src/format-urls.js.map +1 -1
  20. package/dist/src/gateway.d.ts.map +1 -1
  21. package/dist/src/gateway.js +11 -8
  22. package/dist/src/gateway.js.map +1 -1
  23. package/dist/src/history/text-processing.d.ts.map +1 -1
  24. package/dist/src/history/text-processing.js +2 -0
  25. package/dist/src/history/text-processing.js.map +1 -1
  26. package/dist/src/inbound.d.ts.map +1 -1
  27. package/dist/src/inbound.js +18 -19
  28. package/dist/src/inbound.js.map +1 -1
  29. package/dist/src/socket/handlers.d.ts +26 -0
  30. package/dist/src/socket/handlers.d.ts.map +1 -0
  31. package/dist/src/socket/handlers.js +130 -0
  32. package/dist/src/socket/handlers.js.map +1 -0
  33. package/dist/src/socket/index.d.ts +11 -0
  34. package/dist/src/socket/index.d.ts.map +1 -0
  35. package/dist/src/socket/index.js +9 -0
  36. package/dist/src/socket/index.js.map +1 -0
  37. package/dist/src/socket/registry.d.ts +59 -0
  38. package/dist/src/socket/registry.d.ts.map +1 -0
  39. package/dist/src/socket/registry.js +125 -0
  40. package/dist/src/socket/registry.js.map +1 -0
  41. package/dist/src/socket/reliable-emitter.d.ts +79 -0
  42. package/dist/src/socket/reliable-emitter.d.ts.map +1 -0
  43. package/dist/src/socket/reliable-emitter.js +213 -0
  44. package/dist/src/socket/reliable-emitter.js.map +1 -0
  45. package/dist/src/socket-handlers.d.ts.map +1 -1
  46. package/dist/src/socket-handlers.js +45 -45
  47. package/dist/src/socket-handlers.js.map +1 -1
  48. package/dist/src/types.d.ts +2 -2
  49. package/dist/src/types.d.ts.map +1 -1
  50. package/dist/src/upload-tool.d.ts +1 -1
  51. package/dist/src/upload-tool.js +3 -3
  52. package/dist/src/upload-tool.js.map +1 -1
  53. package/package.json +1 -1
  54. package/skills/lightclaw-media/SKILL.md +22 -102
  55. package/dist/public/data/scripts/manifest.json +0 -11
  56. package/dist/public/data/scripts/preflight.78097a58.sh +0 -94
  57. package/dist/public/data/scripts/preflight.sh +0 -94
  58. package/dist/src/session-history.d.ts +0 -88
  59. package/dist/src/session-history.d.ts.map +0 -1
  60. package/dist/src/session-history.js +0 -598
  61. package/dist/src/session-history.js.map +0 -1
@@ -0,0 +1,130 @@
1
+ /**
2
+ * LightClaw — Socket.IO 事件处理器
3
+ *
4
+ * 将 socket 事件监听(message:private、history:request、sessions:request)
5
+ * 从 gateway 主逻辑中解耦。
6
+ *
7
+ * 所有出站 socket.emit 通过 ReliableEmitter 实现 ACK 确认 + 自动重试。
8
+ */
9
+ import { CHANNEL_KEY, EVENT_MESSAGE_PRIVATE, EVENT_HISTORY_REQUEST, EVENT_HISTORY_RESPONSE, EVENT_SESSIONS_REQUEST, EVENT_SESSIONS_RESPONSE, DEFAULT_HISTORY_LIMIT, } from "../config.js";
10
+ import { isDuplicate, debounceHistoryRequest, generateMsgId } from "../dedup.js";
11
+ import { getAssistantRuntime } from "../runtime.js";
12
+ import { readSessionHistoryWithCron, listSessions } from "../history/index.js";
13
+ /**
14
+ * 绑定所有 Socket.IO 事件监听器
15
+ */
16
+ export function bindSocketHandlers(socket, deps) {
17
+ const { account, botClientId, log, enqueueMessage, onEvent, reliableEmitter } = deps;
18
+ // ---- 接收用户消息 ----
19
+ socket.on(EVENT_MESSAGE_PRIVATE, (data, ack) => {
20
+ ack?.();
21
+ log?.info(`[${CHANNEL_KEY}] Received private message: ${JSON.stringify(data)},botClientId:${botClientId}`);
22
+ // 回环防御:过滤自身发出的消息
23
+ if (data.from === botClientId)
24
+ return;
25
+ // 跳过控制消息(typing / stream 信号),只处理真实用户消息
26
+ if (data.kind && data.kind !== "text")
27
+ return;
28
+ // 无内容跳过
29
+ const hasContent = data.content?.trim();
30
+ const hasFiles = data.files && data.files.length > 0;
31
+ if (!hasContent && !hasFiles)
32
+ return;
33
+ // 去重
34
+ if (isDuplicate(data.msgId))
35
+ return;
36
+ // 通知框架收到入站事件(更新 lastEventAt,防止 stale-socket 误判)
37
+ onEvent?.();
38
+ log?.info(`[${CHANNEL_KEY}] Message from ${data.from}: "${(data.content || "").slice(0, 60)}" files=${data.files?.length ?? 0}`);
39
+ enqueueMessage({
40
+ senderId: data.from,
41
+ text: data.content || "",
42
+ messageId: data.msgId,
43
+ files: data.files ?? [],
44
+ timestamp: data.timestamp,
45
+ });
46
+ });
47
+ // ---- 历史消息请求 ----
48
+ socket.on(EVENT_HISTORY_REQUEST, (data, ack) => {
49
+ ack?.();
50
+ if (!data?.from) {
51
+ log?.warn(`[${CHANNEL_KEY}] History request missing userId, ignoring`);
52
+ return;
53
+ }
54
+ // 回环防御:忽略 bot 自身发出的请求
55
+ if (data.from === botClientId)
56
+ return;
57
+ // 去重:如果有 msgId,走消息级去重
58
+ if (data.msgId && isDuplicate(data.msgId)) {
59
+ log?.warn(`[${CHANNEL_KEY}] Duplicate history request (msgId), ignoring`);
60
+ return;
61
+ }
62
+ // 防抖:同一用户高频请求只处理最后一条
63
+ debounceHistoryRequest(data.from, () => {
64
+ // 通知框架收到入站事件(更新 lastEventAt,防止 stale-socket 误判)
65
+ onEvent?.();
66
+ try {
67
+ const pluginRuntime = getAssistantRuntime();
68
+ const currentCfg = pluginRuntime.config.loadConfig();
69
+ const route = pluginRuntime.channel.routing.resolveAgentRoute({
70
+ cfg: currentCfg,
71
+ channel: CHANNEL_KEY,
72
+ accountId: account.accountId,
73
+ peer: { kind: "direct", id: data.from },
74
+ });
75
+ const sessionKey = route?.sessionKey ?? `${CHANNEL_KEY}:dm:${data.from}`;
76
+ const messages = readSessionHistoryWithCron(sessionKey, {
77
+ limit: data.limit ?? DEFAULT_HISTORY_LIMIT,
78
+ chatOnly: data.chatOnly ?? true,
79
+ includeCron: true,
80
+ });
81
+ log?.info(`[${CHANNEL_KEY}] History request: userId=${data.from} sessionKey=${sessionKey} found=${messages.length}`);
82
+ const historyMsgId = generateMsgId();
83
+ reliableEmitter.emitWithAck(EVENT_HISTORY_RESPONSE, {
84
+ msgId: historyMsgId,
85
+ from: botClientId,
86
+ to: data.from,
87
+ sessionKey,
88
+ messages: messages.filter(msg => !!msg.content.trim() || (msg.files && msg.files.length > 0)),
89
+ }, historyMsgId);
90
+ }
91
+ catch (err) {
92
+ log?.error(`[${CHANNEL_KEY}] History request error: ${err}`);
93
+ const errorMsgId = generateMsgId();
94
+ reliableEmitter.emitWithAck(EVENT_HISTORY_RESPONSE, {
95
+ msgId: errorMsgId,
96
+ from: botClientId,
97
+ to: data.from,
98
+ sessionKey: "",
99
+ messages: [],
100
+ error: err instanceof Error ? err.message : String(err),
101
+ }, errorMsgId);
102
+ }
103
+ });
104
+ });
105
+ // ---- 会话列表请求 ----
106
+ socket.on(EVENT_SESSIONS_REQUEST, (data) => {
107
+ // 通知框架收到入站事件(更新 lastEventAt,防止 stale-socket 误判)
108
+ onEvent?.();
109
+ try {
110
+ const sessions = listSessions();
111
+ const sessionsMsgId = generateMsgId();
112
+ reliableEmitter.emitWithAck(EVENT_SESSIONS_RESPONSE, {
113
+ requestId: data.requestId,
114
+ sessions,
115
+ msgId: sessionsMsgId,
116
+ }, sessionsMsgId);
117
+ }
118
+ catch (err) {
119
+ log?.error(`[${CHANNEL_KEY}] Sessions list error: ${err}`);
120
+ const sessionsErrMsgId = generateMsgId();
121
+ reliableEmitter.emitWithAck(EVENT_SESSIONS_RESPONSE, {
122
+ requestId: data.requestId,
123
+ sessions: [],
124
+ error: err instanceof Error ? err.message : String(err),
125
+ msgId: sessionsErrMsgId,
126
+ }, sessionsErrMsgId);
127
+ }
128
+ });
129
+ }
130
+ //# sourceMappingURL=handlers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handlers.js","sourceRoot":"","sources":["../../../src/socket/handlers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * LightClaw — Socket 模块统一入口
3
+ *
4
+ * 收敛所有 socket 相关逻辑:事件处理器、注册表、可靠发送。
5
+ */
6
+ export { bindSocketHandlers } from "./handlers.js";
7
+ export type { SocketHandlerDeps } from "./handlers.js";
8
+ export { registerSocket, unregisterSocket, getSocket, hasEntry, getBotClientId, getReliableEmitter, bufferMessage, flushPendingMessages, getPendingCount, } from "./registry.js";
9
+ export { ReliableEmitter } from "./reliable-emitter.js";
10
+ export type { EmitterStats } from "./reliable-emitter.js";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/socket/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,YAAY,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEvD,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,SAAS,EACT,QAAQ,EACR,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACpB,eAAe,GAChB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,YAAY,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * LightClaw — Socket 模块统一入口
3
+ *
4
+ * 收敛所有 socket 相关逻辑:事件处理器、注册表、可靠发送。
5
+ */
6
+ export { bindSocketHandlers } from "./handlers.js";
7
+ export { registerSocket, unregisterSocket, getSocket, hasEntry, getBotClientId, getReliableEmitter, bufferMessage, flushPendingMessages, getPendingCount, } from "./registry.js";
8
+ export { ReliableEmitter } from "./reliable-emitter.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/socket/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAGnD,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,SAAS,EACT,QAAQ,EACR,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACpB,eAAe,GAChB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * LightClaw — Socket 注册表
3
+ *
4
+ * 让 gateway 启动时注册 socket 实例,
5
+ * outbound 在需要发送消息时可以通过 WS 连接直接发送,
6
+ * 无需走 REST API。
7
+ *
8
+ * 断线缓冲:
9
+ * 当 socket 暂时断开(Socket.IO 自动重连中)时,outbound 消息
10
+ * 会被缓冲在 pendingMessages 队列中,重连后自动 flush 发送。
11
+ * 仅在 gateway 彻底销毁(cleanup)时才删除 entry。
12
+ */
13
+ import type { Socket } from "socket.io-client";
14
+ import type { PrivateMessageData } from "../types.js";
15
+ import type { ReliableEmitter } from "./reliable-emitter.js";
16
+ interface SocketEntry {
17
+ socket: Socket;
18
+ botClientId: string;
19
+ /** 断线期间缓冲的待发消息 */
20
+ pendingMessages: PrivateMessageData[];
21
+ /** 可靠发送器(由 gateway 创建后注入) */
22
+ reliableEmitter?: ReliableEmitter;
23
+ }
24
+ /** 注册 socket(gateway 首次连接时调用) */
25
+ export declare function registerSocket(accountId: string, socket: Socket, botClientId: string, reliableEmitter?: ReliableEmitter): void;
26
+ /**
27
+ * 注销 socket(gateway 彻底销毁时调用)。
28
+ * 注意:普通断线重连不应调用此函数,只在 cleanup 时调用。
29
+ */
30
+ export declare function unregisterSocket(accountId: string): void;
31
+ /** 获取可用的 socket(仅在 connected 时返回) */
32
+ export declare function getSocket(accountId: string): Pick<SocketEntry, "socket" | "botClientId"> | undefined;
33
+ /** 检查 account 是否有注册的 entry(不管是否 connected) */
34
+ export declare function hasEntry(accountId: string): boolean;
35
+ /** 获取 botClientId(不管 socket 是否 connected) */
36
+ export declare function getBotClientId(accountId: string): string | undefined;
37
+ /** 获取可靠发送器 */
38
+ export declare function getReliableEmitter(accountId: string): ReliableEmitter | undefined;
39
+ /**
40
+ * 缓冲一条消息(socket 断开期间由 outbound 调用)。
41
+ * 返回 true 表示成功缓冲,false 表示该 account 没有注册的 entry。
42
+ */
43
+ export declare function bufferMessage(accountId: string, message: PrivateMessageData): boolean;
44
+ /**
45
+ * flush 所有缓冲消息(重连成功后由 gateway 调用)。
46
+ * 如果有 ReliableEmitter,走可靠发送;否则 fallback 直接 emit。
47
+ * 返回发送成功 / 失败的计数。
48
+ */
49
+ export declare function flushPendingMessages(accountId: string, log?: {
50
+ info: (msg: string) => void;
51
+ warn: (msg: string) => void;
52
+ }): {
53
+ sent: number;
54
+ failed: number;
55
+ };
56
+ /** 获取缓冲队列长度(调试/监控用) */
57
+ export declare function getPendingCount(accountId: string): number;
58
+ export {};
59
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/socket/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAM7D,UAAU,WAAW;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB;IAClB,eAAe,EAAE,kBAAkB,EAAE,CAAC;IACtC,6BAA6B;IAC7B,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC;AASD,iCAAiC;AACjC,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,eAAe,CAAC,EAAE,eAAe,GAChC,IAAI,CAUN;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAExD;AAMD,qCAAqC;AACrC,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC,WAAW,EAAE,QAAQ,GAAG,aAAa,CAAC,GAAG,SAAS,CAIpG;AAED,8CAA8C;AAC9C,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED,6CAA6C;AAC7C,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEpE;AAED,cAAc;AACd,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAEjF;AAMD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAUrF;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GACjE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAqClC;AAED,uBAAuB;AACvB,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEzD"}
@@ -0,0 +1,125 @@
1
+ /**
2
+ * LightClaw — Socket 注册表
3
+ *
4
+ * 让 gateway 启动时注册 socket 实例,
5
+ * outbound 在需要发送消息时可以通过 WS 连接直接发送,
6
+ * 无需走 REST API。
7
+ *
8
+ * 断线缓冲:
9
+ * 当 socket 暂时断开(Socket.IO 自动重连中)时,outbound 消息
10
+ * 会被缓冲在 pendingMessages 队列中,重连后自动 flush 发送。
11
+ * 仅在 gateway 彻底销毁(cleanup)时才删除 entry。
12
+ */
13
+ import { MAX_PENDING_MESSAGES, EVENT_MESSAGE_PRIVATE } from "../config.js";
14
+ /** accountId → SocketEntry */
15
+ const registry = new Map();
16
+ // ============================================================
17
+ // 注册 / 注销
18
+ // ============================================================
19
+ /** 注册 socket(gateway 首次连接时调用) */
20
+ export function registerSocket(accountId, socket, botClientId, reliableEmitter) {
21
+ const existing = registry.get(accountId);
22
+ if (existing) {
23
+ // 重连场景:更新 socket 引用,保留 pending 队列
24
+ existing.socket = socket;
25
+ existing.botClientId = botClientId;
26
+ existing.reliableEmitter = reliableEmitter;
27
+ }
28
+ else {
29
+ registry.set(accountId, { socket, botClientId, pendingMessages: [], reliableEmitter });
30
+ }
31
+ }
32
+ /**
33
+ * 注销 socket(gateway 彻底销毁时调用)。
34
+ * 注意:普通断线重连不应调用此函数,只在 cleanup 时调用。
35
+ */
36
+ export function unregisterSocket(accountId) {
37
+ registry.delete(accountId);
38
+ }
39
+ // ============================================================
40
+ // 查询
41
+ // ============================================================
42
+ /** 获取可用的 socket(仅在 connected 时返回) */
43
+ export function getSocket(accountId) {
44
+ const entry = registry.get(accountId);
45
+ if (entry && entry.socket.connected)
46
+ return entry;
47
+ return undefined;
48
+ }
49
+ /** 检查 account 是否有注册的 entry(不管是否 connected) */
50
+ export function hasEntry(accountId) {
51
+ return registry.has(accountId);
52
+ }
53
+ /** 获取 botClientId(不管 socket 是否 connected) */
54
+ export function getBotClientId(accountId) {
55
+ return registry.get(accountId)?.botClientId;
56
+ }
57
+ /** 获取可靠发送器 */
58
+ export function getReliableEmitter(accountId) {
59
+ return registry.get(accountId)?.reliableEmitter;
60
+ }
61
+ // ============================================================
62
+ // 断线缓冲
63
+ // ============================================================
64
+ /**
65
+ * 缓冲一条消息(socket 断开期间由 outbound 调用)。
66
+ * 返回 true 表示成功缓冲,false 表示该 account 没有注册的 entry。
67
+ */
68
+ export function bufferMessage(accountId, message) {
69
+ const entry = registry.get(accountId);
70
+ if (!entry)
71
+ return false;
72
+ if (entry.pendingMessages.length >= MAX_PENDING_MESSAGES) {
73
+ // 队列满了,丢弃最早的消息
74
+ entry.pendingMessages.shift();
75
+ }
76
+ entry.pendingMessages.push(message);
77
+ return true;
78
+ }
79
+ /**
80
+ * flush 所有缓冲消息(重连成功后由 gateway 调用)。
81
+ * 如果有 ReliableEmitter,走可靠发送;否则 fallback 直接 emit。
82
+ * 返回发送成功 / 失败的计数。
83
+ */
84
+ export function flushPendingMessages(accountId, log) {
85
+ const entry = registry.get(accountId);
86
+ if (!entry)
87
+ return { sent: 0, failed: 0 };
88
+ const pending = entry.pendingMessages.splice(0); // 取出全部并清空
89
+ if (pending.length === 0)
90
+ return { sent: 0, failed: 0 };
91
+ let sent = 0;
92
+ let failed = 0;
93
+ for (const msg of pending) {
94
+ if (!entry.socket.connected) {
95
+ // socket 又断了,把剩余消息放回去
96
+ entry.pendingMessages.unshift(...pending.slice(sent + failed));
97
+ break;
98
+ }
99
+ if (entry.reliableEmitter) {
100
+ // 通过可靠发送器发送
101
+ entry.reliableEmitter.emitWithAck(EVENT_MESSAGE_PRIVATE, msg, msg.msgId);
102
+ sent++;
103
+ }
104
+ else {
105
+ // fallback: 直接 emit(不应该走到这里)
106
+ try {
107
+ entry.socket.emit(EVENT_MESSAGE_PRIVATE, msg);
108
+ sent++;
109
+ }
110
+ catch {
111
+ failed++;
112
+ log?.warn(`[socket-registry] Failed to flush buffered message: msgId=${msg.msgId}`);
113
+ }
114
+ }
115
+ }
116
+ if (sent > 0 || failed > 0) {
117
+ log?.info(`[socket-registry] Flushed pending messages: sent=${sent}, failed=${failed}`);
118
+ }
119
+ return { sent, failed };
120
+ }
121
+ /** 获取缓冲队列长度(调试/监控用) */
122
+ export function getPendingCount(accountId) {
123
+ return registry.get(accountId)?.pendingMessages.length ?? 0;
124
+ }
125
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../src/socket/registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * LightClaw — 可靠发送器
3
+ *
4
+ * 所有出站 socket.emit 都通过此模块,利用 Socket.IO 原生 ACK + timeout 机制:
5
+ * socket.timeout(ms).emit(event, data, (err, ...args) => { ... })
6
+ * 超时由 Socket.IO 原生处理,回调第一个参数为 Error(超时)或 null(成功)。
7
+ *
8
+ * 未收到 ACK 则指数退避重试,断线期间暂停重试、重连后立即重发。
9
+ */
10
+ import type { Socket } from "socket.io-client";
11
+ export interface EmitterStats {
12
+ totalEmitted: number;
13
+ totalConfirmed: number;
14
+ totalRetries: number;
15
+ totalFailed: number;
16
+ currentPending: number;
17
+ }
18
+ type LogFn = {
19
+ info: (msg: string) => void;
20
+ warn: (msg: string) => void;
21
+ error: (msg: string) => void;
22
+ };
23
+ export declare class ReliableEmitter {
24
+ private getSocket;
25
+ private log?;
26
+ private pending;
27
+ private paused;
28
+ private idCounter;
29
+ private stats;
30
+ constructor(getSocket: () => Socket | null, log?: LogFn | undefined);
31
+ /**
32
+ * 可靠发送 — 带 ACK 确认 + 自动重试。
33
+ *
34
+ * 每次调用都是独立的发送请求,不会因相同 msgId 被合并。
35
+ * msgId 仅用于日志追踪,内部使用自增 emitId 作为 pending key。
36
+ *
37
+ * @param event Socket.IO 事件名
38
+ * @param data 消息体
39
+ * @param msgId 业务层消息 ID(仅用于日志追踪,可选)
40
+ * @returns true = server 已确认, false = 重试耗尽未确认
41
+ */
42
+ emitWithAck(event: string, data: unknown, msgId?: string): Promise<boolean>;
43
+ /**
44
+ * 断线时调用 — 暂停所有重试计时器
45
+ */
46
+ pause(): void;
47
+ /**
48
+ * 重连时调用 — 立即重发所有待确认消息
49
+ */
50
+ resume(): void;
51
+ /**
52
+ * 销毁 — 清除所有定时器,resolve 所有 pending 为 false
53
+ */
54
+ destroy(): void;
55
+ /** 当前待确认消息数 */
56
+ get pendingCount(): number;
57
+ /** 获取统计信息 */
58
+ getStats(): EmitterStats;
59
+ /**
60
+ * 通过 socket.timeout(ms).emit(event, data, callback) 发送消息。
61
+ *
62
+ * - 超时由 Socket.IO 原生 .timeout() 处理,无需手动 setTimeout
63
+ * - 回调签名为 (err: Error | null, ...args),err 非空说明超时或出错
64
+ * - socket.emit 是异步的,不会抛同步异常,无需 try-catch
65
+ */
66
+ private doEmit;
67
+ private confirm;
68
+ private scheduleRetry;
69
+ /**
70
+ * 指数退避 + 随机抖动
71
+ * delay = min(base * 2^(retryCount-1) + jitter, maxDelay)
72
+ */
73
+ private getRetryDelay;
74
+ /** 队列满时淘汰最早的 pending 消息 */
75
+ private evictIfNeeded;
76
+ private generateEmitId;
77
+ }
78
+ export {};
79
+ //# sourceMappingURL=reliable-emitter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reliable-emitter.d.ts","sourceRoot":"","sources":["../../../src/socket/reliable-emitter.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAwB/C,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,KAAK,KAAK,GAAG;IACX,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B,CAAC;AAMF,qBAAa,eAAe;IAcxB,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,GAAG,CAAC;IAdd,OAAO,CAAC,OAAO,CAAqC;IACpD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAK;IAGtB,OAAO,CAAC,KAAK,CAKX;gBAGQ,SAAS,EAAE,MAAM,MAAM,GAAG,IAAI,EAC9B,GAAG,CAAC,EAAE,KAAK,YAAA;IAOrB;;;;;;;;;;OAUG;IACH,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA+B3E;;OAEG;IACH,KAAK,IAAI,IAAI;IAYb;;OAEG;IACH,MAAM,IAAI,IAAI;IASd;;OAEG;IACH,OAAO,IAAI,IAAI;IASf,eAAe;IACf,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,aAAa;IACb,QAAQ,IAAI,YAAY;IAQxB;;;;;;OAMG;IACH,OAAO,CAAC,MAAM;IAuBd,OAAO,CAAC,OAAO;IAaf,OAAO,CAAC,aAAa;IA4BrB;;;OAGG;IACH,OAAO,CAAC,aAAa;IAMrB,2BAA2B;IAC3B,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,cAAc;CAGvB"}
@@ -0,0 +1,213 @@
1
+ /**
2
+ * LightClaw — 可靠发送器
3
+ *
4
+ * 所有出站 socket.emit 都通过此模块,利用 Socket.IO 原生 ACK + timeout 机制:
5
+ * socket.timeout(ms).emit(event, data, (err, ...args) => { ... })
6
+ * 超时由 Socket.IO 原生处理,回调第一个参数为 Error(超时)或 null(成功)。
7
+ *
8
+ * 未收到 ACK 则指数退避重试,断线期间暂停重试、重连后立即重发。
9
+ */
10
+ import { EMIT_ACK_TIMEOUT, EMIT_MAX_RETRIES, EMIT_RETRY_BASE_DELAY, EMIT_RETRY_MAX_DELAY, EMIT_PENDING_MAX, } from "../config.js";
11
+ // ============================================================
12
+ // ReliableEmitter
13
+ // ============================================================
14
+ export class ReliableEmitter {
15
+ getSocket;
16
+ log;
17
+ pending = new Map();
18
+ paused = false;
19
+ idCounter = 0;
20
+ // 统计
21
+ stats = {
22
+ totalEmitted: 0,
23
+ totalConfirmed: 0,
24
+ totalRetries: 0,
25
+ totalFailed: 0,
26
+ };
27
+ constructor(getSocket, log) {
28
+ this.getSocket = getSocket;
29
+ this.log = log;
30
+ }
31
+ // ----------------------------------------------------------
32
+ // 公开 API
33
+ // ----------------------------------------------------------
34
+ /**
35
+ * 可靠发送 — 带 ACK 确认 + 自动重试。
36
+ *
37
+ * 每次调用都是独立的发送请求,不会因相同 msgId 被合并。
38
+ * msgId 仅用于日志追踪,内部使用自增 emitId 作为 pending key。
39
+ *
40
+ * @param event Socket.IO 事件名
41
+ * @param data 消息体
42
+ * @param msgId 业务层消息 ID(仅用于日志追踪,可选)
43
+ * @returns true = server 已确认, false = 重试耗尽未确认
44
+ */
45
+ emitWithAck(event, data, msgId) {
46
+ // 每次调用生成唯一的内部 ID,确保同一 msgId 的多次 emit 不会互相覆盖
47
+ const emitId = this.generateEmitId();
48
+ this.stats.totalEmitted++;
49
+ // 队列满时淘汰最早的
50
+ this.evictIfNeeded();
51
+ return new Promise((resolve) => {
52
+ const entry = {
53
+ id: emitId,
54
+ msgId,
55
+ event,
56
+ data,
57
+ retryCount: 0,
58
+ createdAt: Date.now(),
59
+ retryTimer: null,
60
+ resolve,
61
+ };
62
+ this.pending.set(emitId, entry);
63
+ if (this.paused) {
64
+ // 断线中,先挂着,resume 时统一重发
65
+ this.log?.info(`[ReliableEmitter] Queued while paused: emitId=${emitId}, msgId=${msgId}`);
66
+ return;
67
+ }
68
+ this.doEmit(entry);
69
+ });
70
+ }
71
+ /**
72
+ * 断线时调用 — 暂停所有重试计时器
73
+ */
74
+ pause() {
75
+ if (this.paused)
76
+ return;
77
+ this.paused = true;
78
+ for (const entry of this.pending.values()) {
79
+ if (entry.retryTimer) {
80
+ clearTimeout(entry.retryTimer);
81
+ entry.retryTimer = null;
82
+ }
83
+ }
84
+ this.log?.info(`[ReliableEmitter] Paused, ${this.pending.size} message(s) pending`);
85
+ }
86
+ /**
87
+ * 重连时调用 — 立即重发所有待确认消息
88
+ */
89
+ resume() {
90
+ if (!this.paused)
91
+ return;
92
+ this.paused = false;
93
+ this.log?.info(`[ReliableEmitter] Resumed, re-emitting ${this.pending.size} pending message(s)`);
94
+ for (const entry of this.pending.values()) {
95
+ this.doEmit(entry);
96
+ }
97
+ }
98
+ /**
99
+ * 销毁 — 清除所有定时器,resolve 所有 pending 为 false
100
+ */
101
+ destroy() {
102
+ for (const entry of this.pending.values()) {
103
+ if (entry.retryTimer)
104
+ clearTimeout(entry.retryTimer);
105
+ entry.resolve(false);
106
+ }
107
+ this.pending.clear();
108
+ this.log?.info(`[ReliableEmitter] Destroyed`);
109
+ }
110
+ /** 当前待确认消息数 */
111
+ get pendingCount() {
112
+ return this.pending.size;
113
+ }
114
+ /** 获取统计信息 */
115
+ getStats() {
116
+ return { ...this.stats, currentPending: this.pending.size };
117
+ }
118
+ // ----------------------------------------------------------
119
+ // 内部方法
120
+ // ----------------------------------------------------------
121
+ /**
122
+ * 通过 socket.timeout(ms).emit(event, data, callback) 发送消息。
123
+ *
124
+ * - 超时由 Socket.IO 原生 .timeout() 处理,无需手动 setTimeout
125
+ * - 回调签名为 (err: Error | null, ...args),err 非空说明超时或出错
126
+ * - socket.emit 是异步的,不会抛同步异常,无需 try-catch
127
+ */
128
+ doEmit(entry) {
129
+ const socket = this.getSocket();
130
+ if (!socket?.connected) {
131
+ // socket 不可用,等 resume 时重发(不计重试次数)
132
+ return;
133
+ }
134
+ socket.timeout(EMIT_ACK_TIMEOUT).emit(entry.event, entry.data, (err) => {
135
+ // 已被 destroy 或 confirm
136
+ if (!this.pending.has(entry.id))
137
+ return;
138
+ if (err) {
139
+ // 超时或服务端回传了错误
140
+ this.log?.warn(`[ReliableEmitter] ACK error: emitId=${entry.id}, msgId=${entry.msgId}, err=${err.message}, retryCount=${entry.retryCount}`);
141
+ this.scheduleRetry(entry);
142
+ }
143
+ else {
144
+ this.log?.info(`[ReliableEmitter] ACK success: emitId=${entry.id}, msgId=${entry.msgId}, retryCount=${entry.retryCount}`);
145
+ // 服务端已确认
146
+ this.confirm(entry.id);
147
+ }
148
+ });
149
+ }
150
+ confirm(id) {
151
+ const entry = this.pending.get(id);
152
+ if (!entry)
153
+ return; // 已被 confirm 或 destroy
154
+ if (entry.retryTimer) {
155
+ clearTimeout(entry.retryTimer);
156
+ entry.retryTimer = null;
157
+ }
158
+ this.pending.delete(id);
159
+ this.stats.totalConfirmed++;
160
+ entry.resolve(true);
161
+ }
162
+ scheduleRetry(entry) {
163
+ // 断线中不重试
164
+ if (this.paused)
165
+ return;
166
+ if (entry.retryCount >= EMIT_MAX_RETRIES) {
167
+ // 重试耗尽
168
+ this.pending.delete(entry.id);
169
+ this.stats.totalFailed++;
170
+ this.log?.error(`[ReliableEmitter] Gave up after ${entry.retryCount} retries: emitId=${entry.id}, msgId=${entry.msgId}, ` +
171
+ `elapsed=${Date.now() - entry.createdAt}ms`);
172
+ entry.resolve(false);
173
+ return;
174
+ }
175
+ entry.retryCount++;
176
+ this.stats.totalRetries++;
177
+ const delay = this.getRetryDelay(entry.retryCount);
178
+ this.log?.info(`[ReliableEmitter] Retry #${entry.retryCount} in ${delay}ms: emitId=${entry.id}, msgId=${entry.msgId}`);
179
+ entry.retryTimer = setTimeout(() => {
180
+ entry.retryTimer = null;
181
+ if (this.paused)
182
+ return; // 重试期间断线了
183
+ this.doEmit(entry);
184
+ }, delay);
185
+ }
186
+ /**
187
+ * 指数退避 + 随机抖动
188
+ * delay = min(base * 2^(retryCount-1) + jitter, maxDelay)
189
+ */
190
+ getRetryDelay(retryCount) {
191
+ const base = EMIT_RETRY_BASE_DELAY * Math.pow(2, retryCount - 1);
192
+ const jitter = Math.random() * 1000;
193
+ return Math.min(base + jitter, EMIT_RETRY_MAX_DELAY);
194
+ }
195
+ /** 队列满时淘汰最早的 pending 消息 */
196
+ evictIfNeeded() {
197
+ while (this.pending.size >= EMIT_PENDING_MAX) {
198
+ const oldest = this.pending.values().next().value;
199
+ if (!oldest)
200
+ break;
201
+ if (oldest.retryTimer)
202
+ clearTimeout(oldest.retryTimer);
203
+ this.pending.delete(oldest.id);
204
+ this.stats.totalFailed++;
205
+ this.log?.warn(`[ReliableEmitter] Evicted oldest pending: emitId=${oldest.id}, msgId=${oldest.msgId}`);
206
+ oldest.resolve(false);
207
+ }
208
+ }
209
+ generateEmitId() {
210
+ return `_re_${Date.now().toString(36)}_${(this.idCounter++).toString(36)}`;
211
+ }
212
+ }
213
+ //# sourceMappingURL=reliable-emitter.js.map