@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.
@@ -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
+ }
@@ -1,17 +1,23 @@
1
1
  /**
2
- * 环境变量工具 - 安全版本 v1.0.4
3
- * 仅使用 ~/.cuecue 目录,不写入共享配置文件
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
- const CUECUE_DIR = path.join(homedir(), '.cuecue');
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.4 - Secure Environment Variables', '# DO NOT SHARE THIS FILE'];
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. 首先检查 process.env (由 OpenClaw 注入)
92
- if (process.env[keyName]) {
93
- return process.env[keyName];
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
- // 2. 然后检查技能自己的安全文件
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
- // ✅ 安全修复:仅保存到 ~/.cuecue/.env.secure
157
+ // ✅ 安全修复:仅保存到用户工作区
112
158
  const env = await loadEnvFile();
113
159
  env.set(keyName, value);
114
160
  await saveEnvFile(env);
@@ -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
- const CUECUE_DIR = path.join(homedir(), '.cuecue');
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, 'users', chatId);
59
+ return path.join(CUECUE_DIR);
53
60
  }
54
61
 
55
62
  /**