evolclaw 2.1.1 → 2.2.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.md +10 -3
- package/data/evolclaw.sample.json +9 -1
- package/dist/agents/claude-runner.js +612 -0
- package/dist/agents/codex-runner.js +310 -0
- package/dist/channels/aun.js +416 -9
- package/dist/channels/feishu.js +397 -104
- package/dist/channels/wechat.js +84 -2
- package/dist/cli.js +427 -126
- package/dist/config.js +102 -4
- package/dist/core/adapters/claude-session-file-adapter.js +144 -0
- package/dist/core/adapters/codex-session-file-adapter.js +196 -0
- package/dist/core/agent-loader.js +39 -0
- package/dist/core/channel-loader.js +60 -0
- package/dist/core/command-handler.js +908 -304
- package/dist/core/event-bus.js +32 -0
- package/dist/core/ipc-server.js +71 -0
- package/dist/core/message-bridge.js +187 -0
- package/dist/core/message-processor.js +370 -227
- package/dist/core/message-queue.js +153 -29
- package/dist/core/permission.js +58 -0
- package/dist/core/session-file-adapter.js +7 -0
- package/dist/core/session-manager.js +571 -223
- package/dist/core/stats-collector.js +86 -0
- package/dist/index.js +309 -243
- package/dist/paths.js +1 -0
- package/dist/utils/error-utils.js +4 -2
- package/dist/utils/init-feishu.js +2 -0
- package/dist/utils/init-wechat.js +2 -0
- package/dist/utils/init.js +285 -53
- package/dist/utils/ipc-client.js +36 -0
- package/dist/utils/migrate-project.js +122 -0
- package/dist/utils/{permission.js → permission-utils.js} +31 -3
- package/dist/utils/rich-content-renderer.js +228 -0
- package/dist/utils/session-file-health.js +11 -34
- package/dist/utils/stream-debouncer.js +122 -0
- package/dist/utils/stream-idle-monitor.js +1 -1
- package/package.json +3 -1
- package/dist/core/agent-runner.js +0 -348
- package/dist/core/message-stream.js +0 -59
- package/dist/index.js.bak +0 -340
- package/dist/utils/markdown-to-feishu.js +0 -94
- /package/dist/utils/{platform.js → cross-platform.js} +0 -0
- /package/dist/{core → utils}/message-cache.js +0 -0
package/dist/index.js.bak
DELETED
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
import { loadConfig, ensureDataDirs, resolvePaths, resolveAnthropicConfig } from './config.js';
|
|
2
|
-
import { SessionManager } from './core/session-manager.js';
|
|
3
|
-
import { AgentRunner } from './core/agent-runner.js';
|
|
4
|
-
import { FeishuChannel } from './channels/feishu.js';
|
|
5
|
-
import { AUNChannel } from './channels/aun.js';
|
|
6
|
-
import { WechatChannel } from './channels/wechat.js';
|
|
7
|
-
import { MessageProcessor } from './core/message-processor.js';
|
|
8
|
-
import { MessageQueue } from './core/message-queue.js';
|
|
9
|
-
import { MessageCache } from './core/message-cache.js';
|
|
10
|
-
import { CommandHandler } from './core/command-handler.js';
|
|
11
|
-
import { logger } from './utils/logger.js';
|
|
12
|
-
import path from 'path';
|
|
13
|
-
import fs from 'fs';
|
|
14
|
-
async function main() {
|
|
15
|
-
// 过滤飞书 SDK 的 info 日志
|
|
16
|
-
const originalLog = console.log;
|
|
17
|
-
const originalInfo = console.info;
|
|
18
|
-
const filter = (...args) => {
|
|
19
|
-
const firstArg = String(args[0] || '');
|
|
20
|
-
return firstArg.includes('[info]') || firstArg.includes('[ws]');
|
|
21
|
-
};
|
|
22
|
-
console.log = (...args) => {
|
|
23
|
-
if (filter(...args))
|
|
24
|
-
return;
|
|
25
|
-
originalLog(...args);
|
|
26
|
-
};
|
|
27
|
-
console.info = (...args) => {
|
|
28
|
-
if (filter(...args))
|
|
29
|
-
return;
|
|
30
|
-
originalInfo(...args);
|
|
31
|
-
};
|
|
32
|
-
logger.info('EvolClaw starting...');
|
|
33
|
-
// 确保数据目录存在
|
|
34
|
-
ensureDataDirs();
|
|
35
|
-
// 加载配置
|
|
36
|
-
const config = loadConfig();
|
|
37
|
-
const anthropic = resolveAnthropicConfig(config);
|
|
38
|
-
logger.info('✓ Config loaded (API keys hidden)');
|
|
39
|
-
if (anthropic.baseUrl) {
|
|
40
|
-
logger.info(`✓ Using custom API base URL: ${anthropic.baseUrl}`);
|
|
41
|
-
}
|
|
42
|
-
// 初始化数据库
|
|
43
|
-
const sessionManager = new SessionManager();
|
|
44
|
-
logger.info('✓ Database initialized');
|
|
45
|
-
// 初始化 Agent Runner(带持久化回调)
|
|
46
|
-
const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, async (sessionId, agentSessionId) => {
|
|
47
|
-
await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
|
|
48
|
-
}, anthropic.baseUrl, config);
|
|
49
|
-
if (anthropic.effort) {
|
|
50
|
-
agentRunner.setEffort(anthropic.effort);
|
|
51
|
-
}
|
|
52
|
-
logger.info('✓ Agent runner ready');
|
|
53
|
-
// 创建消息缓存
|
|
54
|
-
const messageCache = new MessageCache();
|
|
55
|
-
logger.info('✓ Message cache initialized');
|
|
56
|
-
// 定期清理过期消息(每小时)
|
|
57
|
-
setInterval(() => {
|
|
58
|
-
messageCache.cleanupExpired();
|
|
59
|
-
}, 60 * 60 * 1000);
|
|
60
|
-
// 飞书渠道(条件初始化)
|
|
61
|
-
let feishu = null;
|
|
62
|
-
if (config.channels?.feishu?.enabled !== false && config.channels?.feishu?.appId) {
|
|
63
|
-
feishu = new FeishuChannel({
|
|
64
|
-
appId: config.channels.feishu.appId,
|
|
65
|
-
appSecret: config.channels.feishu.appSecret,
|
|
66
|
-
db: sessionManager.getDatabase()
|
|
67
|
-
});
|
|
68
|
-
// 设置项目路径提供器
|
|
69
|
-
feishu.onProjectPathRequest(async (chatId) => {
|
|
70
|
-
const session = await sessionManager.getOrCreateSession('feishu', chatId, config.projects?.defaultPath || process.cwd());
|
|
71
|
-
return path.isAbsolute(session.projectPath)
|
|
72
|
-
? session.projectPath
|
|
73
|
-
: path.resolve(process.cwd(), session.projectPath);
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
// AUN 渠道(条件初始化)
|
|
77
|
-
let aun = null;
|
|
78
|
-
if (config.channels?.aun?.enabled !== false && config.channels?.aun?.domain) {
|
|
79
|
-
aun = new AUNChannel({ domain: config.channels.aun.domain, agentName: config.channels.aun.agentName });
|
|
80
|
-
}
|
|
81
|
-
// 创建命令处理器
|
|
82
|
-
const cmdHandler = new CommandHandler(sessionManager, agentRunner, config, messageCache);
|
|
83
|
-
// 创建消息处理器
|
|
84
|
-
const processor = new MessageProcessor(agentRunner, sessionManager, config, messageCache, (content, channel, channelId, userId, threadId) => {
|
|
85
|
-
const sendFn = async (id, text, opts) => {
|
|
86
|
-
const adapter = cmdHandler.getAdapter(channel);
|
|
87
|
-
if (!adapter)
|
|
88
|
-
return;
|
|
89
|
-
// 文件标记处理(通过 adapter.sendFile 能力判断,不按渠道名分支)
|
|
90
|
-
if (adapter.sendFile) {
|
|
91
|
-
const fileMarkerPattern = /\[SEND_FILE:([^\]]+)\]/g;
|
|
92
|
-
const fileMatches = [...text.matchAll(fileMarkerPattern)];
|
|
93
|
-
for (const match of fileMatches) {
|
|
94
|
-
const filePath = match[1].trim();
|
|
95
|
-
// 跳过占位符/代码片段中的伪路径
|
|
96
|
-
if (!filePath || /[\\[\]{}*+?|^$]/.test(filePath))
|
|
97
|
-
continue;
|
|
98
|
-
const session = await sessionManager.getActiveSession(channel, channelId);
|
|
99
|
-
const projectPath = session?.projectPath || process.cwd();
|
|
100
|
-
const absoluteFilePath = path.isAbsolute(filePath) ? filePath : path.join(projectPath, filePath);
|
|
101
|
-
try {
|
|
102
|
-
await adapter.sendFile(id, absoluteFilePath);
|
|
103
|
-
}
|
|
104
|
-
catch (error) {
|
|
105
|
-
logger.error(`[${channel}] Failed to send file: ${absoluteFilePath}`, error);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
text = text.replace(fileMarkerPattern, '').trim();
|
|
109
|
-
}
|
|
110
|
-
if (text) {
|
|
111
|
-
await adapter.sendText(id, text, opts);
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
return cmdHandler.handle(content, channel, channelId, sendFn, userId, threadId);
|
|
115
|
-
});
|
|
116
|
-
// 回填 processor 和 messageQueue 的引用
|
|
117
|
-
cmdHandler.setProcessor(processor);
|
|
118
|
-
// 设置 compact 开始回调
|
|
119
|
-
agentRunner.setCompactStartCallback((sessionId) => {
|
|
120
|
-
processor.handleCompactStart();
|
|
121
|
-
});
|
|
122
|
-
// 创建消息队列
|
|
123
|
-
const messageQueue = new MessageQueue(async (message) => {
|
|
124
|
-
await processor.processMessage(message);
|
|
125
|
-
});
|
|
126
|
-
// 设置中断回调
|
|
127
|
-
messageQueue.setInterruptCallback(async (sessionKey) => {
|
|
128
|
-
await agentRunner.interrupt(sessionKey);
|
|
129
|
-
});
|
|
130
|
-
// 回填 messageQueue 引用
|
|
131
|
-
cmdHandler.setMessageQueue(messageQueue);
|
|
132
|
-
// 注册 Feishu 适配器(如果已初始化)
|
|
133
|
-
if (feishu) {
|
|
134
|
-
const feishuAdapter = {
|
|
135
|
-
name: 'feishu',
|
|
136
|
-
sendText: (channelId, text, options) => feishu.sendMessage(channelId, text, options),
|
|
137
|
-
sendFile: (channelId, filePath) => feishu.sendFile(channelId, filePath),
|
|
138
|
-
isGroupChat: (channelId) => feishu.getChatMode(channelId).then(m => m === 'group'),
|
|
139
|
-
};
|
|
140
|
-
const feishuOptions = {
|
|
141
|
-
systemPromptAppend: '[重要系统功能] 你可以通过飞书发送文件给用户。方法:在响应中使用 [SEND_FILE:文件路径] 标记。示例:文件已准备好![SEND_FILE:./report.txt] 路径支持相对路径(相对项目目录)或绝对路径。系统会自动上传并发送。',
|
|
142
|
-
fileMarkerPattern: /\[SEND_FILE:([^\]]+)\]/g,
|
|
143
|
-
supportsImages: true,
|
|
144
|
-
};
|
|
145
|
-
processor.registerChannel(feishuAdapter, feishuOptions);
|
|
146
|
-
cmdHandler.registerAdapter(feishuAdapter);
|
|
147
|
-
}
|
|
148
|
-
// 注册 AUN 适配器(如果已初始化)
|
|
149
|
-
if (aun) {
|
|
150
|
-
const aunAdapter = {
|
|
151
|
-
name: 'aun',
|
|
152
|
-
sendText: (channelId, text) => aun.sendMessage(channelId, text),
|
|
153
|
-
};
|
|
154
|
-
processor.registerChannel(aunAdapter);
|
|
155
|
-
cmdHandler.registerAdapter(aunAdapter);
|
|
156
|
-
}
|
|
157
|
-
// ── WeChat 渠道(条件初始化)──
|
|
158
|
-
let wechat = null;
|
|
159
|
-
if (config.channels?.wechat?.enabled && config.channels?.wechat?.token) {
|
|
160
|
-
wechat = new WechatChannel({
|
|
161
|
-
baseUrl: config.channels.wechat.baseUrl || 'https://ilinkai.weixin.qq.com',
|
|
162
|
-
token: config.channels.wechat.token,
|
|
163
|
-
});
|
|
164
|
-
// 设置项目路径提供器(用于接收文件保存)
|
|
165
|
-
wechat.onProjectPathRequest(async (channelId) => {
|
|
166
|
-
const session = await sessionManager.getOrCreateSession('wechat', channelId, config.projects?.defaultPath || process.cwd());
|
|
167
|
-
return path.isAbsolute(session.projectPath)
|
|
168
|
-
? session.projectPath
|
|
169
|
-
: path.resolve(process.cwd(), session.projectPath);
|
|
170
|
-
});
|
|
171
|
-
const wechatAdapter = {
|
|
172
|
-
name: 'wechat',
|
|
173
|
-
sendText: (channelId, text) => wechat.sendMessage(channelId, text),
|
|
174
|
-
sendFile: (channelId, filePath) => wechat.sendFile(channelId, filePath),
|
|
175
|
-
};
|
|
176
|
-
const wechatOptions = {
|
|
177
|
-
systemPromptAppend: '[系统功能] 你可以发送文件给用户。方法:在响应中使用 [SEND_FILE:文件路径] 标记。示例:文件已准备好![SEND_FILE:./report.txt]',
|
|
178
|
-
fileMarkerPattern: /\[SEND_FILE:([^\]]+)\]/g,
|
|
179
|
-
};
|
|
180
|
-
processor.registerChannel(wechatAdapter, wechatOptions);
|
|
181
|
-
cmdHandler.registerAdapter(wechatAdapter);
|
|
182
|
-
// Session 过期通知(通过 Feishu 等其他渠道告知用户)
|
|
183
|
-
wechat.onSessionExpiredNotify(async (message) => {
|
|
184
|
-
// 尝试通过已注册的 Feishu owner 通知
|
|
185
|
-
const feishuOwner = config.channels?.feishu?.owner;
|
|
186
|
-
if (feishuOwner) {
|
|
187
|
-
try {
|
|
188
|
-
// Feishu owner ID 是 open_id,但 sendMessage 需要 chat_id
|
|
189
|
-
// 这里只记日志,因为 owner 的 chat_id 需要从 session 中获取
|
|
190
|
-
logger.warn(`[WeChat] ${message}`);
|
|
191
|
-
}
|
|
192
|
-
catch { }
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
logger.warn(`[WeChat] ${message}`);
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
wechat.onMessage(async (channelId, content, userId, images) => {
|
|
199
|
-
content = content.trim();
|
|
200
|
-
// 首次交互自动绑定主人
|
|
201
|
-
if (userId && !config.channels?.wechat?.owner) {
|
|
202
|
-
const { setOwner } = await import('./config.js');
|
|
203
|
-
setOwner(config, 'wechat', userId);
|
|
204
|
-
logger.info(`[Owner] Auto-bound WeChat owner: ${userId}`);
|
|
205
|
-
}
|
|
206
|
-
// 命令快速路径
|
|
207
|
-
if (cmdHandler.isCommand(content)) {
|
|
208
|
-
const cmdResult = await cmdHandler.handle(content, 'wechat', channelId, undefined, userId);
|
|
209
|
-
if (cmdResult !== null) {
|
|
210
|
-
if (cmdResult) {
|
|
211
|
-
try {
|
|
212
|
-
await wechat.sendMessage(channelId, cmdResult);
|
|
213
|
-
}
|
|
214
|
-
catch (error) {
|
|
215
|
-
logger.error('[WeChat] Failed to send command response:', error);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
// 获取当前项目路径
|
|
222
|
-
const session = await sessionManager.getOrCreateSession('wechat', channelId, config.projects?.defaultPath || process.cwd());
|
|
223
|
-
// 普通消息进入队列
|
|
224
|
-
await messageQueue.enqueue(`wechat-${channelId}`, { channel: 'wechat', channelId, content, images, timestamp: Date.now(), userId }, session.projectPath);
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
// Feishu 消息处理
|
|
228
|
-
if (feishu) {
|
|
229
|
-
feishu.onMessage(async ({ channelId: chatId, content: rawContent, images, userId, userName, messageId, mentions, threadId, rootId }) => {
|
|
230
|
-
let content = rawContent.trim();
|
|
231
|
-
// 首次交互自动绑定主人
|
|
232
|
-
if (userId && !config.channels?.feishu?.owner) {
|
|
233
|
-
const { setOwner } = await import('./config.js');
|
|
234
|
-
setOwner(config, 'feishu', userId);
|
|
235
|
-
logger.info(`[Owner] Auto-bound owner: ${userName} (${userId})`);
|
|
236
|
-
}
|
|
237
|
-
// 命令立即处理,不进入队列
|
|
238
|
-
if (cmdHandler.isCommand(content)) {
|
|
239
|
-
const cmdResult = await cmdHandler.handle(content, 'feishu', chatId, undefined, userId, threadId);
|
|
240
|
-
if (cmdResult !== null) {
|
|
241
|
-
if (cmdResult) {
|
|
242
|
-
try {
|
|
243
|
-
await feishu.sendMessage(chatId, cmdResult, { forceText: true, replyToMessageId: rootId, replyInThread: true });
|
|
244
|
-
}
|
|
245
|
-
catch (error) {
|
|
246
|
-
logger.error('[Feishu] Failed to send command response:', error);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
// 获取当前项目路径(话题会话自动创建,携带 metadata)
|
|
253
|
-
const metadata = rootId ? { feishu: { rootId } } : undefined;
|
|
254
|
-
const session = await sessionManager.getOrCreateSession('feishu', chatId, config.projects?.defaultPath || process.cwd(), threadId, metadata);
|
|
255
|
-
// 群聊消息添加用户名前缀
|
|
256
|
-
const chatMode = await feishu.getChatMode(chatId);
|
|
257
|
-
if (chatMode === 'group' && userName) {
|
|
258
|
-
content = `[${userName}] ${content}`;
|
|
259
|
-
}
|
|
260
|
-
// 普通消息进入队列(使用 session.id 作为 key,话题间可并行)
|
|
261
|
-
await messageQueue.enqueue(session.id, { channel: 'feishu', channelId: chatId, content, images, timestamp: Date.now(), userId, userName, messageId, isGroup: chatMode === 'group', mentions, threadId }, session.projectPath);
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
// AUN 消息处理
|
|
265
|
-
if (aun) {
|
|
266
|
-
aun.onMessage(async (sessionId, content) => {
|
|
267
|
-
content = content.trim();
|
|
268
|
-
// 首次交互自动绑定主人
|
|
269
|
-
if (!config.channels?.aun?.owner) {
|
|
270
|
-
const { setOwner } = await import('./config.js');
|
|
271
|
-
setOwner(config, 'aun', sessionId);
|
|
272
|
-
logger.info(`[Owner] Auto-bound AUN owner: ${sessionId}`);
|
|
273
|
-
}
|
|
274
|
-
// 命令立即处理,不进入队列
|
|
275
|
-
if (cmdHandler.isCommand(content)) {
|
|
276
|
-
const cmdResult = await cmdHandler.handle(content, 'aun', sessionId, undefined, sessionId);
|
|
277
|
-
if (cmdResult) {
|
|
278
|
-
await aun.sendMessage(sessionId, cmdResult);
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
// 获取当前项目路径
|
|
283
|
-
const session = await sessionManager.getOrCreateSession('aun', sessionId, config.projects?.defaultPath || process.cwd());
|
|
284
|
-
// 普通消息进入队列
|
|
285
|
-
await messageQueue.enqueue(`aun-${sessionId}`, { channel: 'aun', channelId: sessionId, content, timestamp: Date.now(), userId: sessionId }, session.projectPath);
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
// 连接渠道
|
|
289
|
-
const channels = [];
|
|
290
|
-
const channelInstances = [
|
|
291
|
-
...(feishu ? [{ name: 'Feishu', instance: feishu, timeout: 5000 }] : []),
|
|
292
|
-
...(aun ? [{ name: 'AUN', instance: aun }] : []),
|
|
293
|
-
...(wechat ? [{ name: 'WeChat', instance: wechat }] : []),
|
|
294
|
-
];
|
|
295
|
-
for (const { name, instance, timeout } of channelInstances) {
|
|
296
|
-
try {
|
|
297
|
-
if (timeout) {
|
|
298
|
-
await Promise.race([
|
|
299
|
-
instance.connect(),
|
|
300
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Connection timeout')), timeout))
|
|
301
|
-
]);
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
await instance.connect();
|
|
305
|
-
}
|
|
306
|
-
logger.info(`✓ ${name} connected`);
|
|
307
|
-
channels.push(name);
|
|
308
|
-
}
|
|
309
|
-
catch (error) {
|
|
310
|
-
logger.warn(`⚠ ${name} connection failed (will continue without it)`);
|
|
311
|
-
if (error instanceof Error) {
|
|
312
|
-
logger.warn(` Reason: ${error.message}`);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
logger.info(`\n🚀 EvolClaw is running with ${channels.length} channel(s): ${channels.join(', ')}\n`);
|
|
317
|
-
// 写入 ready 信号,供 restart-monitor 检测启动成功
|
|
318
|
-
const readySignalPath = resolvePaths().readySignal;
|
|
319
|
-
fs.writeFileSync(readySignalPath, String(Date.now()));
|
|
320
|
-
logger.info(`✓ Ready signal written: ${readySignalPath}`);
|
|
321
|
-
// 优雅关闭
|
|
322
|
-
const shutdown = async () => {
|
|
323
|
-
logger.info('\n\nShutting down gracefully...');
|
|
324
|
-
if (feishu)
|
|
325
|
-
await feishu.disconnect();
|
|
326
|
-
if (aun)
|
|
327
|
-
await aun.disconnect();
|
|
328
|
-
if (wechat)
|
|
329
|
-
await wechat.disconnect();
|
|
330
|
-
sessionManager.close();
|
|
331
|
-
logger.info('✓ Shutdown complete');
|
|
332
|
-
process.exit(0);
|
|
333
|
-
};
|
|
334
|
-
process.on('SIGINT', shutdown);
|
|
335
|
-
process.on('SIGTERM', shutdown);
|
|
336
|
-
}
|
|
337
|
-
main().catch((error) => {
|
|
338
|
-
logger.error('Fatal error:', error);
|
|
339
|
-
process.exit(1);
|
|
340
|
-
});
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Markdown 到飞书富文本格式转换工具
|
|
3
|
-
* 使用飞书 post 格式的 md tag 原生渲染 Markdown
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* 计算字符串的显示宽度(CJK 字符按 2 宽度计算)
|
|
7
|
-
*/
|
|
8
|
-
function displayWidth(str) {
|
|
9
|
-
let width = 0;
|
|
10
|
-
for (const ch of str) {
|
|
11
|
-
const code = ch.codePointAt(0);
|
|
12
|
-
// CJK Unified Ideographs, CJK Compatibility, Fullwidth Forms, etc.
|
|
13
|
-
if ((code >= 0x4E00 && code <= 0x9FFF) || // CJK Unified
|
|
14
|
-
(code >= 0x3400 && code <= 0x4DBF) || // CJK Extension A
|
|
15
|
-
(code >= 0xF900 && code <= 0xFAFF) || // CJK Compatibility
|
|
16
|
-
(code >= 0xFF01 && code <= 0xFF60) || // Fullwidth Forms
|
|
17
|
-
(code >= 0x3000 && code <= 0x303F) // CJK Symbols
|
|
18
|
-
) {
|
|
19
|
-
width += 2;
|
|
20
|
-
}
|
|
21
|
-
else {
|
|
22
|
-
width += 1;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return width;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* 用空格填充字符串到指定显示宽度
|
|
29
|
-
*/
|
|
30
|
-
function padToWidth(str, targetWidth) {
|
|
31
|
-
const current = displayWidth(str);
|
|
32
|
-
const padding = Math.max(0, targetWidth - current);
|
|
33
|
-
return str + ' '.repeat(padding);
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* 将 Markdown 表格转换为代码块内的对齐文本
|
|
37
|
-
* 飞书 post md tag 不支持标准 markdown 表格,会静默丢弃内容
|
|
38
|
-
* 用代码块 + 等宽对齐保留二维结构
|
|
39
|
-
*/
|
|
40
|
-
function convertTablesToText(text) {
|
|
41
|
-
const tableRegex = /^(\|.+\|)\n(\|[\s:|-]+\|)\n((?:\|.+\|\n?)+)/gm;
|
|
42
|
-
return text.replace(tableRegex, (_match, headerLine, _sep, bodyBlock) => {
|
|
43
|
-
const parseRow = (line) => line.split('|').slice(1, -1).map((c) => c.trim());
|
|
44
|
-
const headers = parseRow(headerLine);
|
|
45
|
-
const rows = bodyBlock.trim().split('\n').map(parseRow);
|
|
46
|
-
// 计算每列最大显示宽度
|
|
47
|
-
const colWidths = headers.map((h, i) => {
|
|
48
|
-
const cellWidths = rows.map(r => displayWidth(r[i] || ''));
|
|
49
|
-
return Math.max(displayWidth(h), ...cellWidths);
|
|
50
|
-
});
|
|
51
|
-
// 构建对齐的表格文本
|
|
52
|
-
const headerStr = headers.map((h, i) => padToWidth(h, colWidths[i])).join(' ');
|
|
53
|
-
const sepStr = colWidths.map(w => '-'.repeat(w)).join(' ');
|
|
54
|
-
const rowStrs = rows.map(r => headers.map((_, i) => padToWidth(r[i] || '', colWidths[i])).join(' '));
|
|
55
|
-
return '```\n' + [headerStr, sepStr, ...rowStrs].join('\n') + '\n```';
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* 将 Markdown 文本转换为飞书 post 消息格式
|
|
60
|
-
* 利用 md tag 让飞书原生渲染,支持代码高亮、嵌套列表、引用等全部语法
|
|
61
|
-
*/
|
|
62
|
-
export function markdownToFeishuPost(markdown, defaultTitle) {
|
|
63
|
-
const match = markdown.match(/^# (.+)$/m);
|
|
64
|
-
const title = match?.[1] ?? defaultTitle ?? '';
|
|
65
|
-
let body = match ? markdown.replace(/^# .+\n?/, '') : markdown;
|
|
66
|
-
// 转换飞书不支持的 markdown 表格
|
|
67
|
-
body = convertTablesToText(body);
|
|
68
|
-
return {
|
|
69
|
-
zh_cn: {
|
|
70
|
-
title,
|
|
71
|
-
content: [[{ tag: 'md', text: body.trim() }]]
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* 检测文本是否包含 Markdown 语法
|
|
77
|
-
*/
|
|
78
|
-
export function hasMarkdownSyntax(text) {
|
|
79
|
-
const markdownPatterns = [
|
|
80
|
-
/^#{1,6}\s/m, // 标题
|
|
81
|
-
/\*\*.*?\*\*/, // 粗体
|
|
82
|
-
/\*.*?\*/, // 斜体
|
|
83
|
-
/__.*?__/, // 粗体
|
|
84
|
-
/_.*?_/, // 斜体
|
|
85
|
-
/~~.*?~~/, // 删除线
|
|
86
|
-
/`.*?`/, // 行内代码
|
|
87
|
-
/```[\s\S]*?```/, // 代码块
|
|
88
|
-
/\[.*?\]\(.*?\)/, // 链接
|
|
89
|
-
/^[\s]*[-*+]\s/m, // 无序列表
|
|
90
|
-
/^[\s]*\d+\.\s/m, // 有序列表
|
|
91
|
-
/^\|.+\|$/m // 表格
|
|
92
|
-
];
|
|
93
|
-
return markdownPatterns.some(pattern => pattern.test(text));
|
|
94
|
-
}
|
|
File without changes
|
|
File without changes
|