evolclaw 2.1.2 → 2.3.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.
Files changed (54) hide show
  1. package/README.md +59 -30
  2. package/data/evolclaw.sample.json +15 -4
  3. package/dist/agents/claude-runner.js +685 -0
  4. package/dist/agents/codex-runner.js +315 -0
  5. package/dist/agents/gemini-runner.js +425 -0
  6. package/dist/channels/aun.js +580 -10
  7. package/dist/channels/feishu.js +888 -135
  8. package/dist/channels/wechat.js +127 -21
  9. package/dist/cli.js +519 -136
  10. package/dist/config.js +277 -25
  11. package/dist/core/agent-loader.js +39 -0
  12. package/dist/core/channel-loader.js +67 -0
  13. package/dist/core/command-handler.js +1537 -392
  14. package/dist/core/event-bus.js +32 -0
  15. package/dist/core/interaction-router.js +68 -0
  16. package/dist/core/message/message-bridge.js +216 -0
  17. package/dist/core/message/message-processor.js +1028 -0
  18. package/dist/core/message/message-queue.js +240 -0
  19. package/dist/core/message/stream-debouncer.js +122 -0
  20. package/dist/{utils → core/message}/stream-flusher.js +73 -13
  21. package/dist/{utils → core/message}/stream-idle-monitor.js +1 -1
  22. package/dist/core/permission.js +259 -0
  23. package/dist/core/session/adapters/claude-session-file-adapter.js +144 -0
  24. package/dist/core/session/adapters/codex-session-file-adapter.js +261 -0
  25. package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
  26. package/dist/core/session/session-file-adapter.js +7 -0
  27. package/dist/core/session/session-file-health.js +45 -0
  28. package/dist/core/session/session-manager.js +1072 -0
  29. package/dist/index.js +402 -252
  30. package/dist/ipc.js +106 -0
  31. package/dist/paths.js +1 -0
  32. package/dist/types.js +3 -0
  33. package/dist/utils/{platform.js → cross-platform.js} +38 -1
  34. package/dist/utils/error-utils.js +130 -5
  35. package/dist/utils/init-channel.js +649 -0
  36. package/dist/utils/init.js +190 -53
  37. package/dist/utils/logger.js +8 -3
  38. package/dist/utils/media-cache.js +207 -0
  39. package/dist/utils/migrate-project.js +122 -0
  40. package/dist/utils/rich-content-renderer.js +228 -0
  41. package/dist/utils/stats-collector.js +102 -0
  42. package/package.json +4 -2
  43. package/dist/core/agent-runner.js +0 -348
  44. package/dist/core/message-processor.js +0 -604
  45. package/dist/core/message-queue.js +0 -116
  46. package/dist/core/message-stream.js +0 -59
  47. package/dist/core/session-manager.js +0 -664
  48. package/dist/index.js.bak +0 -340
  49. package/dist/utils/init-feishu.js +0 -261
  50. package/dist/utils/init-wechat.js +0 -170
  51. package/dist/utils/markdown-to-feishu.js +0 -94
  52. package/dist/utils/permission.js +0 -43
  53. package/dist/utils/session-file-health.js +0 -68
  54. /package/dist/core/{message-cache.js → message/message-cache.js} +0 -0
@@ -1,170 +0,0 @@
1
- import fs from 'fs';
2
- import readline from 'readline';
3
- import { resolvePaths } from '../paths.js';
4
- const DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com';
5
- const BOT_TYPE = '3';
6
- const QR_POLL_TIMEOUT_MS = 35_000;
7
- const LOGIN_TIMEOUT_MS = 480_000;
8
- function ask(rl, question) {
9
- return new Promise(resolve => rl.question(question, resolve));
10
- }
11
- async function fetchQRCode(baseUrl) {
12
- const base = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
13
- const url = `${base}ilink/bot/get_bot_qrcode?bot_type=${BOT_TYPE}`;
14
- const res = await fetch(url);
15
- if (!res.ok)
16
- throw new Error(`QR fetch failed: ${res.status}`);
17
- return (await res.json());
18
- }
19
- async function pollQRStatus(baseUrl, qrcode) {
20
- const base = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
21
- const url = `${base}ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`;
22
- const controller = new AbortController();
23
- const timer = setTimeout(() => controller.abort(), QR_POLL_TIMEOUT_MS);
24
- try {
25
- const res = await fetch(url, {
26
- headers: { 'iLink-App-ClientVersion': '1' },
27
- signal: controller.signal,
28
- });
29
- clearTimeout(timer);
30
- if (!res.ok)
31
- throw new Error(`QR status failed: ${res.status}`);
32
- return (await res.json());
33
- }
34
- catch (err) {
35
- clearTimeout(timer);
36
- if (err instanceof Error && err.name === 'AbortError') {
37
- return { status: 'wait' };
38
- }
39
- throw err;
40
- }
41
- }
42
- export async function runWechatQrFlow() {
43
- const qrResp = await fetchQRCode(DEFAULT_BASE_URL);
44
- try {
45
- const qrterm = await import('qrcode-terminal');
46
- await new Promise(resolve => {
47
- qrterm.default.generate(qrResp.qrcode_img_content, { small: true }, (qr) => {
48
- console.log(qr);
49
- resolve();
50
- });
51
- });
52
- }
53
- catch {
54
- console.log(`请在浏览器中打开此链接扫码: ${qrResp.qrcode_img_content}\n`);
55
- }
56
- console.log('请用微信扫描上方二维码...\n');
57
- const deadline = Date.now() + LOGIN_TIMEOUT_MS;
58
- let scannedPrinted = false;
59
- while (Date.now() < deadline) {
60
- const status = await pollQRStatus(DEFAULT_BASE_URL, qrResp.qrcode);
61
- switch (status.status) {
62
- case 'wait':
63
- process.stdout.write('.');
64
- break;
65
- case 'scaned':
66
- if (!scannedPrinted) {
67
- console.log('\n👀 已扫码,请在微信中确认...');
68
- scannedPrinted = true;
69
- }
70
- break;
71
- case 'expired':
72
- console.error('\n二维码已过期');
73
- return null;
74
- case 'confirmed':
75
- if (!status.ilink_bot_id || !status.bot_token) {
76
- console.error('\n登录失败:服务器未返回完整信息');
77
- return null;
78
- }
79
- return {
80
- baseUrl: status.baseurl || DEFAULT_BASE_URL,
81
- token: status.bot_token,
82
- };
83
- }
84
- await new Promise(r => setTimeout(r, 1000));
85
- }
86
- console.log('\n登录超时');
87
- return null;
88
- }
89
- export async function cmdInitWechat() {
90
- const p = resolvePaths();
91
- if (!fs.existsSync(p.config)) {
92
- console.log(`❌ 配置文件不存在,请先运行 evolclaw init`);
93
- return;
94
- }
95
- const config = JSON.parse(fs.readFileSync(p.config, 'utf-8'));
96
- // 检查已有配置
97
- if (config.channels?.wechat?.token) {
98
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
99
- try {
100
- const answer = (await ask(rl, '已有微信配置,是否重新登录?[y/N] ')).trim().toLowerCase();
101
- if (answer !== 'y' && answer !== 'yes') {
102
- console.log('已取消');
103
- return;
104
- }
105
- }
106
- finally {
107
- rl.close();
108
- }
109
- }
110
- console.log('正在获取微信登录二维码...\n');
111
- const qrResp = await fetchQRCode(DEFAULT_BASE_URL);
112
- // 终端显示二维码
113
- try {
114
- const qrterm = await import('qrcode-terminal');
115
- await new Promise(resolve => {
116
- qrterm.default.generate(qrResp.qrcode_img_content, { small: true }, (qr) => {
117
- console.log(qr);
118
- resolve();
119
- });
120
- });
121
- }
122
- catch {
123
- console.log(`请在浏览器中打开此链接扫码: ${qrResp.qrcode_img_content}\n`);
124
- }
125
- console.log('请用微信扫描上方二维码...\n');
126
- const deadline = Date.now() + LOGIN_TIMEOUT_MS;
127
- let scannedPrinted = false;
128
- while (Date.now() < deadline) {
129
- const status = await pollQRStatus(DEFAULT_BASE_URL, qrResp.qrcode);
130
- switch (status.status) {
131
- case 'wait':
132
- process.stdout.write('.');
133
- break;
134
- case 'scaned':
135
- if (!scannedPrinted) {
136
- console.log('\n👀 已扫码,请在微信中确认...');
137
- scannedPrinted = true;
138
- }
139
- break;
140
- case 'expired':
141
- console.log('\n二维码已过期,请重新运行 evolclaw init wechat');
142
- process.exit(1);
143
- break;
144
- case 'confirmed': {
145
- if (!status.ilink_bot_id || !status.bot_token) {
146
- console.error('\n登录失败:服务器未返回完整信息');
147
- process.exit(1);
148
- }
149
- // 写入配置
150
- if (!config.channels)
151
- config.channels = {};
152
- config.channels.wechat = {
153
- enabled: true,
154
- baseUrl: status.baseurl || DEFAULT_BASE_URL,
155
- token: status.bot_token,
156
- };
157
- fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
158
- console.log(`\n✅ 微信连接成功!`);
159
- console.log(` Bot ID: ${status.ilink_bot_id}`);
160
- console.log(` User ID: ${status.ilink_user_id}`);
161
- console.log(` 配置已写入: ${p.config}`);
162
- console.log(`\n现在可以启动服务: evolclaw restart`);
163
- return;
164
- }
165
- }
166
- await new Promise(r => setTimeout(r, 1000));
167
- }
168
- console.log('\n登录超时,请重新运行');
169
- process.exit(1);
170
- }
@@ -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
- }
@@ -1,43 +0,0 @@
1
- // 危险命令黑名单(正则表达式)
2
- const DANGEROUS_PATTERNS = [
3
- // Unix
4
- /\brm\s+-\w*r\w*f/, // rm -rf
5
- /\bsudo\b/, // sudo
6
- /\bmkfs\b/, // mkfs (格式化文件系统)
7
- /\bdd\s+if=/, // dd (磁盘操作)
8
- /\bchmod\s+777/, // chmod 777 (危险权限)
9
- />\s*\/dev\//, // 重定向到设备文件
10
- /\bshutdown\b/, // 关机
11
- /\breboot\b/, // 重启
12
- // Windows
13
- /\bformat\s+[a-zA-Z]:/i, // format C: (格式化磁盘)
14
- /\brd\s+\/s/i, // rd /s (递归删除目录)
15
- /\bdel\s+\/[sfq]/i, // del /f, /s, /q (强制删除)
16
- /\breg\s+delete/i, // reg delete (删除注册表)
17
- /\bnet\s+stop/i, // net stop (停止服务)
18
- ];
19
- /**
20
- * 权限检查回调函数
21
- * 符合 Claude Agent SDK 的 can_use_tool 接口
22
- */
23
- export async function canUseTool(toolName, input) {
24
- // 只检查 Bash 工具,其余工具全部放行
25
- if (toolName === 'Bash') {
26
- const cmd = input.command || '';
27
- // 空命令直接放行
28
- if (!cmd || cmd.trim() === '') {
29
- return { behavior: 'allow', updatedInput: input };
30
- }
31
- // 检查黑名单
32
- for (const pattern of DANGEROUS_PATTERNS) {
33
- if (pattern.test(cmd)) {
34
- return {
35
- behavior: 'deny',
36
- message: `⛔ 危险命令被拦截: ${cmd.substring(0, 80)}`
37
- };
38
- }
39
- }
40
- }
41
- // 默认允许
42
- return { behavior: 'allow', updatedInput: input };
43
- }
@@ -1,68 +0,0 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
- import { logger } from './logger.js';
4
- /**
5
- * 检查会话文件是否存在
6
- */
7
- async function fileExists(filePath) {
8
- try {
9
- await fs.access(filePath);
10
- return true;
11
- }
12
- catch {
13
- return false;
14
- }
15
- }
16
- /**
17
- * 检查会话文件健康度
18
- */
19
- export async function checkSessionFileHealth(projectPath, agentSessionId) {
20
- const issues = [];
21
- const sessionFile = path.join(projectPath, '.claude', `${agentSessionId}.jsonl`);
22
- // 检查文件是否存在
23
- if (!(await fileExists(sessionFile))) {
24
- // 新会话没有文件是正常的
25
- return { healthy: true, issues: [] };
26
- }
27
- try {
28
- // 检查文件大小
29
- const stats = await fs.stat(sessionFile);
30
- const sizeMB = stats.size / (1024 * 1024);
31
- if (stats.size > 50 * 1024 * 1024) {
32
- issues.push(`会话文件过大: ${sizeMB.toFixed(1)}MB`);
33
- }
34
- // 检查 JSON 格式
35
- const content = await fs.readFile(sessionFile, 'utf-8');
36
- const lines = content.split('\n').filter(l => l.trim());
37
- for (let i = 0; i < lines.length; i++) {
38
- try {
39
- JSON.parse(lines[i]);
40
- }
41
- catch (e) {
42
- issues.push(`会话文件格式损坏(第 ${i + 1} 行)`);
43
- return { healthy: false, issues, corrupt: true, fileSize: stats.size };
44
- }
45
- }
46
- return {
47
- healthy: issues.length === 0,
48
- issues,
49
- fileSize: stats.size
50
- };
51
- }
52
- catch (error) {
53
- logger.error('[SessionFileHealth] Check failed:', error);
54
- issues.push(`文件读取失败: ${error.message}`);
55
- return { healthy: false, issues, corrupt: true };
56
- }
57
- }
58
- /**
59
- * 备份会话目录
60
- */
61
- export async function backupClaudeDir(projectPath) {
62
- const claudeDir = path.join(projectPath, '.claude');
63
- const dirName = path.basename(claudeDir);
64
- const backupDir = path.join(path.dirname(claudeDir), `${dirName}-backup-${Date.now()}`);
65
- await fs.cp(claudeDir, backupDir, { recursive: true });
66
- logger.info(`[SessionFileHealth] Backup created: ${backupDir}`);
67
- return backupDir;
68
- }