@cuebot/skill 1.0.5 → 1.0.6
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 +1 -1
- package/config/modes.json +32 -0
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/api/cuecueClient.js +54 -13
- package/src/core/backgroundExecutor.js +8 -8
- package/src/core/monitorManager.js +14 -0
- package/src/cron/monitor-daemon.js +36 -24
- package/src/index.js +4 -4
- package/src/notifier/index.js +43 -3
- package/src/utils/dataSource.js +145 -20
- package/src/utils/envAdapter.js +233 -0
- package/src/utils/envUtils.js +57 -11
- package/src/utils/fileUtils.js +10 -3
- package/src/utils/notificationQueue.js +134 -246
- package/src/utils/openclawUtils.js +28 -1
- package/src/utils/subagentScheduler.js +147 -0
- package/src/utils/validators.js +0 -122
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 环境适配层
|
|
3
|
+
* 自动检测运行环境,选择最佳适配器
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import { createLogger } from '../core/logger.js';
|
|
8
|
+
|
|
9
|
+
const logger = createLogger('EnvAdapter');
|
|
10
|
+
|
|
11
|
+
let cachedEnv = null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 检测运行环境
|
|
15
|
+
* @returns {'openclaw' | 'standalone'}
|
|
16
|
+
*/
|
|
17
|
+
export function detectEnv() {
|
|
18
|
+
if (cachedEnv) return cachedEnv;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
// 尝试执行 openclaw 命令
|
|
22
|
+
execSync('openclaw --version', { stdio: 'ignore' });
|
|
23
|
+
cachedEnv = 'openclaw';
|
|
24
|
+
logger.info('Detected environment: OpenClaw Gateway');
|
|
25
|
+
} catch (e) {
|
|
26
|
+
cachedEnv = 'standalone';
|
|
27
|
+
logger.info('Detected environment: Standalone');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return cachedEnv;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 是否在 OpenClaw 环境中
|
|
35
|
+
* @returns {boolean}
|
|
36
|
+
*/
|
|
37
|
+
export function isOpenClaw() {
|
|
38
|
+
return detectEnv() === 'openclaw';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 是否在独立环境中
|
|
43
|
+
* @returns {boolean}
|
|
44
|
+
*/
|
|
45
|
+
export function isStandalone() {
|
|
46
|
+
return detectEnv() === 'standalone';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 获取通知适配器
|
|
51
|
+
* @returns {Object}
|
|
52
|
+
*/
|
|
53
|
+
export function getNotifier() {
|
|
54
|
+
const env = detectEnv();
|
|
55
|
+
|
|
56
|
+
if (env === 'openclaw') {
|
|
57
|
+
return {
|
|
58
|
+
name: 'OpenClaw',
|
|
59
|
+
send: openclawNotify,
|
|
60
|
+
sendToUser: openclawSendToUser
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
name: 'Standalone',
|
|
66
|
+
send: standaloneNotify,
|
|
67
|
+
sendToUser: standaloneNotify
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* OpenClaw 通知 - 通过 Hook
|
|
73
|
+
* @param {Object} options
|
|
74
|
+
*/
|
|
75
|
+
async function openclawNotify(options) {
|
|
76
|
+
const { title, message, chatId } = options;
|
|
77
|
+
|
|
78
|
+
// 使用 openclaw message 命令发送
|
|
79
|
+
try {
|
|
80
|
+
const cmd = chatId
|
|
81
|
+
? `openclaw message send --to "${chatId}" --message "${title}\n\n${message}"`
|
|
82
|
+
: `openclaw message broadcast --message "${title}\n\n${message}"`;
|
|
83
|
+
|
|
84
|
+
execSync(cmd, { stdio: 'ignore' });
|
|
85
|
+
logger.info(`Notification sent via OpenClaw: ${title}`);
|
|
86
|
+
return true;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
logger.error('OpenClaw notification failed:', error.message);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* OpenClaw 发送消息给用户
|
|
95
|
+
*/
|
|
96
|
+
async function openclawSendToUser(chatId, message) {
|
|
97
|
+
try {
|
|
98
|
+
const cmd = `openclaw message send --to "${chatId}" --message "${message}"`;
|
|
99
|
+
execSync(cmd, { stdio: 'ignore' });
|
|
100
|
+
return true;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
logger.error('Send to user failed:', error.message);
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 独立环境通知 - Webhook 或 CLI
|
|
109
|
+
*/
|
|
110
|
+
async function standaloneNotify(options) {
|
|
111
|
+
const { title, message, webhookUrl } = options;
|
|
112
|
+
|
|
113
|
+
// 优先使用 Webhook
|
|
114
|
+
if (webhookUrl) {
|
|
115
|
+
try {
|
|
116
|
+
await fetch(webhookUrl, {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: { 'Content-Type': 'application/json' },
|
|
119
|
+
body: JSON.stringify({ title, message })
|
|
120
|
+
});
|
|
121
|
+
return true;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
logger.error('Webhook notification failed:', error.message);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 降级到 stdout
|
|
128
|
+
console.log(`[NOTIFY] ${title}: ${message}`);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 获取调度适配器
|
|
134
|
+
* @returns {Object}
|
|
135
|
+
*/
|
|
136
|
+
export function getScheduler() {
|
|
137
|
+
const env = detectEnv();
|
|
138
|
+
|
|
139
|
+
if (env === 'openclaw') {
|
|
140
|
+
return {
|
|
141
|
+
name: 'OpenClaw Cron',
|
|
142
|
+
register: registerOpenClawCron,
|
|
143
|
+
unregister: unregisterOpenClawCron,
|
|
144
|
+
list: listOpenClawCron
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
name: 'Node Cron',
|
|
150
|
+
register: registerNodeCron,
|
|
151
|
+
unregister: unregisterNodeCron,
|
|
152
|
+
list: () => []
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ============ OpenClaw Cron 实现 ============
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 注册 OpenClaw Cron 任务
|
|
160
|
+
*/
|
|
161
|
+
async function registerOpenClawCron(options) {
|
|
162
|
+
const { name, message, cron, chatId } = options;
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
// 先检查是否存在
|
|
166
|
+
const tasks = listOpenClawCron();
|
|
167
|
+
if (tasks.some(t => t.name === name)) {
|
|
168
|
+
logger.info(`Cron task already exists: ${name}`);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const cmd = [
|
|
173
|
+
'openclaw cron add',
|
|
174
|
+
`--name "${name}"`,
|
|
175
|
+
`--message "${message}"`,
|
|
176
|
+
`--cron "${cron}"`,
|
|
177
|
+
'--channel ${process.env.OPENCLAW_CHANNEL || \'feishu\'}',
|
|
178
|
+
`--to "${chatId}"`,
|
|
179
|
+
'--announce'
|
|
180
|
+
].join(' ');
|
|
181
|
+
|
|
182
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
183
|
+
logger.info(`Registered cron task: ${name}`);
|
|
184
|
+
return true;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
logger.error('Failed to register cron task:', error.message);
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 注销 OpenClaw Cron 任务
|
|
193
|
+
*/
|
|
194
|
+
async function unregisterOpenClawCron(name) {
|
|
195
|
+
try {
|
|
196
|
+
execSync(`openclaw cron rm "${name}"`, { stdio: 'ignore' });
|
|
197
|
+
logger.info(`Unregistered cron task: ${name}`);
|
|
198
|
+
return true;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
logger.warn('Failed to unregister cron task:', error.message);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 列出 OpenClaw Cron 任务
|
|
207
|
+
*/
|
|
208
|
+
function listOpenClawCron() {
|
|
209
|
+
try {
|
|
210
|
+
const output = execSync('openclaw cron list --json', { encoding: 'utf-8' });
|
|
211
|
+
return JSON.parse(output || '[]');
|
|
212
|
+
} catch (e) {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ============ Node Cron 占位实现 ============
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Node Cron 注册(需要外部实现)
|
|
221
|
+
*/
|
|
222
|
+
async function registerNodeCron(options) {
|
|
223
|
+
logger.warn('Node Cron registration not implemented in envAdapter');
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Node Cron 注销
|
|
229
|
+
*/
|
|
230
|
+
async function unregisterNodeCron(name) {
|
|
231
|
+
logger.warn('Node Cron unregistration not implemented in envAdapter');
|
|
232
|
+
return false;
|
|
233
|
+
}
|
package/src/utils/envUtils.js
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 环境变量工具 - 安全版本 v1.0.
|
|
3
|
-
*
|
|
2
|
+
* 环境变量工具 - 安全版本 v1.0.6
|
|
3
|
+
* 用户工作区存储:/root/.openclaw/workspaces/{channel}-{user_id}/.cuecue
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import fs from 'fs-extra';
|
|
7
7
|
import path from 'path';
|
|
8
|
-
import { homedir } from 'os';
|
|
9
8
|
import { createLogger } from '../core/logger.js';
|
|
10
9
|
|
|
11
10
|
const logger = createLogger('EnvUtils');
|
|
12
11
|
|
|
13
|
-
// ✅
|
|
14
|
-
|
|
12
|
+
// ✅ 通用化:支持任意 channel
|
|
13
|
+
function getUserWorkspaceDir(chatId) {
|
|
14
|
+
const workspaceBase = process.env.OPENCLAW_WORKSPACE || '/root/.openclaw/workspaces';
|
|
15
|
+
const channel = process.env.OPENCLAW_CHANNEL || 'feishu';
|
|
16
|
+
return path.join(workspaceBase, `${channel}-${chatId}`, '.cuecue');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const chatId = process.env.CHAT_ID || process.env.FEISHU_CHAT_ID || 'default';
|
|
20
|
+
const CUECUE_DIR = getUserWorkspaceDir(chatId);
|
|
15
21
|
const SECURE_ENV_FILE = path.join(CUECUE_DIR, '.env.secure');
|
|
16
22
|
|
|
17
23
|
/**
|
|
@@ -65,7 +71,7 @@ export async function saveEnvFile(env) {
|
|
|
65
71
|
// ✅ 安全修复:确保目录存在并设置权限
|
|
66
72
|
await ensureSecureDir(CUECUE_DIR, 0o700);
|
|
67
73
|
|
|
68
|
-
const lines = ['# Cue v1.0.
|
|
74
|
+
const lines = ['# Cue v1.0.6 - Secure Environment Variables', '# DO NOT SHARE THIS FILE'];
|
|
69
75
|
for (const [key, value] of env) {
|
|
70
76
|
lines.push(`export ${key}="${value}"`);
|
|
71
77
|
}
|
|
@@ -81,6 +87,26 @@ export async function saveEnvFile(env) {
|
|
|
81
87
|
}
|
|
82
88
|
}
|
|
83
89
|
|
|
90
|
+
/**
|
|
91
|
+
* 从用户配置文件获取 API Key
|
|
92
|
+
* @param {string} userId - 用户 ID
|
|
93
|
+
* @returns {Promise<string|null>}
|
|
94
|
+
*/
|
|
95
|
+
async function getUserApiKey(userId) {
|
|
96
|
+
if (!userId) return null;
|
|
97
|
+
|
|
98
|
+
const userConfigPath = path.join(CUECUE_DIR, 'config.json');
|
|
99
|
+
try {
|
|
100
|
+
if (await fs.pathExists(userConfigPath)) {
|
|
101
|
+
const config = await fs.readJson(userConfigPath);
|
|
102
|
+
return config.api_key || config.apiKey || null;
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
// 忽略错误
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
84
110
|
/**
|
|
85
111
|
* 获取 API Key
|
|
86
112
|
* 优先从环境变量读取,其次从安全文件读取
|
|
@@ -88,12 +114,32 @@ export async function saveEnvFile(env) {
|
|
|
88
114
|
* @returns {Promise<string|null>}
|
|
89
115
|
*/
|
|
90
116
|
export async function getApiKey(keyName) {
|
|
91
|
-
// 1.
|
|
92
|
-
|
|
93
|
-
|
|
117
|
+
// 1. 首先检查用户自己的 Key (隔离存储)
|
|
118
|
+
const currentChatId = process.env.CHAT_ID || process.env.FEISHU_CHAT_ID;
|
|
119
|
+
if (currentChatId && currentChatId.startsWith('ou_')) {
|
|
120
|
+
const userApiKey = await getUserApiKey(currentChatId);
|
|
121
|
+
if (userApiKey) {
|
|
122
|
+
return userApiKey;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 2. 尝试通过 OpenClaw Secrets 获取系统 Key
|
|
127
|
+
// 如果在 OpenClaw 环境中,Secrets 会被注入到 process.env
|
|
128
|
+
// 但以脱敏形式,不会暴露原始值
|
|
129
|
+
try {
|
|
130
|
+
const { execSync } = await import('child_process');
|
|
131
|
+
const result = execSync(`openclaw secrets get ${keyName}`, {
|
|
132
|
+
encoding: 'utf-8',
|
|
133
|
+
timeout: 5000
|
|
134
|
+
}).trim();
|
|
135
|
+
if (result && result !== 'not found') {
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
} catch (e) {
|
|
139
|
+
// Secrets 未配置,忽略
|
|
94
140
|
}
|
|
95
141
|
|
|
96
|
-
//
|
|
142
|
+
// 3. 最后检查用户配置文件
|
|
97
143
|
const env = await loadEnvFile();
|
|
98
144
|
return env.get(keyName) || null;
|
|
99
145
|
}
|
|
@@ -108,7 +154,7 @@ export async function setApiKey(keyName, value) {
|
|
|
108
154
|
// 更新当前进程环境变量
|
|
109
155
|
process.env[keyName] = value;
|
|
110
156
|
|
|
111
|
-
// ✅
|
|
157
|
+
// ✅ 安全修复:仅保存到用户工作区
|
|
112
158
|
const env = await loadEnvFile();
|
|
113
159
|
env.set(keyName, value);
|
|
114
160
|
await saveEnvFile(env);
|
package/src/utils/fileUtils.js
CHANGED
|
@@ -5,9 +5,16 @@
|
|
|
5
5
|
|
|
6
6
|
import fs from 'fs-extra';
|
|
7
7
|
import path from 'path';
|
|
8
|
-
import { homedir } from 'os';
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
// ✅ 通用化:支持任意 channel
|
|
10
|
+
function getUserWorkspaceDir(chatId) {
|
|
11
|
+
// OpenClaw 用户工作区: /root/.openclaw/workspaces/{channel}-{user_id}/
|
|
12
|
+
const workspaceBase = process.env.OPENCLAW_WORKSPACE || '/root/.openclaw/workspaces';
|
|
13
|
+
const channel = process.env.OPENCLAW_CHANNEL || 'feishu';
|
|
14
|
+
return path.join(workspaceBase, `${channel}-${chatId}`, '.cuecue');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const CUECUE_DIR = getUserWorkspaceDir(process.env.CHAT_ID || 'default');
|
|
11
18
|
|
|
12
19
|
/**
|
|
13
20
|
* 确保目录存在
|
|
@@ -49,7 +56,7 @@ export async function writeJson(filePath, data) {
|
|
|
49
56
|
* @returns {string} 用户目录路径
|
|
50
57
|
*/
|
|
51
58
|
export function getUserDir(chatId) {
|
|
52
|
-
return path.join(CUECUE_DIR
|
|
59
|
+
return path.join(CUECUE_DIR);
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
/**
|