alemonjs 2.1.28 → 2.1.30-rc.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/README_CONFIG.md CHANGED
@@ -4,8 +4,8 @@
4
4
 
5
5
  ```yaml
6
6
  # === 服务器配置 ===
7
- port: 17117 # CBP服务器端口,快捷参数 --port
8
- serverPort: 18110 # 应用服务器端口(可选,仅在需要Web服务时设置)
7
+ port: 17117 # CBP Websocket 端口 (可选,将允许外部客户端互动)
8
+ serverPort: 18110 # Koa 服务器端口(可选,仅在需要Web服务时设置)
9
9
 
10
10
  # === 应用配置 ===
11
11
  input: 'lib/index.js' # 应用入口文件路径,快捷参数 --input
@@ -119,9 +119,9 @@ Markdown.text = (text) => {
119
119
  Markdown.mention = (uid, options) => {
120
120
  return {
121
121
  type: 'MD.mention',
122
- value: uid || '',
122
+ value: uid || 'everyone',
123
123
  options: options ?? {
124
- belong: 'all'
124
+ belong: 'user'
125
125
  }
126
126
  };
127
127
  };
@@ -18,9 +18,101 @@ import '../../app/message-api.js';
18
18
  import '../../app/message-format-old.js';
19
19
  import { apiResolves, apiTimeouts, actionResolves, actionTimeouts, FULL_RECEIVE_HEADER } from '../processor/config.js';
20
20
  import { createWSConnector } from './base.js';
21
+ import { setDirectSend } from '../processor/transport.js';
22
+ import { createDirectServer } from '../../process/direct-channel.js';
21
23
 
24
+ const handleParsedMessage = (parsedMessage) => {
25
+ if (parsedMessage?.apiId) {
26
+ const resolve = apiResolves.get(parsedMessage.apiId);
27
+ if (resolve) {
28
+ apiResolves.delete(parsedMessage.apiId);
29
+ const timeout = apiTimeouts.get(parsedMessage.apiId);
30
+ if (timeout) {
31
+ apiTimeouts.delete(parsedMessage.apiId);
32
+ clearTimeout(timeout);
33
+ }
34
+ if (Array.isArray(parsedMessage.payload)) {
35
+ resolve(parsedMessage.payload);
36
+ }
37
+ else {
38
+ resolve([createResult(ResultCode.Fail, '接口处理错误', null)]);
39
+ }
40
+ }
41
+ }
42
+ else if (parsedMessage?.actionId) {
43
+ const resolve = actionResolves.get(parsedMessage.actionId);
44
+ if (resolve) {
45
+ actionResolves.delete(parsedMessage.actionId);
46
+ const timeout = actionTimeouts.get(parsedMessage.actionId);
47
+ if (timeout) {
48
+ actionTimeouts.delete(parsedMessage.actionId);
49
+ clearTimeout(timeout);
50
+ }
51
+ if (Array.isArray(parsedMessage.payload)) {
52
+ resolve(parsedMessage.payload);
53
+ }
54
+ else {
55
+ resolve([createResult(ResultCode.Fail, '消费处理错误', null)]);
56
+ }
57
+ }
58
+ }
59
+ else if (parsedMessage.name) {
60
+ onProcessor(parsedMessage.name, parsedMessage, parsedMessage.value);
61
+ }
62
+ };
63
+ const cbpClientDirect = (sockPath, open) => {
64
+ createDirectServer(sockPath, (data) => {
65
+ handleParsedMessage(data);
66
+ }).then((channel) => {
67
+ setDirectSend(channel.send);
68
+ open();
69
+ logger.debug({
70
+ code: ResultCode.Ok,
71
+ message: '客户端已启用直连通道模式(Unix Domain Socket)',
72
+ data: null
73
+ });
74
+ }).catch((err) => {
75
+ logger.error({
76
+ code: ResultCode.Fail,
77
+ message: '客户端直连通道建立失败,回退 fork IPC',
78
+ data: err
79
+ });
80
+ cbpClientIPC(open);
81
+ });
82
+ };
83
+ const cbpClientIPC = (open) => {
84
+ process.on('message', (message) => {
85
+ try {
86
+ const msg = typeof message === 'string' ? JSON.parse(message) : message;
87
+ if (msg?.type === 'ipc:data') {
88
+ handleParsedMessage(msg.data);
89
+ }
90
+ }
91
+ catch (error) {
92
+ logger.error({
93
+ code: ResultCode.Fail,
94
+ message: 'IPC 客户端解析消息失败',
95
+ data: error
96
+ });
97
+ }
98
+ });
99
+ open();
100
+ logger.debug({
101
+ code: ResultCode.Ok,
102
+ message: '客户端已启用 IPC 极速通讯模式',
103
+ data: null
104
+ });
105
+ };
22
106
  const cbpClient = (url, options = {}) => {
23
107
  const { open = () => { }, isFullReceive = true } = options;
108
+ if (process.env.__ALEMON_DIRECT_SOCK && typeof process.send === 'function') {
109
+ cbpClientDirect(process.env.__ALEMON_DIRECT_SOCK, open);
110
+ return;
111
+ }
112
+ if (process.env.__ALEMON_IPC === '1' && typeof process.send === 'function') {
113
+ cbpClientIPC(open);
114
+ return;
115
+ }
24
116
  createWSConnector({
25
117
  url,
26
118
  role: 'client',
@@ -41,42 +133,8 @@ const cbpClient = (url, options = {}) => {
41
133
  }
42
134
  }
43
135
  }
44
- else if (parsedMessage?.apiId) {
45
- const resolve = apiResolves.get(parsedMessage.apiId);
46
- if (resolve) {
47
- apiResolves.delete(parsedMessage.apiId);
48
- const timeout = apiTimeouts.get(parsedMessage.apiId);
49
- if (timeout) {
50
- apiTimeouts.delete(parsedMessage.apiId);
51
- clearTimeout(timeout);
52
- }
53
- if (Array.isArray(parsedMessage.payload)) {
54
- resolve(parsedMessage.payload);
55
- }
56
- else {
57
- resolve([createResult(ResultCode.Fail, '接口处理错误', null)]);
58
- }
59
- }
60
- }
61
- else if (parsedMessage?.actionId) {
62
- const resolve = actionResolves.get(parsedMessage.actionId);
63
- if (resolve) {
64
- actionResolves.delete(parsedMessage.actionId);
65
- const timeout = actionTimeouts.get(parsedMessage.actionId);
66
- if (timeout) {
67
- actionTimeouts.delete(parsedMessage.actionId);
68
- clearTimeout(timeout);
69
- }
70
- if (Array.isArray(parsedMessage.payload)) {
71
- resolve(parsedMessage.payload);
72
- }
73
- else {
74
- resolve([createResult(ResultCode.Fail, '消费处理错误', null)]);
75
- }
76
- }
77
- }
78
- else if (parsedMessage.name) {
79
- onProcessor(parsedMessage.name, parsedMessage, parsedMessage.value);
136
+ else {
137
+ handleParsedMessage(parsedMessage);
80
138
  }
81
139
  }
82
140
  catch (error) {
@@ -7,11 +7,165 @@ import 'path';
7
7
  import 'yaml';
8
8
  import '../../core/utils.js';
9
9
  import { createWSConnector } from './base.js';
10
+ import { createDirectClient } from '../../process/direct-channel.js';
10
11
 
12
+ const cbpPlatformDirect = (sockPath, open) => {
13
+ const actionReplys = [];
14
+ const apiReplys = [];
15
+ let channel = null;
16
+ const pendingQueue = [];
17
+ const send = (data) => {
18
+ data.DeviceId = deviceId;
19
+ if (channel) {
20
+ channel.send(data);
21
+ }
22
+ else {
23
+ pendingQueue.push({ ...data });
24
+ }
25
+ };
26
+ const replyAction = (data, payload) => {
27
+ channel?.send({
28
+ action: data.action,
29
+ payload: payload,
30
+ actionId: data.actionId,
31
+ DeviceId: data.DeviceId
32
+ });
33
+ };
34
+ const replyApi = (data, payload) => {
35
+ channel?.send({
36
+ action: data.action,
37
+ apiId: data.apiId,
38
+ DeviceId: data.DeviceId,
39
+ payload: payload
40
+ });
41
+ };
42
+ const onactions = (reply) => {
43
+ actionReplys.push(reply);
44
+ };
45
+ const onapis = (reply) => {
46
+ apiReplys.push(reply);
47
+ };
48
+ createDirectClient(sockPath, (data) => {
49
+ if (data?.apiId) {
50
+ for (const cb of apiReplys) {
51
+ void cb(data, val => replyApi(data, val));
52
+ }
53
+ }
54
+ else if (data?.actionId) {
55
+ for (const cb of actionReplys) {
56
+ void cb(data, val => replyAction(data, val));
57
+ }
58
+ }
59
+ }).then((ch) => {
60
+ channel = ch;
61
+ for (const msg of pendingQueue) {
62
+ channel.send(msg);
63
+ }
64
+ pendingQueue.length = 0;
65
+ open();
66
+ logger.debug({
67
+ code: ResultCode.Ok,
68
+ message: '平台端已启用直连通道模式(Unix Domain Socket)',
69
+ data: null
70
+ });
71
+ }).catch((err) => {
72
+ logger.error({
73
+ code: ResultCode.Fail,
74
+ message: '平台端直连通道建立失败,回退 fork IPC',
75
+ data: err
76
+ });
77
+ cbpPlatformIPC(open, actionReplys, apiReplys);
78
+ });
79
+ return { send, onactions, onapis };
80
+ };
81
+ const cbpPlatformIPC = (open, existingActionReplys, existingApiReplys) => {
82
+ const actionReplys = existingActionReplys ?? [];
83
+ const apiReplys = existingApiReplys ?? [];
84
+ const send = (data) => {
85
+ if (typeof process.send === 'function') {
86
+ data.DeviceId = deviceId;
87
+ process.send({ type: 'ipc:data', data });
88
+ }
89
+ };
90
+ const replyAction = (data, payload) => {
91
+ if (typeof process.send === 'function') {
92
+ process.send({
93
+ type: 'ipc:data',
94
+ data: {
95
+ action: data.action,
96
+ payload: payload,
97
+ actionId: data.actionId,
98
+ DeviceId: data.DeviceId
99
+ }
100
+ });
101
+ }
102
+ };
103
+ const replyApi = (data, payload) => {
104
+ if (typeof process.send === 'function') {
105
+ process.send({
106
+ type: 'ipc:data',
107
+ data: {
108
+ action: data.action,
109
+ apiId: data.apiId,
110
+ DeviceId: data.DeviceId,
111
+ payload: payload
112
+ }
113
+ });
114
+ }
115
+ };
116
+ const onactions = (reply) => {
117
+ actionReplys.push(reply);
118
+ };
119
+ const onapis = (reply) => {
120
+ apiReplys.push(reply);
121
+ };
122
+ process.on('message', (message) => {
123
+ try {
124
+ const msg = typeof message === 'string' ? JSON.parse(message) : message;
125
+ if (msg?.type === 'ipc:data') {
126
+ const data = msg.data;
127
+ if (data?.apiId) {
128
+ for (const cb of apiReplys) {
129
+ void cb(data, val => replyApi(data, val));
130
+ }
131
+ }
132
+ else if (data?.actionId) {
133
+ for (const cb of actionReplys) {
134
+ void cb(data, val => replyAction(data, val));
135
+ }
136
+ }
137
+ }
138
+ }
139
+ catch (error) {
140
+ logger.error({
141
+ code: ResultCode.Fail,
142
+ message: 'IPC 平台端解析消息失败',
143
+ data: error
144
+ });
145
+ }
146
+ });
147
+ open();
148
+ logger.debug({
149
+ code: ResultCode.Ok,
150
+ message: '平台端已启用 IPC 极速通讯模式',
151
+ data: null
152
+ });
153
+ return {
154
+ send,
155
+ onactions,
156
+ onapis
157
+ };
158
+ };
11
159
  const cbpPlatform = (url, options = {
12
160
  open: () => { }
13
161
  }) => {
14
162
  const { open = () => { } } = options;
163
+ if (process.env.__ALEMON_DIRECT_SOCK && typeof process.send === 'function') {
164
+ return cbpPlatformDirect(process.env.__ALEMON_DIRECT_SOCK, open);
165
+ }
166
+ if (process.env.__ALEMON_IPC === '1' && typeof process.send === 'function') {
167
+ return cbpPlatformIPC(open);
168
+ }
15
169
  const send = (data) => {
16
170
  if (global.chatbotPlatform && global.chatbotPlatform.readyState === WebSocket.OPEN) {
17
171
  data.DeviceId = deviceId;
@@ -5,27 +5,42 @@ import 'path';
5
5
  import 'yaml';
6
6
  import { createResult } from '../../core/utils.js';
7
7
  import { generateUniqueId, deviceId, actionResolves, actionTimeouts, timeoutTime } from './config.js';
8
+ import { getDirectSend } from './transport.js';
8
9
 
10
+ const setupActionResolve = (actionId, resolve) => {
11
+ actionResolves.set(actionId, resolve);
12
+ const timeout = setTimeout(() => {
13
+ if (!actionResolves.has(actionId) || !actionTimeouts.has(actionId)) {
14
+ return;
15
+ }
16
+ actionResolves.delete(actionId);
17
+ actionTimeouts.delete(actionId);
18
+ resolve([createResult(ResultCode.Fail, '行为超时', null)]);
19
+ }, timeoutTime);
20
+ actionTimeouts.set(actionId, timeout);
21
+ };
9
22
  const sendAction = (data) => {
10
23
  const actionId = generateUniqueId();
11
24
  return new Promise(resolve => {
25
+ data.actionId = actionId;
26
+ data.DeviceId = deviceId;
27
+ const directSend = getDirectSend();
28
+ if (directSend) {
29
+ directSend(data);
30
+ setupActionResolve(actionId, resolve);
31
+ return;
32
+ }
33
+ if (process.env.__ALEMON_IPC === '1' && typeof process.send === 'function') {
34
+ process.send({ type: 'ipc:data', data });
35
+ setupActionResolve(actionId, resolve);
36
+ return;
37
+ }
12
38
  if (!global.chatbotClient?.send) {
13
39
  resolve([createResult(ResultCode.Fail, 'Chatbot client is not available', null)]);
14
40
  return;
15
41
  }
16
- data.actionId = actionId;
17
- data.DeviceId = deviceId;
18
42
  global.chatbotClient?.send(flattedJSON.stringify(data));
19
- actionResolves.set(actionId, resolve);
20
- const timeout = setTimeout(() => {
21
- if (!actionResolves.has(actionId) || !actionTimeouts.has(actionId)) {
22
- return;
23
- }
24
- actionResolves.delete(actionId);
25
- actionTimeouts.delete(actionId);
26
- resolve([createResult(ResultCode.Fail, '行为超时', null)]);
27
- }, timeoutTime);
28
- actionTimeouts.set(actionId, timeout);
43
+ setupActionResolve(actionId, resolve);
29
44
  });
30
45
  };
31
46
 
@@ -5,27 +5,42 @@ import 'yaml';
5
5
  import { createResult } from '../../core/utils.js';
6
6
  import { generateUniqueId, deviceId, apiResolves, apiTimeouts, timeoutTime } from './config.js';
7
7
  import * as flattedJSON from 'flatted';
8
+ import { getDirectSend } from './transport.js';
8
9
 
10
+ const setupApiResolve = (apiId, resolve) => {
11
+ apiResolves.set(apiId, resolve);
12
+ const timeout = setTimeout(() => {
13
+ if (!apiResolves.has(apiId) || !apiTimeouts.has(apiId)) {
14
+ return;
15
+ }
16
+ apiResolves.delete(apiId);
17
+ apiTimeouts.delete(apiId);
18
+ resolve([createResult(ResultCode.Fail, '接口超时', null)]);
19
+ }, timeoutTime);
20
+ apiTimeouts.set(apiId, timeout);
21
+ };
9
22
  const sendAPI = (data) => {
10
23
  const ApiId = generateUniqueId();
11
24
  return new Promise(resolve => {
25
+ data.apiId = ApiId;
26
+ data.DeviceId = deviceId;
27
+ const directSend = getDirectSend();
28
+ if (directSend) {
29
+ directSend(data);
30
+ setupApiResolve(ApiId, resolve);
31
+ return;
32
+ }
33
+ if (process.env.__ALEMON_IPC === '1' && typeof process.send === 'function') {
34
+ process.send({ type: 'ipc:data', data });
35
+ setupApiResolve(ApiId, resolve);
36
+ return;
37
+ }
12
38
  if (!global.chatbotClient?.send) {
13
39
  resolve([createResult(ResultCode.Fail, 'Chatbot client is not available', null)]);
14
40
  return;
15
41
  }
16
- data.apiId = ApiId;
17
- data.DeviceId = deviceId;
18
42
  global.chatbotClient?.send(flattedJSON.stringify(data));
19
- apiResolves.set(ApiId, resolve);
20
- const timeout = setTimeout(() => {
21
- if (!apiResolves.has(ApiId) || !apiTimeouts.has(ApiId)) {
22
- return;
23
- }
24
- apiResolves.delete(ApiId);
25
- apiTimeouts.delete(ApiId);
26
- resolve([createResult(ResultCode.Fail, '接口超时', null)]);
27
- }, timeoutTime);
28
- apiTimeouts.set(ApiId, timeout);
43
+ setupApiResolve(ApiId, resolve);
29
44
  });
30
45
  };
31
46
 
@@ -17,8 +17,10 @@ const apiResolves = new Map();
17
17
  const actionTimeouts = new Map();
18
18
  const apiTimeouts = new Map();
19
19
  const childrenBind = new Map();
20
+ let _idCounter = 0;
21
+ const _idPrefix = process.pid.toString(36) + Date.now().toString(36);
20
22
  const generateUniqueId = () => {
21
- return Date.now().toString(36) + Math.random().toString(36).substring(2);
23
+ return _idPrefix + (++_idCounter).toString(36);
22
24
  };
23
25
  const timeoutTime = 1000 * 60 * 3;
24
26
  const reconnectInterval = 1000 * 6;
@@ -0,0 +1,4 @@
1
+ type SendFunc = (data: any) => void;
2
+ export declare const setDirectSend: (fn: SendFunc) => void;
3
+ export declare const getDirectSend: () => SendFunc | null;
4
+ export {};
@@ -0,0 +1,7 @@
1
+ let _directSend = null;
2
+ const setDirectSend = (fn) => {
3
+ _directSend = fn;
4
+ };
5
+ const getDirectSend = () => _directSend;
6
+
7
+ export { getDirectSend, setDirectSend };
package/lib/client.js CHANGED
@@ -8,6 +8,9 @@ import { cbpClient } from './cbp/connects/client.js';
8
8
  import 'flatted';
9
9
  import 'ws';
10
10
  import './cbp/processor/config.js';
11
+ import 'net';
12
+ import 'v8';
13
+ import 'os';
11
14
  import 'koa';
12
15
  import '@koa/cors';
13
16
  import './cbp/routers/router.js';
package/lib/core/utils.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import crypto from 'crypto';
2
- import fs, { existsSync, readdirSync } from 'fs';
3
- import path, { join } from 'path';
2
+ import fs__default, { existsSync, readdirSync } from 'fs';
3
+ import path__default, { join } from 'path';
4
4
  import { fileSuffixResponse, ResultCode } from './variable.js';
5
5
  import module from 'module';
6
6
 
@@ -87,8 +87,8 @@ const createExports = (packageJson) => {
87
87
  }
88
88
  };
89
89
  const getInputExportPath = (input) => {
90
- const packageJsonPath = path.join(input ?? process.cwd(), 'package.json');
91
- if (fs.existsSync(packageJsonPath)) {
90
+ const packageJsonPath = path__default.join(input ?? process.cwd(), 'package.json');
91
+ if (fs__default.existsSync(packageJsonPath)) {
92
92
  const packageJson = require(packageJsonPath);
93
93
  const main = packageJson?.main || createExports(packageJson);
94
94
  if (main) {
package/lib/main.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { getConfig } from './core/config.js';
2
2
  import { cbpServer } from './cbp/server/main.js';
3
- import { defaultPort, filePrefixCommon, defaultPlatformCommonPrefix } from './core/variable.js';
3
+ import { filePrefixCommon, defaultPlatformCommonPrefix } from './core/variable.js';
4
4
  import { startPlatformAdapterWithFallback } from './process/platform.js';
5
5
  import { startModuleAdapter } from './process/module.js';
6
+ import { generateSocketPath } from './process/direct-channel.js';
6
7
 
7
8
  const createOptionsByKey = (options, key, defaultValue) => {
8
9
  const cfg = getConfig();
@@ -41,7 +42,7 @@ const startClient = (options) => {
41
42
  process.env.input = createOptionsByKey(options, 'input', '');
42
43
  process.env.output = createOptionsByKey(options, 'output', '');
43
44
  process.env.is_full_receive = String(createOptionsByKey(options, 'is_full_receive', true));
44
- process.env.port = String(createOptionsByKey(options, 'port', defaultPort));
45
+ process.env.port = String(createOptionsByKey(options, 'port', '') || '');
45
46
  process.env.url = createOptionsByKey(options, 'url', '');
46
47
  startModuleAdapter();
47
48
  };
@@ -50,18 +51,27 @@ const start = (options = {}) => {
50
51
  options = { input: options };
51
52
  }
52
53
  global.__options = options;
53
- const port = createOptionsByKey(options, 'port', defaultPort);
54
+ const port = createOptionsByKey(options, 'port', '');
54
55
  const serverPort = createOptionsByKey(options, 'serverPort', '');
55
- process.env.port = port;
56
+ process.env.port = port ? String(port) : '';
56
57
  process.env.serverPort = serverPort;
57
- cbpServer(port, () => {
58
- const httpURL = `http://127.0.0.1:${port}`;
59
- const wsURL = `ws://127.0.0.1:${port}`;
60
- logger.info(`[CBP server started at ${httpURL}]`);
61
- logger.info(`[CBP server started at ${wsURL}]`);
58
+ if (port) {
59
+ cbpServer(port, () => {
60
+ const httpURL = `http://127.0.0.1:${port}`;
61
+ const wsURL = `ws://127.0.0.1:${port}`;
62
+ logger.info(`[CBP server started at ${httpURL}]`);
63
+ logger.info(`[CBP server started at ${wsURL}]`);
64
+ startClient(options);
65
+ startPlatform(options);
66
+ });
67
+ }
68
+ else {
69
+ const sockPath = generateSocketPath();
70
+ process.env.__ALEMON_DIRECT_SOCK = sockPath;
71
+ logger.info('[Direct-IPC mode] 平台↔客户端直连通道,无主进程桥接');
62
72
  startClient(options);
63
73
  startPlatform(options);
64
- });
74
+ }
65
75
  };
66
76
 
67
77
  export { start };
@@ -0,0 +1,7 @@
1
+ export declare const generateSocketPath: () => string;
2
+ export interface DirectChannel {
3
+ send: (data: any) => void;
4
+ close: () => void;
5
+ }
6
+ export declare const createDirectServer: (sockPath: string, onMessage: (data: any) => void) => Promise<DirectChannel>;
7
+ export declare const createDirectClient: (sockPath: string, onMessage: (data: any) => void, maxRetries?: number, retryDelay?: number) => Promise<DirectChannel>;
@@ -0,0 +1,110 @@
1
+ import * as net from 'net';
2
+ import * as v8 from 'v8';
3
+ import * as os from 'os';
4
+ import * as path from 'path';
5
+ import * as fs from 'fs';
6
+
7
+ const generateSocketPath = () => {
8
+ if (process.platform === 'win32') {
9
+ return `\\\\.\\pipe\\alemon-direct-${process.pid}-${Date.now()}`;
10
+ }
11
+ return path.join(os.tmpdir(), `alemon-direct-${process.pid}-${Date.now()}.sock`);
12
+ };
13
+ const encodeMessage = (data) => {
14
+ const serialized = v8.serialize(data);
15
+ const header = Buffer.alloc(4);
16
+ header.writeUInt32BE(serialized.length, 0);
17
+ return Buffer.concat([header, serialized]);
18
+ };
19
+ const createMessageParser = (onMessage) => {
20
+ let buffer = Buffer.alloc(0);
21
+ return (chunk) => {
22
+ buffer = Buffer.concat([buffer, chunk]);
23
+ while (buffer.length >= 4) {
24
+ const msgLen = buffer.readUInt32BE(0);
25
+ if (buffer.length < 4 + msgLen) {
26
+ break;
27
+ }
28
+ const msgBuf = buffer.subarray(4, 4 + msgLen);
29
+ buffer = buffer.subarray(4 + msgLen);
30
+ try {
31
+ onMessage(v8.deserialize(msgBuf));
32
+ }
33
+ catch {
34
+ }
35
+ }
36
+ };
37
+ };
38
+ const createDirectServer = (sockPath, onMessage) => {
39
+ return new Promise((resolve, reject) => {
40
+ let connection = null;
41
+ if (process.platform !== 'win32') {
42
+ try {
43
+ fs.unlinkSync(sockPath);
44
+ }
45
+ catch { }
46
+ }
47
+ const server = net.createServer((socket) => {
48
+ connection = socket;
49
+ const parser = createMessageParser(onMessage);
50
+ socket.on('data', parser);
51
+ socket.on('error', () => { connection = null; });
52
+ socket.on('close', () => { connection = null; });
53
+ });
54
+ const cleanup = () => {
55
+ try {
56
+ server.close();
57
+ connection?.destroy();
58
+ if (process.platform !== 'win32') {
59
+ try {
60
+ fs.unlinkSync(sockPath);
61
+ }
62
+ catch { }
63
+ }
64
+ }
65
+ catch { }
66
+ };
67
+ process.on('exit', cleanup);
68
+ server.listen(sockPath, () => {
69
+ resolve({
70
+ send: (data) => {
71
+ if (connection && !connection.destroyed) {
72
+ connection.write(encodeMessage(data));
73
+ }
74
+ },
75
+ close: cleanup
76
+ });
77
+ });
78
+ server.on('error', reject);
79
+ });
80
+ };
81
+ const createDirectClient = (sockPath, onMessage, maxRetries = 30, retryDelay = 150) => {
82
+ let attempts = 0;
83
+ const tryConnect = () => {
84
+ return new Promise((resolve, reject) => {
85
+ const parser = createMessageParser(onMessage);
86
+ const socket = net.createConnection(sockPath, () => {
87
+ resolve({
88
+ send: (data) => {
89
+ if (!socket.destroyed) {
90
+ socket.write(encodeMessage(data));
91
+ }
92
+ },
93
+ close: () => {
94
+ socket.destroy();
95
+ }
96
+ });
97
+ });
98
+ socket.on('data', parser);
99
+ socket.on('error', reject);
100
+ }).catch((err) => {
101
+ if (++attempts < maxRetries) {
102
+ return new Promise((r) => setTimeout(() => r(tryConnect()), retryDelay));
103
+ }
104
+ throw err;
105
+ });
106
+ };
107
+ return tryConnect();
108
+ };
109
+
110
+ export { createDirectClient, createDirectServer, generateSocketPath };
@@ -1,2 +1,4 @@
1
1
  export * from './module';
2
2
  export * from './platform';
3
+ export * from './ipc-bridge';
4
+ export * from './direct-channel';
@@ -1,2 +1,4 @@
1
1
  export { startModuleAdapter } from './module.js';
2
2
  export { startPlatformAdapterWithFallback } from './platform.js';
3
+ export { forwardFromClient, forwardFromPlatform, getClientChild, getPlatformChild, setClientChild, setPlatformChild } from './ipc-bridge.js';
4
+ export { createDirectClient, createDirectServer, generateSocketPath } from './direct-channel.js';
@@ -0,0 +1,7 @@
1
+ import type { ChildProcess } from 'child_process';
2
+ export declare const setPlatformChild: (child: ChildProcess | null) => void;
3
+ export declare const setClientChild: (child: ChildProcess | null) => void;
4
+ export declare const getPlatformChild: () => ChildProcess;
5
+ export declare const getClientChild: () => ChildProcess;
6
+ export declare const forwardFromPlatform: (data: any) => void;
7
+ export declare const forwardFromClient: (data: any) => void;
@@ -0,0 +1,39 @@
1
+ import { WebSocket } from 'ws';
2
+ import { fullClient } from '../cbp/processor/config.js';
3
+ import * as flattedJSON from 'flatted';
4
+
5
+ let platformChild = null;
6
+ let clientChild = null;
7
+ const setPlatformChild = (child) => {
8
+ platformChild = child;
9
+ };
10
+ const setClientChild = (child) => {
11
+ clientChild = child;
12
+ };
13
+ const getPlatformChild = () => platformChild;
14
+ const getClientChild = () => clientChild;
15
+ const forwardFromPlatform = (data) => {
16
+ if (clientChild?.connected) {
17
+ clientChild.send({ type: 'ipc:data', data });
18
+ }
19
+ try {
20
+ const messageStr = flattedJSON.stringify(data);
21
+ fullClient.forEach((ws, id) => {
22
+ if (ws.readyState === WebSocket.OPEN) {
23
+ ws.send(messageStr);
24
+ }
25
+ else {
26
+ fullClient.delete(id);
27
+ }
28
+ });
29
+ }
30
+ catch {
31
+ }
32
+ };
33
+ const forwardFromClient = (data) => {
34
+ if (platformChild?.connected) {
35
+ platformChild.send({ type: 'ipc:data', data });
36
+ }
37
+ };
38
+
39
+ export { forwardFromClient, forwardFromPlatform, getClientChild, getPlatformChild, setClientChild, setPlatformChild };
@@ -3,6 +3,7 @@ import { ResultCode } from '../core/variable.js';
3
3
  import { getConfigValue } from '../core/config.js';
4
4
  import '../core/utils.js';
5
5
  import module from 'module';
6
+ import { setClientChild, forwardFromClient } from './ipc-bridge.js';
6
7
 
7
8
  const initRequire = () => { };
8
9
  initRequire.resolve = () => '';
@@ -31,14 +32,18 @@ function startModuleAdapter() {
31
32
  restarted: false,
32
33
  ready: false
33
34
  };
34
- const cleanup = () => {
35
+ const clearTimer = () => {
35
36
  if (manager.timer) {
36
37
  clearTimeout(manager.timer);
37
38
  manager.timer = undefined;
38
39
  }
40
+ };
41
+ const cleanup = () => {
42
+ clearTimer();
39
43
  if (manager.child) {
40
44
  manager.child.removeAllListeners();
41
45
  }
46
+ setClientChild(null);
42
47
  };
43
48
  const restart = () => {
44
49
  if (manager.restarted) {
@@ -66,7 +71,9 @@ function startModuleAdapter() {
66
71
  };
67
72
  try {
68
73
  manager.child = childProcess.fork(modulePath, [], {
69
- execArgv: process.execArgv
74
+ execArgv: process.execArgv,
75
+ env: { ...process.env, __ALEMON_IPC: '1' },
76
+ serialization: 'advanced'
70
77
  });
71
78
  manager.timer = setTimeout(checkTimeout, CONFIG.FORK_TIMEOUT);
72
79
  manager.child.on('exit', (code, signal) => {
@@ -83,14 +90,18 @@ function startModuleAdapter() {
83
90
  const data = typeof message === 'string' ? JSON.parse(message) : message;
84
91
  if (data?.type === 'ready') {
85
92
  manager.ready = true;
86
- cleanup();
93
+ clearTimer();
94
+ setClientChild(manager.child);
87
95
  logger?.debug?.({
88
96
  code: ResultCode.Ok,
89
- message: '模块加载已就绪(子进程 fork 模式)',
97
+ message: '模块加载已就绪(IPC)',
90
98
  data: null
91
99
  });
92
100
  manager.child?.send?.({ type: 'start' });
93
101
  }
102
+ else if (data?.type === 'ipc:data') {
103
+ forwardFromClient(data.data);
104
+ }
94
105
  }
95
106
  catch (error) {
96
107
  logger?.error?.({
@@ -3,6 +3,7 @@ import { ResultCode } from '../core/variable.js';
3
3
  import { getConfigValue } from '../core/config.js';
4
4
  import '../core/utils.js';
5
5
  import module from 'module';
6
+ import { setPlatformChild, forwardFromPlatform } from './ipc-bridge.js';
6
7
 
7
8
  const initRequire = () => { };
8
9
  initRequire.resolve = () => '';
@@ -91,6 +92,7 @@ function startPlatformAdapterWithFallback() {
91
92
  if (manager.child) {
92
93
  manager.child.removeAllListeners();
93
94
  }
95
+ setPlatformChild(null);
94
96
  };
95
97
  const restart = () => {
96
98
  if (manager.restarted || imported) {
@@ -110,6 +112,14 @@ function startPlatformAdapterWithFallback() {
110
112
  }
111
113
  isForkFailed = true;
112
114
  cleanup();
115
+ if (!process.env.port) {
116
+ logger?.error?.({
117
+ code: ResultCode.Fail,
118
+ message: 'fork 启动平台连接失败,当前为纯 IPC 模式(未配置 port),无法降级到 WebSocket,已终止平台连接',
119
+ data: error
120
+ });
121
+ return;
122
+ }
113
123
  logger?.warn?.({
114
124
  code: ResultCode.Fail,
115
125
  message: 'fork 启动平台连接失败,将尝试 import 加载',
@@ -135,7 +145,9 @@ function startPlatformAdapterWithFallback() {
135
145
  };
136
146
  try {
137
147
  manager.child = childProcess.fork(modulePath, [], {
138
- execArgv: process.execArgv
148
+ execArgv: process.execArgv,
149
+ env: { ...process.env, __ALEMON_IPC: '1' },
150
+ serialization: 'advanced'
139
151
  });
140
152
  manager.timer = setTimeout(checkTimeout, CONFIG.FORK_TIMEOUT);
141
153
  manager.child.on('exit', (code, signal) => {
@@ -161,13 +173,17 @@ function startPlatformAdapterWithFallback() {
161
173
  clearTimeout(manager.timer);
162
174
  manager.timer = undefined;
163
175
  }
176
+ setPlatformChild(manager.child);
164
177
  logger?.debug?.({
165
178
  code: ResultCode.Ok,
166
- message: '平台连接已就绪(子进程 fork 模式)',
179
+ message: '平台连接已就绪(IPC)',
167
180
  data: null
168
181
  });
169
182
  manager.child?.send?.({ type: 'start' });
170
183
  }
184
+ else if (data?.type === 'ipc:data') {
185
+ forwardFromPlatform(data.data);
186
+ }
171
187
  }
172
188
  catch (error) {
173
189
  logger?.error?.({
@@ -1,5 +1,5 @@
1
1
  import { existsSync } from 'fs';
2
- import path, { dirname } from 'path';
2
+ import path__default, { dirname } from 'path';
3
3
 
4
4
  async function collectMiddlewares(routeFile) {
5
5
  const middlewares = [];
@@ -7,7 +7,7 @@ async function collectMiddlewares(routeFile) {
7
7
  const suffixes = ['.ts', '.js', '.cjs', '.mjs', '.tsx', '.jsx'];
8
8
  while (true) {
9
9
  for (const ext of suffixes) {
10
- const mwPath = path.join(dir, `_middleware${ext}`);
10
+ const mwPath = path__default.join(dir, `_middleware${ext}`);
11
11
  if (existsSync(mwPath)) {
12
12
  const module = await import(`file://${mwPath}`);
13
13
  const mw = module?.default ?? {};
@@ -1,6 +1,6 @@
1
1
  import KoaRouter from 'koa-router';
2
- import fs, { existsSync } from 'fs';
3
- import path, { join, dirname } from 'path';
2
+ import fs__default, { existsSync } from 'fs';
3
+ import path__default, { join, dirname } from 'path';
4
4
  import mime from 'mime-types';
5
5
  import hello from './hello.html.js';
6
6
  import { getModuelFile, formatPath } from './utils.js';
@@ -56,7 +56,7 @@ router.all('app/{*path}', async (ctx) => {
56
56
  const mainDir = dirname(mainPath);
57
57
  try {
58
58
  const dir = join(mainDir, 'route', ctx.path?.replace(apiPath, '/api') || '');
59
- if (existsSync(dir) && fs.statSync(dir).isFile()) {
59
+ if (existsSync(dir) && fs__default.statSync(dir).isFile()) {
60
60
  ctx.status = 404;
61
61
  ctx.body = {
62
62
  code: 404,
@@ -102,7 +102,7 @@ router.all('app/{*path}', async (ctx) => {
102
102
  let root = '';
103
103
  const resourcePath = formatPath(ctx.params?.path);
104
104
  try {
105
- const pkg = require(path.join(rootPath, 'package.json')) ?? {};
105
+ const pkg = require(path__default.join(rootPath, 'package.json')) ?? {};
106
106
  root = pkg.alemonjs?.web?.root ?? '';
107
107
  }
108
108
  catch (err) {
@@ -114,9 +114,9 @@ router.all('app/{*path}', async (ctx) => {
114
114
  };
115
115
  return;
116
116
  }
117
- const fullPath = root ? path.join(rootPath, root, resourcePath) : path.join(rootPath, resourcePath);
117
+ const fullPath = root ? path__default.join(rootPath, root, resourcePath) : path__default.join(rootPath, resourcePath);
118
118
  try {
119
- const file = await fs.promises.readFile(fullPath);
119
+ const file = await fs__default.promises.readFile(fullPath);
120
120
  const mimeType = mime.lookup(fullPath) || 'application/octet-stream';
121
121
  ctx.set('Content-Type', mimeType);
122
122
  ctx.body = file;
@@ -164,7 +164,7 @@ router.all('apps/:app/{*path}', async (ctx) => {
164
164
  mainDirMap.set(appName, mainDir);
165
165
  }
166
166
  const dir = join(mainDirMap.get(appName), 'route', ctx.path?.replace(apiPath, '/api') || '');
167
- if (existsSync(dir) && fs.statSync(dir).isFile()) {
167
+ if (existsSync(dir) && fs__default.statSync(dir).isFile()) {
168
168
  ctx.status = 404;
169
169
  ctx.body = {
170
170
  code: 404,
@@ -211,7 +211,7 @@ router.all('apps/:app/{*path}', async (ctx) => {
211
211
  ctx.status = 405;
212
212
  return;
213
213
  }
214
- const rootPath = path.join(process.cwd(), 'node_modules', appName);
214
+ const rootPath = path__default.join(process.cwd(), 'node_modules', appName);
215
215
  const resourcePath = formatPath(ctx.params?.path);
216
216
  let root = '';
217
217
  try {
@@ -227,9 +227,9 @@ router.all('apps/:app/{*path}', async (ctx) => {
227
227
  };
228
228
  return;
229
229
  }
230
- const fullPath = root ? path.join(rootPath, root, resourcePath) : path.join(rootPath, resourcePath);
230
+ const fullPath = root ? path__default.join(rootPath, root, resourcePath) : path__default.join(rootPath, resourcePath);
231
231
  try {
232
- const file = await fs.promises.readFile(fullPath);
232
+ const file = await fs__default.promises.readFile(fullPath);
233
233
  const mimeType = mime.lookup(fullPath) || 'application/octet-stream';
234
234
  ctx.set('Content-Type', mimeType);
235
235
  ctx.body = file;
@@ -1,4 +1,4 @@
1
- import fs, { existsSync } from 'fs';
1
+ import fs__default, { existsSync } from 'fs';
2
2
 
3
3
  const getModuelFile = (dir) => {
4
4
  const dirMap = {
@@ -17,7 +17,7 @@ const getModuelFile = (dir) => {
17
17
  };
18
18
  for (const key in dirMap) {
19
19
  const filePath = dirMap[key];
20
- if (existsSync(filePath) && fs.statSync(filePath)) {
20
+ if (existsSync(filePath) && fs__default.statSync(filePath)) {
21
21
  return filePath;
22
22
  }
23
23
  }
@@ -4,9 +4,9 @@ export type DataMarkdownText = {
4
4
  };
5
5
  export type DataMarkdownMention = {
6
6
  type: 'MD.mention';
7
- value?: string;
7
+ value?: string | 'everyone';
8
8
  options?: {
9
- belong?: 'user' | 'channel' | 'all';
9
+ belong?: 'user' | 'channel';
10
10
  };
11
11
  };
12
12
  export type DataMarkdownContent = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alemonjs",
3
- "version": "2.1.28",
3
+ "version": "2.1.30-rc.0",
4
4
  "description": "bot script",
5
5
  "author": "lemonade",
6
6
  "license": "MIT",