@ian2018cs/agenthub 0.2.5 → 0.2.7
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/dist/assets/index-C2Iik5Ac.js +203 -0
- package/dist/assets/index-JK2KzOYf.css +32 -0
- package/dist/assets/{vendor-icons-BLTKkQIn.js → vendor-icons-ByJAiqkO.js} +1 -1
- package/dist/index.html +3 -3
- package/package.json +2 -2
- package/server/claude-sdk.js +16 -0
- package/server/index.js +7 -0
- package/server/routes/skill-suite.js +347 -0
- package/server/services/builtin-tools/cron-tool.js +63 -15
- package/server/services/user-directories.js +1 -0
- package/dist/assets/index-CqQO7fqd.js +0 -200
- package/dist/assets/index-XCDtdmfg.css +0 -32
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { spawn, exec } from 'child_process';
|
|
5
|
+
import { getUserPaths, getPublicPaths } from '../services/user-directories.js';
|
|
6
|
+
|
|
7
|
+
const router = express.Router();
|
|
8
|
+
|
|
9
|
+
// ─── lark-cli 套装常量 ──────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
const LARK_CLI_SKILLS = [
|
|
12
|
+
'lark-base', 'lark-calendar', 'lark-contact', 'lark-doc', 'lark-drive',
|
|
13
|
+
'lark-event', 'lark-im', 'lark-mail', 'lark-minutes', 'lark-openapi-explorer',
|
|
14
|
+
'lark-shared', 'lark-sheets', 'lark-skill-maker', 'lark-task', 'lark-vc',
|
|
15
|
+
'lark-whiteboard', 'lark-wiki', 'lark-workflow-meeting-summary',
|
|
16
|
+
'lark-workflow-standup-report',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const LARK_CLI_REPO_URL = 'https://github.com/larksuite/cli';
|
|
20
|
+
const LARK_CLI_REPO_OWNER = 'larksuite';
|
|
21
|
+
const LARK_CLI_REPO_NAME = 'cli';
|
|
22
|
+
|
|
23
|
+
// ─── 认证策略表(便于未来扩展其他套装) ────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
const AUTH_STRATEGIES = {
|
|
26
|
+
feishu: {
|
|
27
|
+
getEnv: (larkCliDir) => ({ LARKSUITE_CLI_CONFIG_DIR: larkCliDir }),
|
|
28
|
+
// Phase 1: --no-wait --json → 立即返回 { device_code, verification_url, expires_in }
|
|
29
|
+
noWaitArgs: (domain) => ['auth', 'login', '--domain', domain, '--no-wait', '--json'],
|
|
30
|
+
// Phase 2: --device-code <code> --json → 阻塞直到用户完成授权
|
|
31
|
+
deviceCodeArgs: (code) => ['auth', 'login', '--device-code', code, '--json'],
|
|
32
|
+
domain: 'all',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ─── 辅助:运行命令收集 stdout,返回 JSON ──────────────────────────────────
|
|
37
|
+
|
|
38
|
+
function spawnJson(command, args, env) {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const proc = spawn(command, args, {
|
|
41
|
+
env: { ...process.env, ...env },
|
|
42
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
43
|
+
});
|
|
44
|
+
let stdout = '';
|
|
45
|
+
let stderr = '';
|
|
46
|
+
proc.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
47
|
+
proc.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
48
|
+
proc.on('close', (code) => {
|
|
49
|
+
try {
|
|
50
|
+
resolve(JSON.parse(stdout.trim()));
|
|
51
|
+
} catch {
|
|
52
|
+
reject(new Error(stderr.trim() || `exit ${code}`));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
proc.on('error', reject);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function checkLarkCliAvailable() {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
exec('which lark-cli', { timeout: 3000 }, (err) => resolve(!err));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function checkConfig(larkCliDir) {
|
|
66
|
+
try {
|
|
67
|
+
const configPath = path.join(larkCliDir, 'config.json');
|
|
68
|
+
const content = await fs.readFile(configPath, 'utf8');
|
|
69
|
+
const parsed = JSON.parse(content);
|
|
70
|
+
const appId = parsed?.apps?.[0]?.appId;
|
|
71
|
+
return { ok: !!appId, appId: appId || null };
|
|
72
|
+
} catch {
|
|
73
|
+
return { ok: false, appId: null };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function checkAuth(larkCliDir) {
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
const proc = spawn('lark-cli', ['auth', 'status'], {
|
|
80
|
+
env: { ...process.env, LARKSUITE_CLI_CONFIG_DIR: larkCliDir },
|
|
81
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
let stdout = '';
|
|
85
|
+
const timer = setTimeout(() => {
|
|
86
|
+
proc.kill();
|
|
87
|
+
resolve({ ok: false, userName: null, userOpenId: null });
|
|
88
|
+
}, 5000);
|
|
89
|
+
|
|
90
|
+
proc.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
91
|
+
|
|
92
|
+
proc.on('close', () => {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(stdout.trim());
|
|
96
|
+
if (parsed.userName && parsed.tokenStatus !== 'expired') {
|
|
97
|
+
resolve({ ok: true, userName: parsed.userName, userOpenId: parsed.userOpenId || null });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
} catch { /* ignore */ }
|
|
101
|
+
resolve({ ok: false, userName: null, userOpenId: null });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
proc.on('error', () => {
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
resolve({ ok: false, userName: null, userOpenId: null });
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function checkSkills(skillsDir) {
|
|
112
|
+
let installed = 0;
|
|
113
|
+
const installedList = [];
|
|
114
|
+
for (const name of LARK_CLI_SKILLS) {
|
|
115
|
+
try {
|
|
116
|
+
await fs.lstat(path.join(skillsDir, name));
|
|
117
|
+
installed++;
|
|
118
|
+
installedList.push(name);
|
|
119
|
+
} catch {
|
|
120
|
+
// not installed
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { ok: installed === LARK_CLI_SKILLS.length, installed, total: LARK_CLI_SKILLS.length, installedList };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── 端点1:GET /status ─────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
router.get('/status', async (req, res) => {
|
|
129
|
+
try {
|
|
130
|
+
const userUuid = req.user.uuid;
|
|
131
|
+
const paths = getUserPaths(userUuid);
|
|
132
|
+
|
|
133
|
+
const larkCliInstalled = await checkLarkCliAvailable();
|
|
134
|
+
const envConfigured = !!(process.env.FEISHU_APP_ID && process.env.FEISHU_APP_SECRET);
|
|
135
|
+
|
|
136
|
+
const config = await checkConfig(paths.larkCliDir);
|
|
137
|
+
const auth = config.ok
|
|
138
|
+
? await checkAuth(paths.larkCliDir)
|
|
139
|
+
: { ok: false, userName: null, userOpenId: null };
|
|
140
|
+
const skills = await checkSkills(paths.skillsDir);
|
|
141
|
+
|
|
142
|
+
const overall = config.ok && auth.ok && skills.ok ? 'installed' : 'incomplete';
|
|
143
|
+
|
|
144
|
+
res.json({ larkCliInstalled, envConfigured, config, auth, skills, overall });
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error('[skill-suite] status error:', err);
|
|
147
|
+
res.status(500).json({ error: err.message });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ─── 端点2:POST /init-config ────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
router.post('/init-config', async (req, res) => {
|
|
154
|
+
try {
|
|
155
|
+
const appId = process.env.FEISHU_APP_ID;
|
|
156
|
+
const appSecret = process.env.FEISHU_APP_SECRET;
|
|
157
|
+
if (!appId || !appSecret) {
|
|
158
|
+
return res.status(400).json({ error: '请先配置 FEISHU_APP_ID 和 FEISHU_APP_SECRET 环境变量' });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const userUuid = req.user.uuid;
|
|
162
|
+
const paths = getUserPaths(userUuid);
|
|
163
|
+
const configPath = path.join(paths.larkCliDir, 'config.json');
|
|
164
|
+
|
|
165
|
+
// 幂等:已有有效配置则跳过
|
|
166
|
+
const existing = await checkConfig(paths.larkCliDir);
|
|
167
|
+
if (existing.ok) {
|
|
168
|
+
return res.json({ success: true, alreadyExists: true, appId: existing.appId });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
await fs.mkdir(paths.larkCliDir, { recursive: true });
|
|
172
|
+
|
|
173
|
+
const config = {
|
|
174
|
+
apps: [{
|
|
175
|
+
appId,
|
|
176
|
+
appSecret,
|
|
177
|
+
brand: 'feishu',
|
|
178
|
+
lang: 'zh',
|
|
179
|
+
}],
|
|
180
|
+
};
|
|
181
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
182
|
+
|
|
183
|
+
res.json({ success: true, alreadyExists: false, appId });
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.error('[skill-suite] init-config error:', err);
|
|
186
|
+
res.status(500).json({ error: err.message });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ─── 端点3:POST /install-skills ────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
router.post('/install-skills', async (req, res) => {
|
|
193
|
+
try {
|
|
194
|
+
const userUuid = req.user.uuid;
|
|
195
|
+
const paths = getUserPaths(userUuid);
|
|
196
|
+
const publicPaths = getPublicPaths();
|
|
197
|
+
|
|
198
|
+
const publicRepoPath = path.join(publicPaths.skillsRepoDir, LARK_CLI_REPO_OWNER, LARK_CLI_REPO_NAME);
|
|
199
|
+
|
|
200
|
+
// 确保公共仓库已克隆
|
|
201
|
+
let repoExists = false;
|
|
202
|
+
try {
|
|
203
|
+
await fs.access(publicRepoPath);
|
|
204
|
+
repoExists = true;
|
|
205
|
+
} catch {
|
|
206
|
+
repoExists = false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!repoExists) {
|
|
210
|
+
const parentDir = path.join(publicPaths.skillsRepoDir, LARK_CLI_REPO_OWNER);
|
|
211
|
+
await fs.mkdir(parentDir, { recursive: true });
|
|
212
|
+
await new Promise((resolve, reject) => {
|
|
213
|
+
const proc = spawn('git', ['clone', '--depth', '1', LARK_CLI_REPO_URL, publicRepoPath], {
|
|
214
|
+
stdio: 'inherit',
|
|
215
|
+
});
|
|
216
|
+
proc.on('close', (code) => {
|
|
217
|
+
if (code === 0) resolve();
|
|
218
|
+
else reject(new Error(`git clone failed with exit code ${code}`));
|
|
219
|
+
});
|
|
220
|
+
proc.on('error', reject);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 确保用户的 skillsRepoDir 符号链接存在
|
|
225
|
+
const userRepoLinkParent = path.join(paths.skillsRepoDir, LARK_CLI_REPO_OWNER);
|
|
226
|
+
const userRepoLink = path.join(userRepoLinkParent, LARK_CLI_REPO_NAME);
|
|
227
|
+
await fs.mkdir(userRepoLinkParent, { recursive: true });
|
|
228
|
+
try {
|
|
229
|
+
await fs.lstat(userRepoLink);
|
|
230
|
+
} catch {
|
|
231
|
+
await fs.symlink(publicRepoPath, userRepoLink);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 批量创建技能符号链接
|
|
235
|
+
await fs.mkdir(paths.skillsDir, { recursive: true });
|
|
236
|
+
const skillsSourceDir = path.join(publicRepoPath, 'skills');
|
|
237
|
+
const results = [];
|
|
238
|
+
|
|
239
|
+
for (const name of LARK_CLI_SKILLS) {
|
|
240
|
+
const sourcePath = path.join(skillsSourceDir, name);
|
|
241
|
+
const linkPath = path.join(paths.skillsDir, name);
|
|
242
|
+
|
|
243
|
+
// 检查源目录是否存在
|
|
244
|
+
try {
|
|
245
|
+
await fs.access(path.join(sourcePath, 'SKILL.md'));
|
|
246
|
+
} catch {
|
|
247
|
+
results.push({ name, ok: false, reason: 'not found in repo' });
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 幂等:先删除旧链接再创建
|
|
252
|
+
try {
|
|
253
|
+
await fs.lstat(linkPath);
|
|
254
|
+
await fs.unlink(linkPath);
|
|
255
|
+
} catch {
|
|
256
|
+
// 不存在,正常继续
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
await fs.symlink(sourcePath, linkPath);
|
|
261
|
+
results.push({ name, ok: true });
|
|
262
|
+
} catch (err) {
|
|
263
|
+
results.push({ name, ok: false, reason: err.message });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const installed = results.filter((r) => r.ok).length;
|
|
268
|
+
res.json({ success: true, installed, total: LARK_CLI_SKILLS.length, results });
|
|
269
|
+
} catch (err) {
|
|
270
|
+
console.error('[skill-suite] install-skills error:', err);
|
|
271
|
+
res.status(500).json({ error: err.message });
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// ─── 端点4:GET /auth (SSE,策略模式,两阶段 JSON)──────────────────────
|
|
276
|
+
|
|
277
|
+
router.get('/auth', async (req, res) => {
|
|
278
|
+
const type = req.query.type || 'feishu';
|
|
279
|
+
const strategy = AUTH_STRATEGIES[type];
|
|
280
|
+
if (!strategy) {
|
|
281
|
+
return res.status(400).json({ error: `Unknown auth type: ${type}` });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 设置 SSE 响应头
|
|
285
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
286
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
287
|
+
res.setHeader('Connection', 'keep-alive');
|
|
288
|
+
res.flushHeaders();
|
|
289
|
+
|
|
290
|
+
const sendEvent = (event, data) => {
|
|
291
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
let phase2Proc = null;
|
|
295
|
+
|
|
296
|
+
req.on('close', () => { phase2Proc?.kill(); });
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const userUuid = req.user.uuid;
|
|
300
|
+
const paths = getUserPaths(userUuid);
|
|
301
|
+
const env = strategy.getEnv(paths.larkCliDir);
|
|
302
|
+
|
|
303
|
+
// 检查 config 是否存在
|
|
304
|
+
const configCheck = await checkConfig(paths.larkCliDir);
|
|
305
|
+
if (!configCheck.ok) {
|
|
306
|
+
sendEvent('error', { message: '请先完成配置步骤' });
|
|
307
|
+
return res.end();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Phase 1: --no-wait --json,立即获取 verification_url 和 device_code
|
|
311
|
+
const phase1 = await spawnJson('lark-cli', strategy.noWaitArgs(strategy.domain), env);
|
|
312
|
+
const { device_code, verification_url } = phase1;
|
|
313
|
+
sendEvent('url', { url: verification_url });
|
|
314
|
+
|
|
315
|
+
// Phase 2: --device-code,阻塞等待用户在浏览器完成授权
|
|
316
|
+
// 注意:--json 对 --device-code 子命令不生效(成功信息走 stderr 文本),
|
|
317
|
+
// 所以改为:exit code 0 = 成功,再调 auth status 取用户信息
|
|
318
|
+
await new Promise((resolve, reject) => {
|
|
319
|
+
phase2Proc = spawn('lark-cli', strategy.deviceCodeArgs(device_code), {
|
|
320
|
+
env: { ...process.env, ...env },
|
|
321
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
let stderr = '';
|
|
325
|
+
phase2Proc.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
326
|
+
|
|
327
|
+
phase2Proc.on('close', async (code) => {
|
|
328
|
+
if (code === 0) {
|
|
329
|
+
// 授权成功,调 auth status 获取用户信息
|
|
330
|
+
const authInfo = await checkAuth(env.LARKSUITE_CLI_CONFIG_DIR);
|
|
331
|
+
sendEvent('success', { user: authInfo.userName || null });
|
|
332
|
+
resolve();
|
|
333
|
+
} else {
|
|
334
|
+
reject(new Error(stderr.trim() || `授权失败 (exit ${code})`));
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
phase2Proc.on('error', reject);
|
|
339
|
+
});
|
|
340
|
+
} catch (err) {
|
|
341
|
+
sendEvent('error', { message: err.message });
|
|
342
|
+
} finally {
|
|
343
|
+
res.end();
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
export default router;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
|
+
import fs from 'fs';
|
|
2
3
|
import cron from 'node-cron';
|
|
3
4
|
import { cronScheduler } from './cron-scheduler.js';
|
|
4
5
|
import { feishuDb } from '../../database/db.js';
|
|
@@ -78,17 +79,35 @@ export const cronCreateTool = {
|
|
|
78
79
|
|
|
79
80
|
systemPrompt: `
|
|
80
81
|
## 定时任务(Cron)
|
|
81
|
-
使用以下 Bash
|
|
82
|
+
使用以下 Bash 命令操作定时任务(任务持久化到数据库,服务重启自动恢复,循环任务永久运行直到用户手动删除,**没有 7 天过期限制**):
|
|
82
83
|
|
|
83
|
-
###
|
|
84
|
+
### 创建定时任务(**必须用文件方式**,避免 prompt 含特殊字符时解析失败)
|
|
85
|
+
|
|
86
|
+
**步骤一**:用 Write 工具把参数写入临时文件(纯文本,无需 JSON,文件名用时间戳保证唯一):
|
|
87
|
+
\`\`\`
|
|
88
|
+
Write /tmp/<userUuid>/cron/task_<timestamp>.txt
|
|
89
|
+
<第1行: cron 表达式>
|
|
90
|
+
<第2行: true 或 false(是否循环)>
|
|
91
|
+
<第3行起: prompt 内容,原样保留,支持任意字符和换行>
|
|
92
|
+
\`\`\`
|
|
93
|
+
|
|
94
|
+
**示例**:
|
|
95
|
+
\`\`\`
|
|
96
|
+
Write /tmp/abc123/cron/task_1700000000.txt
|
|
97
|
+
30 9 * * *
|
|
98
|
+
true
|
|
99
|
+
[channel:feishu]每天查询前天广告收入,分析数据并发送到飞书群
|
|
100
|
+
\`\`\`
|
|
101
|
+
|
|
102
|
+
**步骤二**:把文件路径传给 __cron_create__:
|
|
84
103
|
\`\`\`
|
|
85
|
-
Bash(__cron_create__
|
|
104
|
+
Bash(__cron_create__ /tmp/abc123/cron/task_1700000000.txt)
|
|
86
105
|
\`\`\`
|
|
87
106
|
|
|
88
107
|
参数说明:
|
|
89
|
-
- cron: 标准 5 字段 cron 表达式(如 "*/5 * * * *")
|
|
90
|
-
-
|
|
91
|
-
-
|
|
108
|
+
- 第1行 cron: 标准 5 字段 cron 表达式(如 "*/5 * * * *")
|
|
109
|
+
- 第2行 recurring: true=循环(永久),false=单次(触发后删除)
|
|
110
|
+
- 第3行起 prompt: 触发时执行的内容,开头可加前缀指定会话方式和投递渠道
|
|
92
111
|
|
|
93
112
|
### 前缀说明(**创建前务必询问用户**,在 prompt 开头使用,顺序任意,均可选):
|
|
94
113
|
|
|
@@ -124,12 +143,41 @@ Bash(__cron_list__)
|
|
|
124
143
|
},
|
|
125
144
|
|
|
126
145
|
async execute(hookInput, ctx) {
|
|
127
|
-
const rawArgs = hookInput.tool_input.command.replace(/^\s*__cron_create__\s*/, '');
|
|
146
|
+
const rawArgs = hookInput.tool_input.command.replace(/^\s*__cron_create__\s*/, '').trim();
|
|
128
147
|
let params;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
148
|
+
|
|
149
|
+
if (rawArgs.startsWith('/')) {
|
|
150
|
+
// 文件路径模式:纯文本格式(无 JSON,彻底避免转义问题)
|
|
151
|
+
// 第1行: cron 表达式
|
|
152
|
+
// 第2行: recurring (true/false)
|
|
153
|
+
// 第3行起: prompt 内容(原样保留,支持任意字符和换行)
|
|
154
|
+
let fileContent;
|
|
155
|
+
try {
|
|
156
|
+
fileContent = fs.readFileSync(rawArgs, 'utf8');
|
|
157
|
+
} catch (e) {
|
|
158
|
+
return { decision: 'deny', reason: `读取参数文件失败: ${e.message}(路径: ${rawArgs})` };
|
|
159
|
+
} finally {
|
|
160
|
+
try { fs.unlinkSync(rawArgs); } catch {}
|
|
161
|
+
}
|
|
162
|
+
const lines = fileContent.split('\n');
|
|
163
|
+
const cronExprLine = lines[0]?.trim();
|
|
164
|
+
const recurringLine = lines[1]?.trim().toLowerCase();
|
|
165
|
+
const promptText = lines.slice(2).join('\n').trim();
|
|
166
|
+
if (!cronExprLine || !promptText) {
|
|
167
|
+
return { decision: 'deny', reason: '❌ 参数文件格式错误:第1行应为 cron 表达式,第2行为 true/false,第3行起为 prompt 内容' };
|
|
168
|
+
}
|
|
169
|
+
params = {
|
|
170
|
+
cron: cronExprLine,
|
|
171
|
+
recurring: recurringLine !== 'false',
|
|
172
|
+
prompt: promptText,
|
|
173
|
+
};
|
|
174
|
+
} else {
|
|
175
|
+
// 内联 JSON 模式(兼容旧用法,不推荐)
|
|
176
|
+
try {
|
|
177
|
+
params = parseJsonArg(rawArgs);
|
|
178
|
+
} catch (e) {
|
|
179
|
+
return { decision: 'deny', reason: `参数解析失败: ${e.message}。请改用文件方式:Write /tmp/<uuid>/cron/task_<ts>.txt,第1行cron表达式,第2行true/false,第3行起prompt内容,再调 Bash(__cron_create__ /tmp/.../task.txt)` };
|
|
180
|
+
}
|
|
133
181
|
}
|
|
134
182
|
|
|
135
183
|
const { cron: cronExpr, prompt: rawPrompt, recurring = true } = params;
|
|
@@ -197,10 +245,10 @@ Bash(__cron_list__)
|
|
|
197
245
|
return {
|
|
198
246
|
decision: 'deny',
|
|
199
247
|
reason: [
|
|
200
|
-
`✅
|
|
248
|
+
`✅ 定时任务已创建并持久化(无过期限制,永久有效)`,
|
|
201
249
|
`任务 ID: ${id}`,
|
|
202
250
|
`频率: ${cronExpr}`,
|
|
203
|
-
`类型: ${recurring ? '
|
|
251
|
+
`类型: ${recurring ? '循环(永久运行)' : '单次(触发后删除)'}`,
|
|
204
252
|
`会话方式: ${sessionDesc}`,
|
|
205
253
|
`投递渠道: ${channelDesc}`,
|
|
206
254
|
`内容: ${prompt}`,
|
|
@@ -272,8 +320,8 @@ export const cronListTool = {
|
|
|
272
320
|
// ─── SDK 原生工具重定向(拦截并引导 AI 使用 Bash 版本)─────────────────────────
|
|
273
321
|
|
|
274
322
|
const SDK_CRON_REDIRECT = `❌ 请勿使用 SDK 内置的 CronCreate/CronDelete/CronList 工具,它们在本系统中无法持久化。
|
|
275
|
-
请改用以下 Bash
|
|
276
|
-
-
|
|
323
|
+
请改用以下 Bash 命令(必须用文件方式创建,避免 prompt 特殊字符导致解析失败):
|
|
324
|
+
- 创建:先 Write /tmp/<uuid>/cron/task_<ts>.txt(第1行cron表达式,第2行true/false,第3行起prompt),再 Bash(__cron_create__ /tmp/<uuid>/cron/task_<ts>.txt)
|
|
277
325
|
- 删除:Bash(__cron_delete__ '{"id":"任务ID"}')
|
|
278
326
|
- 列表:Bash(__cron_list__)`;
|
|
279
327
|
|
|
@@ -31,6 +31,7 @@ export function getUserPaths(userUuid) {
|
|
|
31
31
|
mcpRepoDir: path.join(DATA_DIR, 'user-data', userUuid, 'mcp-repo'),
|
|
32
32
|
codexHomeDir: path.join(DATA_DIR, 'user-data', userUuid, 'codex-home'),
|
|
33
33
|
geminiHomeDir: path.join(DATA_DIR, 'user-data', userUuid, 'gemini-home'),
|
|
34
|
+
larkCliDir: path.join(DATA_DIR, 'user-data', userUuid, '.lark-cli'),
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
37
|
|