shipmyagent 1.0.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 (45) hide show
  1. package/README.md +19 -0
  2. package/bin/cli.d.ts.map +1 -0
  3. package/bin/cli.js +24 -0
  4. package/bin/cli.js.map +1 -0
  5. package/bin/commands/init.d.ts.map +1 -0
  6. package/bin/commands/init.js +127 -0
  7. package/bin/commands/init.js.map +1 -0
  8. package/bin/commands/start.d.ts.map +1 -0
  9. package/bin/commands/start.js +114 -0
  10. package/bin/commands/start.js.map +1 -0
  11. package/bin/index.d.ts.map +1 -0
  12. package/bin/index.js +4 -0
  13. package/bin/index.js.map +1 -0
  14. package/bin/integrations/telegram.d.ts.map +1 -0
  15. package/bin/integrations/telegram.js +199 -0
  16. package/bin/integrations/telegram.js.map +1 -0
  17. package/bin/runtime/agent.d.ts.map +1 -0
  18. package/bin/runtime/agent.js +990 -0
  19. package/bin/runtime/agent.js.map +1 -0
  20. package/bin/runtime/index.d.ts.map +1 -0
  21. package/bin/runtime/index.js +7 -0
  22. package/bin/runtime/index.js.map +1 -0
  23. package/bin/runtime/logger.d.ts.map +1 -0
  24. package/bin/runtime/logger.js +103 -0
  25. package/bin/runtime/logger.js.map +1 -0
  26. package/bin/runtime/permission.d.ts.map +1 -0
  27. package/bin/runtime/permission.js +225 -0
  28. package/bin/runtime/permission.js.map +1 -0
  29. package/bin/runtime/scheduler.d.ts.map +1 -0
  30. package/bin/runtime/scheduler.js +148 -0
  31. package/bin/runtime/scheduler.js.map +1 -0
  32. package/bin/runtime/task-executor.d.ts.map +1 -0
  33. package/bin/runtime/task-executor.js +111 -0
  34. package/bin/runtime/task-executor.js.map +1 -0
  35. package/bin/runtime/tools.d.ts.map +1 -0
  36. package/bin/runtime/tools.js +224 -0
  37. package/bin/runtime/tools.js.map +1 -0
  38. package/bin/server/index.d.ts.map +1 -0
  39. package/bin/server/index.js +210 -0
  40. package/bin/server/index.js.map +1 -0
  41. package/bin/shipmyagent.js +64 -0
  42. package/bin/utils.d.ts.map +1 -0
  43. package/bin/utils.js +154 -0
  44. package/bin/utils.js.map +1 -0
  45. package/package.json +52 -0
@@ -0,0 +1,990 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { z } from 'zod';
4
+ import { generateId, getAgentMdPath, getShipJsonPath, getShipDirPath, getApprovalsDirPath, getLogsDirPath, getTimestamp, } from '../utils.js';
5
+ // ==================== Permission Engine ====================
6
+ class PermissionEngine {
7
+ context;
8
+ constructor(context) {
9
+ this.context = context;
10
+ }
11
+ /**
12
+ * 检查是否允许执行某个操作
13
+ */
14
+ canPerform(action, data) {
15
+ const { config } = this.context;
16
+ switch (action) {
17
+ case 'read_repo':
18
+ const readConfig = config.permissions.read_repo;
19
+ if (typeof readConfig === 'boolean') {
20
+ return { allowed: readConfig, requiresApproval: false };
21
+ }
22
+ // 如果有路径限制,检查路径
23
+ if (readConfig.paths && data?.path) {
24
+ const allowed = readConfig.paths.some(p => data.path.includes(p.replace('**/*', '')));
25
+ return { allowed, requiresApproval: false };
26
+ }
27
+ return { allowed: true, requiresApproval: false };
28
+ case 'write_repo':
29
+ const writeConfig = config.permissions.write_repo;
30
+ if (!writeConfig) {
31
+ return { allowed: false, requiresApproval: true, reason: '写入权限未配置' };
32
+ }
33
+ if (writeConfig.paths && data?.path) {
34
+ const allowed = writeConfig.paths.some(p => data.path.includes(p.replace('**/*', '')));
35
+ return {
36
+ allowed: writeConfig.requiresApproval ? false : allowed,
37
+ requiresApproval: writeConfig.requiresApproval,
38
+ reason: allowed ? undefined : '路径不在允许范围内'
39
+ };
40
+ }
41
+ return {
42
+ allowed: false,
43
+ requiresApproval: writeConfig.requiresApproval,
44
+ reason: '写入需要审批'
45
+ };
46
+ case 'exec_shell':
47
+ const execConfig = config.permissions.exec_shell;
48
+ if (!execConfig) {
49
+ return { allowed: false, requiresApproval: true, reason: 'Shell 执行权限未配置' };
50
+ }
51
+ if (execConfig.allow && data?.command) {
52
+ const allowed = execConfig.allow.some(cmd => data.command.startsWith(cmd));
53
+ return {
54
+ allowed: execConfig.requiresApproval ? false : allowed,
55
+ requiresApproval: execConfig.requiresApproval,
56
+ reason: allowed ? undefined : '命令不在允许列表中'
57
+ };
58
+ }
59
+ return {
60
+ allowed: false,
61
+ requiresApproval: execConfig.requiresApproval,
62
+ reason: 'Shell 执行需要审批'
63
+ };
64
+ default:
65
+ return { allowed: false, requiresApproval: true, reason: `未知操作: ${action}` };
66
+ }
67
+ }
68
+ /**
69
+ * 创建审批请求
70
+ */
71
+ async createApproval(type, description, tool, input) {
72
+ const approvalsDir = getApprovalsDirPath(this.context.projectRoot);
73
+ await fs.ensureDir(approvalsDir);
74
+ const approval = {
75
+ id: generateId(),
76
+ timestamp: getTimestamp(),
77
+ type,
78
+ description,
79
+ tool,
80
+ input,
81
+ status: 'pending',
82
+ };
83
+ const approvalFile = path.join(approvalsDir, `${approval.id}.json`);
84
+ await fs.writeJson(approvalFile, approval, { spaces: 2 });
85
+ return approval;
86
+ }
87
+ /**
88
+ * 获取待审批请求
89
+ */
90
+ async getPendingApprovals() {
91
+ const approvalsDir = getApprovalsDirPath(this.context.projectRoot);
92
+ if (!fs.existsSync(approvalsDir)) {
93
+ return [];
94
+ }
95
+ const files = await fs.readdir(approvalsDir);
96
+ const approvals = [];
97
+ for (const file of files) {
98
+ if (file.endsWith('.json')) {
99
+ const content = await fs.readJson(path.join(approvalsDir, file));
100
+ if (content.status === 'pending') {
101
+ approvals.push(content);
102
+ }
103
+ }
104
+ }
105
+ return approvals.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
106
+ }
107
+ /**
108
+ * 审批操作
109
+ */
110
+ async approve(approvalId, approvedBy) {
111
+ const approvalsDir = getApprovalsDirPath(this.context.projectRoot);
112
+ const approvalFile = path.join(approvalsDir, `${approvalId}.json`);
113
+ if (!fs.existsSync(approvalFile)) {
114
+ return false;
115
+ }
116
+ const approval = await fs.readJson(approvalFile);
117
+ approval.status = 'approved';
118
+ approval.approvedBy = approvedBy;
119
+ approval.approvedAt = getTimestamp();
120
+ await fs.writeJson(approvalFile, approval, { spaces: 2 });
121
+ return true;
122
+ }
123
+ /**
124
+ * 拒绝操作
125
+ */
126
+ async reject(approvalId, rejectedBy) {
127
+ const approvalsDir = getApprovalsDirPath(this.context.projectRoot);
128
+ const approvalFile = path.join(approvalsDir, `${approvalId}.json`);
129
+ if (!fs.existsSync(approvalFile)) {
130
+ return false;
131
+ }
132
+ const approval = await fs.readJson(approvalFile);
133
+ approval.status = 'rejected';
134
+ approval.approvedBy = rejectedBy;
135
+ approval.approvedAt = getTimestamp();
136
+ await fs.writeJson(approvalFile, approval, { spaces: 2 });
137
+ return true;
138
+ }
139
+ }
140
+ // ==================== Logger ====================
141
+ class AgentLogger {
142
+ projectRoot;
143
+ constructor(projectRoot) {
144
+ this.projectRoot = projectRoot;
145
+ }
146
+ async log(level, message, data) {
147
+ const logsDir = getLogsDirPath(this.projectRoot);
148
+ await fs.ensureDir(logsDir);
149
+ const logEntry = {
150
+ timestamp: getTimestamp(),
151
+ level,
152
+ message,
153
+ ...(data || {}),
154
+ };
155
+ const today = new Date().toISOString().split('T')[0];
156
+ const logFile = path.join(logsDir, `${today}.json`);
157
+ // Append to log file
158
+ const existingLogs = fs.existsSync(logFile)
159
+ ? await fs.readJson(logFile)
160
+ : [];
161
+ existingLogs.push(logEntry);
162
+ await fs.writeJson(logFile, existingLogs, { spaces: 2 });
163
+ // Also output to console
164
+ const colors = {
165
+ info: '\x1b[32m',
166
+ warn: '\x1b[33m',
167
+ error: '\x1b[31m',
168
+ debug: '\x1b[36m',
169
+ };
170
+ const color = colors[level] || '\x1b[0m';
171
+ console.log(`${color}[${level.toUpperCase()}]${'\x1b[0m'} ${message}`);
172
+ }
173
+ }
174
+ // ==================== Agent Tools ====================
175
+ export class AgentTools {
176
+ context;
177
+ permissionEngine;
178
+ logger;
179
+ constructor(context) {
180
+ this.context = context;
181
+ this.permissionEngine = new PermissionEngine(context);
182
+ this.logger = new AgentLogger(context.projectRoot);
183
+ }
184
+ /**
185
+ * 获取所有工具定义
186
+ */
187
+ getToolDefinitions() {
188
+ return [
189
+ {
190
+ name: 'read_file',
191
+ description: '读取文件内容',
192
+ parameters: {
193
+ type: 'object',
194
+ properties: {
195
+ path: { type: 'string', description: '文件路径' },
196
+ encoding: { type: 'string', description: '编码格式,默认 utf-8' },
197
+ },
198
+ required: ['path'],
199
+ },
200
+ },
201
+ {
202
+ name: 'list_files',
203
+ description: '列出目录中的文件',
204
+ parameters: {
205
+ type: 'object',
206
+ properties: {
207
+ path: { type: 'string', description: '目录路径' },
208
+ pattern: { type: 'string', description: '文件匹配模式' },
209
+ },
210
+ required: ['path'],
211
+ },
212
+ },
213
+ {
214
+ name: 'search_files',
215
+ description: '搜索文件内容',
216
+ parameters: {
217
+ type: 'object',
218
+ properties: {
219
+ pattern: { type: 'string', description: '搜索关键词' },
220
+ path: { type: 'string', description: '搜索目录' },
221
+ glob: { type: 'string', description: '文件类型过滤' },
222
+ },
223
+ required: ['pattern'],
224
+ },
225
+ },
226
+ {
227
+ name: 'write_file',
228
+ description: '创建或修改文件',
229
+ parameters: {
230
+ type: 'object',
231
+ properties: {
232
+ path: { type: 'string', description: '文件路径' },
233
+ content: { type: 'string', description: '文件内容' },
234
+ },
235
+ required: ['path', 'content'],
236
+ },
237
+ },
238
+ {
239
+ name: 'delete_file',
240
+ description: '删除文件或目录',
241
+ parameters: {
242
+ type: 'object',
243
+ properties: {
244
+ path: { type: 'string', description: '文件或目录路径' },
245
+ },
246
+ required: ['path'],
247
+ },
248
+ },
249
+ {
250
+ name: 'exec_shell',
251
+ description: '执行 Shell 命令',
252
+ parameters: {
253
+ type: 'object',
254
+ properties: {
255
+ command: { type: 'string', description: '要执行的命令' },
256
+ timeout: { type: 'number', description: '超时时间(毫秒)' },
257
+ },
258
+ required: ['command'],
259
+ },
260
+ },
261
+ {
262
+ name: 'get_status',
263
+ description: '获取 Agent 和项目状态',
264
+ parameters: {
265
+ type: 'object',
266
+ properties: {},
267
+ required: [],
268
+ },
269
+ },
270
+ {
271
+ name: 'get_tasks',
272
+ description: '获取任务列表',
273
+ parameters: {
274
+ type: 'object',
275
+ properties: {},
276
+ required: [],
277
+ },
278
+ },
279
+ {
280
+ name: 'get_pending_approvals',
281
+ description: '获取待审批请求',
282
+ parameters: {
283
+ type: 'object',
284
+ properties: {},
285
+ required: [],
286
+ },
287
+ },
288
+ {
289
+ name: 'approve',
290
+ description: '审批操作请求',
291
+ parameters: {
292
+ type: 'object',
293
+ properties: {
294
+ approvalId: { type: 'string', description: '审批请求 ID' },
295
+ approved: { type: 'boolean', description: '是否批准' },
296
+ },
297
+ required: ['approvalId'],
298
+ },
299
+ },
300
+ {
301
+ name: 'create_diff',
302
+ description: '创建代码 diff 并请求审批',
303
+ parameters: {
304
+ type: 'object',
305
+ properties: {
306
+ filePath: { type: 'string', description: '文件路径' },
307
+ original: { type: 'string', description: '原始内容' },
308
+ modified: { type: 'string', description: '修改后内容' },
309
+ },
310
+ required: ['filePath', 'original', 'modified'],
311
+ },
312
+ },
313
+ ];
314
+ }
315
+ /**
316
+ * 执行工具调用
317
+ */
318
+ async executeTool(toolName, args) {
319
+ try {
320
+ switch (toolName) {
321
+ case 'read_file':
322
+ return await this.toolReadFile(args);
323
+ case 'list_files':
324
+ return await this.toolListFiles(args);
325
+ case 'search_files':
326
+ return await this.toolSearchFiles(args);
327
+ case 'write_file':
328
+ return await this.toolWriteFile(args);
329
+ case 'delete_file':
330
+ return await this.toolDeleteFile(args);
331
+ case 'exec_shell':
332
+ return await this.toolExecShell(args);
333
+ case 'get_status':
334
+ return await this.toolGetStatus(args);
335
+ case 'get_tasks':
336
+ return await this.toolGetTasks(args);
337
+ case 'get_pending_approvals':
338
+ return await this.toolGetPendingApprovals(args);
339
+ case 'approve':
340
+ return await this.toolApprove(args);
341
+ case 'create_diff':
342
+ return await this.toolCreateDiff(args);
343
+ default:
344
+ return { success: false, result: null, error: `未知工具: ${toolName}` };
345
+ }
346
+ }
347
+ catch (error) {
348
+ await this.logger.log('error', `工具执行失败: ${toolName}`, { error: String(error) });
349
+ return { success: false, result: null, error: String(error) };
350
+ }
351
+ }
352
+ async toolReadFile(args) {
353
+ const { path: filePath, encoding = 'utf-8' } = args;
354
+ // 检查权限
355
+ const permission = this.permissionEngine.canPerform('read_repo', { path: filePath });
356
+ if (!permission.allowed) {
357
+ return { success: false, result: `无权限读取文件: ${filePath}` };
358
+ }
359
+ // 检查文件是否存在
360
+ if (!fs.existsSync(filePath)) {
361
+ return { success: false, result: `文件不存在: ${filePath}` };
362
+ }
363
+ const content = await fs.readFile(filePath, encoding);
364
+ await this.logger.log('debug', `读取文件: ${filePath}`);
365
+ return { success: true, result: content };
366
+ }
367
+ async toolListFiles(args) {
368
+ const { path: dirPath, pattern = '**/*' } = args;
369
+ const permission = this.permissionEngine.canPerform('read_repo', { path: dirPath });
370
+ if (!permission.allowed) {
371
+ return { success: false, result: `无权限访问目录: ${dirPath}` };
372
+ }
373
+ const globImport = await import('fast-glob');
374
+ const files = await globImport.default([`${dirPath}/${pattern}`], {
375
+ cwd: this.context.projectRoot,
376
+ ignore: ['node_modules/**', '.git/**', '.ship/**'],
377
+ });
378
+ await this.logger.log('debug', `列出文件: ${dirPath}`, { count: files.length });
379
+ return { success: true, result: files };
380
+ }
381
+ async toolSearchFiles(args) {
382
+ const { pattern, path: searchPath, glob = '**/*' } = args;
383
+ const permission = this.permissionEngine.canPerform('read_repo', { path: searchPath });
384
+ if (!permission.allowed) {
385
+ return { success: false, result: `无权限搜索: ${searchPath}` };
386
+ }
387
+ const results = [];
388
+ const globImport = await import('fast-glob');
389
+ const files = await globImport.default([`${searchPath}/${glob}`], {
390
+ cwd: this.context.projectRoot,
391
+ ignore: ['node_modules/**', '.git/**', '.ship/**'],
392
+ });
393
+ for (const file of files) {
394
+ try {
395
+ const content = await fs.readFile(file, 'utf-8');
396
+ const lines = content.split('\n');
397
+ lines.forEach((line, index) => {
398
+ if (line.toLowerCase().includes(pattern.toLowerCase())) {
399
+ results.push({
400
+ file,
401
+ line: index + 1,
402
+ content: line.trim(),
403
+ });
404
+ }
405
+ });
406
+ }
407
+ catch {
408
+ // 忽略读取错误
409
+ }
410
+ }
411
+ await this.logger.log('debug', `搜索文件: ${pattern}`, { count: results.length });
412
+ return { success: true, result: results };
413
+ }
414
+ async toolWriteFile(args) {
415
+ const { path: filePath, content } = args;
416
+ const permission = this.permissionEngine.canPerform('write_repo', { path: filePath });
417
+ if (!permission.allowed) {
418
+ if (permission.requiresApproval) {
419
+ // 需要审批
420
+ const approval = await this.permissionEngine.createApproval('write_repo', `写入文件: ${filePath}`, 'write_file', args);
421
+ return { success: false, result: `需要审批才能写入文件`, pendingApproval: approval };
422
+ }
423
+ return { success: false, result: `无权限写入文件: ${filePath}` };
424
+ }
425
+ await fs.ensureDir(path.dirname(filePath));
426
+ await fs.writeFile(filePath, content);
427
+ await this.logger.log('info', `写入文件: ${filePath}`);
428
+ return { success: true, result: `文件已写入: ${filePath}` };
429
+ }
430
+ async toolDeleteFile(args) {
431
+ const { path: filePath } = args;
432
+ const permission = this.permissionEngine.canPerform('write_repo', { path: filePath });
433
+ if (!permission.allowed) {
434
+ if (permission.requiresApproval) {
435
+ const approval = await this.permissionEngine.createApproval('write_repo', `删除文件: ${filePath}`, 'delete_file', args);
436
+ return { success: false, result: `需要审批才能删除文件`, pendingApproval: approval };
437
+ }
438
+ return { success: false, result: `无权限删除文件: ${filePath}` };
439
+ }
440
+ if (fs.existsSync(filePath)) {
441
+ await fs.remove(filePath);
442
+ await this.logger.log('info', `删除文件: ${filePath}`);
443
+ return { success: true, result: `文件已删除: ${filePath}` };
444
+ }
445
+ return { success: false, result: `文件不存在: ${filePath}` };
446
+ }
447
+ async toolExecShell(args) {
448
+ const { command, timeout = 30000 } = args;
449
+ const permission = this.permissionEngine.canPerform('exec_shell', { command });
450
+ if (!permission.allowed) {
451
+ if (permission.requiresApproval) {
452
+ const approval = await this.permissionEngine.createApproval('exec_shell', `执行命令: ${command}`, 'exec_shell', args);
453
+ return { success: false, result: `需要审批才能执行命令`, pendingApproval: approval };
454
+ }
455
+ return { success: false, result: `无权限执行命令: ${command}` };
456
+ }
457
+ try {
458
+ const { execa } = await import('execa');
459
+ const result = await execa(command, [], {
460
+ cwd: this.context.projectRoot,
461
+ timeout: timeout,
462
+ reject: false,
463
+ });
464
+ await this.logger.log('info', `执行命令: ${command}`, {
465
+ exitCode: result.exitCode,
466
+ stdout: result.stdout?.slice(0, 1000),
467
+ stderr: result.stderr?.slice(0, 1000),
468
+ });
469
+ return {
470
+ success: result.exitCode === 0,
471
+ result: {
472
+ exitCode: result.exitCode,
473
+ stdout: result.stdout,
474
+ stderr: result.stderr,
475
+ },
476
+ };
477
+ }
478
+ catch (error) {
479
+ return { success: false, result: `命令执行失败: ${String(error)}` };
480
+ }
481
+ }
482
+ async toolGetStatus(_args) {
483
+ const { config } = this.context;
484
+ const pendingApprovals = await this.permissionEngine.getPendingApprovals();
485
+ return {
486
+ success: true,
487
+ result: {
488
+ name: config.name,
489
+ version: config.version,
490
+ llm: {
491
+ provider: config.llm.provider,
492
+ model: config.llm.model,
493
+ },
494
+ permissions: {
495
+ read_repo: typeof config.permissions.read_repo === 'boolean'
496
+ ? config.permissions.read_repo
497
+ : { paths: config.permissions.read_repo?.paths },
498
+ write_repo: config.permissions.write_repo,
499
+ exec_shell: config.permissions.exec_shell,
500
+ },
501
+ pendingApprovals: pendingApprovals.length,
502
+ projectRoot: this.context.projectRoot,
503
+ },
504
+ };
505
+ }
506
+ async toolGetTasks(_args) {
507
+ const tasksDir = path.join(this.context.projectRoot, '.ship', 'tasks');
508
+ if (!fs.existsSync(tasksDir)) {
509
+ return { success: true, result: [] };
510
+ }
511
+ const files = await fs.readdir(tasksDir);
512
+ const tasks = [];
513
+ for (const file of files) {
514
+ if (file.endsWith('.md')) {
515
+ tasks.push({
516
+ name: file.replace('.md', ''),
517
+ file: path.join(tasksDir, file),
518
+ });
519
+ }
520
+ }
521
+ return { success: true, result: tasks };
522
+ }
523
+ async toolGetPendingApprovals(_args) {
524
+ const approvals = await this.permissionEngine.getPendingApprovals();
525
+ return { success: true, result: approvals };
526
+ }
527
+ async toolApprove(args) {
528
+ const { approvalId, approved } = args;
529
+ const userId = 'user'; // TODO: 从上下文获取
530
+ if (approved) {
531
+ const success = await this.permissionEngine.approve(approvalId, userId);
532
+ return { success, result: success ? '审批已通过' : '审批不存在' };
533
+ }
534
+ else {
535
+ const success = await this.permissionEngine.reject(approvalId, userId);
536
+ return { success, result: success ? '已拒绝' : '审批不存在' };
537
+ }
538
+ }
539
+ async toolCreateDiff(args) {
540
+ const { filePath, original, modified } = args;
541
+ const permission = this.permissionEngine.canPerform('write_repo', { path: filePath });
542
+ if (!permission.allowed) {
543
+ if (permission.requiresApproval) {
544
+ const approval = await this.permissionEngine.createApproval('write_repo', `修改文件: ${filePath}`, 'write_file', { path: filePath, content: modified });
545
+ return { success: false, result: `需要审批才能修改文件`, pendingApproval: approval };
546
+ }
547
+ return { success: false, result: `无权限修改文件: ${filePath}` };
548
+ }
549
+ // 直接写入
550
+ await fs.ensureDir(path.dirname(filePath));
551
+ await fs.writeFile(filePath, modified);
552
+ await this.logger.log('info', `修改文件: ${filePath}`);
553
+ return { success: true, result: `文件已修改: ${filePath}` };
554
+ }
555
+ }
556
+ // ==================== Main Agent Runtime ====================
557
+ export class AgentRuntime {
558
+ context;
559
+ tools;
560
+ permissionEngine;
561
+ initialized = false;
562
+ logger;
563
+ constructor(context) {
564
+ this.context = context;
565
+ this.tools = new AgentTools(context);
566
+ this.permissionEngine = new PermissionEngine(context);
567
+ this.logger = new AgentLogger(context.projectRoot);
568
+ }
569
+ /**
570
+ * 初始化 Agent
571
+ */
572
+ async initialize() {
573
+ try {
574
+ await this.logger.log('info', '初始化 Agent Runtime');
575
+ const { provider, apiKey, baseUrl } = this.context.config.llm;
576
+ const resolvedApiKey = apiKey || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY;
577
+ if (!resolvedApiKey) {
578
+ await this.logger.log('warn', '未配置 API Key,将使用模拟模式');
579
+ return;
580
+ }
581
+ // 验证 ai-sdk 导入
582
+ try {
583
+ if (provider === 'anthropic') {
584
+ const { createAnthropic } = await import('@ai-sdk/anthropic');
585
+ await createAnthropic({ apiKey: resolvedApiKey });
586
+ }
587
+ else {
588
+ const { createOpenAI } = await import('@ai-sdk/openai');
589
+ await createOpenAI({
590
+ apiKey: resolvedApiKey,
591
+ baseURL: baseUrl || 'https://api.openai.com/v1',
592
+ });
593
+ }
594
+ await this.logger.log('info', 'Agent Runtime 初始化完成');
595
+ this.initialized = true;
596
+ }
597
+ catch (importError) {
598
+ await this.logger.log('warn', `ai-sdk 导入失败: ${String(importError)},将使用模拟模式`);
599
+ }
600
+ }
601
+ catch (error) {
602
+ await this.logger.log('error', 'Agent Runtime 初始化失败', { error: String(error) });
603
+ // 不抛出错误,允许在模拟模式下运行
604
+ }
605
+ }
606
+ /**
607
+ * 运行 Agent
608
+ */
609
+ async run(input) {
610
+ const { instructions, context } = input;
611
+ const startTime = Date.now();
612
+ const toolCalls = [];
613
+ // 读取 Agent.md 作为系统提示
614
+ const systemPrompt = this.context.agentMd;
615
+ // 根据任务类型构建提示
616
+ let fullPrompt = instructions;
617
+ if (context?.taskDescription) {
618
+ fullPrompt = `${context.taskDescription}\n\n${instructions}`;
619
+ }
620
+ // 如果已初始化,使用真实的 AI Agent
621
+ if (this.initialized) {
622
+ return this.runWithAI(fullPrompt, systemPrompt, startTime, context);
623
+ }
624
+ // 否则使用模拟模式
625
+ return this.runSimulated(fullPrompt, startTime, toolCalls, context);
626
+ }
627
+ /**
628
+ * 使用 AI SDK 运行真实 Agent
629
+ */
630
+ async runWithAI(prompt, systemPrompt, startTime, context) {
631
+ const toolCalls = [];
632
+ try {
633
+ const { provider, model, apiKey, baseUrl } = this.context.config.llm;
634
+ const resolvedApiKey = apiKey || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY;
635
+ if (!resolvedApiKey) {
636
+ return this.runSimulated(prompt, startTime, toolCalls, context);
637
+ }
638
+ // 导入 AI SDK
639
+ const { generateText } = await import('ai');
640
+ const { tool } = await import('ai');
641
+ let providerInstance;
642
+ if (provider === 'anthropic') {
643
+ const { createAnthropic } = await import('@ai-sdk/anthropic');
644
+ providerInstance = createAnthropic({ apiKey: resolvedApiKey });
645
+ }
646
+ else {
647
+ // 支持 openai, custom 等 OpenAI 兼容的 API
648
+ const { createOpenAI } = await import('@ai-sdk/openai');
649
+ providerInstance = createOpenAI({
650
+ apiKey: resolvedApiKey,
651
+ baseURL: baseUrl || 'https://api.openai.com/v1',
652
+ });
653
+ }
654
+ // 定义工具
655
+ const tools = await this.createAITools();
656
+ // 执行 AI 调用
657
+ const result = await generateText({
658
+ model: providerInstance(model),
659
+ system: systemPrompt,
660
+ prompt,
661
+ tools,
662
+ maxSteps: 10,
663
+ });
664
+ // 记录工具调用
665
+ if (result.steps) {
666
+ for (const step of result.steps) {
667
+ if (step.toolCalls && step.toolCalls.length > 0) {
668
+ for (const toolCall of step.toolCalls) {
669
+ toolCalls.push({
670
+ tool: toolCall.toolName,
671
+ input: {},
672
+ output: '',
673
+ });
674
+ }
675
+ }
676
+ }
677
+ }
678
+ const duration = Date.now() - startTime;
679
+ await this.logger.log('info', `Agent 执行完成`, {
680
+ duration,
681
+ toolCalls: toolCalls.length,
682
+ context: context?.source,
683
+ });
684
+ return {
685
+ success: true,
686
+ output: result.text || '执行完成',
687
+ toolCalls,
688
+ };
689
+ }
690
+ catch (error) {
691
+ await this.logger.log('error', 'Agent 执行失败', { error: String(error) });
692
+ return {
693
+ success: false,
694
+ output: `Agent 执行失败: ${String(error)}`,
695
+ toolCalls,
696
+ };
697
+ }
698
+ }
699
+ /**
700
+ * 创建 AI 工具定义
701
+ */
702
+ async createAITools() {
703
+ const { tool } = await import('ai');
704
+ const tools = {
705
+ read_file: tool({
706
+ description: '读取文件内容',
707
+ parameters: z.object({
708
+ path: z.string().describe('文件路径'),
709
+ encoding: z.string().optional().default('utf-8'),
710
+ }),
711
+ execute: async (args) => this.tools.executeTool('read_file', args),
712
+ }),
713
+ list_files: tool({
714
+ description: '列出目录中的文件',
715
+ parameters: z.object({
716
+ path: z.string().describe('目录路径'),
717
+ pattern: z.string().optional().default('**/*'),
718
+ }),
719
+ execute: async (args) => this.tools.executeTool('list_files', args),
720
+ }),
721
+ search_files: tool({
722
+ description: '搜索文件内容',
723
+ parameters: z.object({
724
+ pattern: z.string().describe('搜索关键词'),
725
+ path: z.string().optional().default('.'),
726
+ glob: z.string().optional().default('**/*'),
727
+ }),
728
+ execute: async (args) => this.tools.executeTool('search_files', args),
729
+ }),
730
+ write_file: tool({
731
+ description: '创建或修改文件',
732
+ parameters: z.object({
733
+ path: z.string().describe('文件路径'),
734
+ content: z.string().describe('文件内容'),
735
+ }),
736
+ execute: async (args) => this.tools.executeTool('write_file', args),
737
+ }),
738
+ delete_file: tool({
739
+ description: '删除文件或目录',
740
+ parameters: z.object({
741
+ path: z.string().describe('文件或目录路径'),
742
+ }),
743
+ execute: async (args) => this.tools.executeTool('delete_file', args),
744
+ }),
745
+ exec_shell: tool({
746
+ description: '执行 Shell 命令',
747
+ parameters: z.object({
748
+ command: z.string().describe('要执行的命令'),
749
+ timeout: z.number().optional().default(30000),
750
+ }),
751
+ execute: async (args) => this.tools.executeTool('exec_shell', args),
752
+ }),
753
+ get_status: tool({
754
+ description: '获取 Agent 和项目状态',
755
+ parameters: z.object({}),
756
+ execute: async (args) => this.tools.executeTool('get_status', args),
757
+ }),
758
+ get_tasks: tool({
759
+ description: '获取任务列表',
760
+ parameters: z.object({}),
761
+ execute: async (args) => this.tools.executeTool('get_tasks', args),
762
+ }),
763
+ get_pending_approvals: tool({
764
+ description: '获取待审批请求',
765
+ parameters: z.object({}),
766
+ execute: async (args) => this.tools.executeTool('get_pending_approvals', args),
767
+ }),
768
+ approve: tool({
769
+ description: '审批操作请求',
770
+ parameters: z.object({
771
+ approvalId: z.string().describe('审批请求 ID'),
772
+ approved: z.boolean().describe('是否批准'),
773
+ }),
774
+ execute: async (args) => this.tools.executeTool('approve', args),
775
+ }),
776
+ create_diff: tool({
777
+ description: '创建代码 diff 并请求审批',
778
+ parameters: z.object({
779
+ filePath: z.string().describe('文件路径'),
780
+ original: z.string().describe('原始内容'),
781
+ modified: z.string().describe('修改后内容'),
782
+ }),
783
+ execute: async (args) => this.tools.executeTool('create_diff', args),
784
+ }),
785
+ };
786
+ return tools;
787
+ }
788
+ /**
789
+ * 执行已审批的操作
790
+ */
791
+ async executeApproved(approvalId) {
792
+ const approvalsDir = getApprovalsDirPath(this.context.projectRoot);
793
+ const approvalFile = path.join(approvalsDir, `${approvalId}.json`);
794
+ if (!fs.existsSync(approvalFile)) {
795
+ return { success: false, result: '审批不存在' };
796
+ }
797
+ const approval = await fs.readJson(approvalFile);
798
+ if (approval.status !== 'approved') {
799
+ return { success: false, result: '审批未通过' };
800
+ }
801
+ // 执行已审批的操作
802
+ const result = await this.tools.executeTool(approval.tool, approval.input);
803
+ return result;
804
+ }
805
+ /**
806
+ * 模拟模式(当 AI 不可用时)
807
+ */
808
+ runSimulated(prompt, startTime, toolCalls, context) {
809
+ const promptLower = prompt.toLowerCase();
810
+ let output = '';
811
+ // 根据不同的指令类型生成响应
812
+ if (promptLower.includes('status') || promptLower.includes('状态')) {
813
+ output = this.generateStatusResponse();
814
+ }
815
+ else if (promptLower.includes('task') || promptLower.includes('任务')) {
816
+ output = this.generateTasksResponse();
817
+ }
818
+ else if (promptLower.includes('scan') || promptLower.includes('扫描')) {
819
+ output = this.generateScanResponse();
820
+ }
821
+ else if (promptLower.includes('approve') || promptLower.includes('审批')) {
822
+ output = this.generateApprovalsResponse();
823
+ }
824
+ else {
825
+ output = `收到指令: "${prompt}"\n\n[模拟模式] AI 服务未配置,请在 ship.json 中配置 API Key 后重启。`;
826
+ }
827
+ const duration = Date.now() - startTime;
828
+ this.logger.log('info', `模拟 Agent 执行完成`, { duration, context: context?.source });
829
+ return {
830
+ success: true,
831
+ output,
832
+ toolCalls,
833
+ };
834
+ }
835
+ generateStatusResponse() {
836
+ const { config } = this.context;
837
+ return `📊 **Agent 状态报告**
838
+
839
+ **项目**: ${config.name}
840
+ **版本**: ${config.version}
841
+ **模型**: ${config.llm.provider} / ${config.llm.model}
842
+
843
+ **权限状态**:
844
+ - 读取代码仓库: ✅ ${typeof config.permissions.read_repo === 'boolean' ? (config.permissions.read_repo ? '已启用' : '已禁用') : '已启用(带路径限制)'}
845
+ - 写入代码: ${config.permissions.write_repo ? (config.permissions.write_repo.requiresApproval ? '⚠️ 需要审批' : '✅ 已启用') : '❌ 已禁用'}
846
+ - 执行 Shell: ${config.permissions.exec_shell ? (config.permissions.exec_shell.requiresApproval ? '⚠️ 需要审批' : '✅ 已启用') : '❌ 已禁用'}
847
+
848
+ **运行时**: 正常运行`;
849
+ }
850
+ generateTasksResponse() {
851
+ const tasksDir = path.join(this.context.projectRoot, '.ship', 'tasks');
852
+ if (!fs.existsSync(tasksDir)) {
853
+ return `📋 **任务列表**
854
+
855
+ 当前没有配置定时任务。
856
+
857
+ 在 .ship/tasks/ 目录下添加 .md 文件来定义任务。`;
858
+ }
859
+ const files = fs.readdirSync(tasksDir).filter(f => f.endsWith('.md'));
860
+ if (files.length === 0) {
861
+ return `📋 **任务列表**
862
+
863
+ 当前没有配置定时任务。`;
864
+ }
865
+ return `📋 **任务列表**
866
+
867
+ 已配置 ${files.length} 个任务:
868
+ ${files.map(f => `- ${f.replace('.md', '')}`).join('\n')}
869
+
870
+ 任务定义位置: .ship/tasks/`;
871
+ }
872
+ generateScanResponse() {
873
+ return `🔍 **代码扫描结果**
874
+
875
+ 扫描目录: ${this.context.projectRoot}
876
+
877
+ **发现**:
878
+ - 代码结构正常
879
+ - 建议定期运行测试
880
+
881
+ **TODO 注释**: 未检测到`;
882
+ }
883
+ generateApprovalsResponse() {
884
+ return `📋 **审批列表**
885
+
886
+ 当前没有待审批的请求。`;
887
+ }
888
+ /**
889
+ * 获取工具实例
890
+ */
891
+ getTools() {
892
+ return this.tools;
893
+ }
894
+ /**
895
+ * 获取权限引擎实例
896
+ */
897
+ getPermissionEngine() {
898
+ return this.permissionEngine;
899
+ }
900
+ /**
901
+ * 获取配置
902
+ */
903
+ getConfig() {
904
+ return this.context.config;
905
+ }
906
+ /**
907
+ * 检查是否已初始化
908
+ */
909
+ isInitialized() {
910
+ return this.initialized;
911
+ }
912
+ }
913
+ // ==================== Factory Functions ====================
914
+ export function createAgentRuntime(context) {
915
+ return new AgentRuntime(context);
916
+ }
917
+ export function createAgentRuntimeFromPath(projectRoot) {
918
+ // 读取配置文件
919
+ const agentMdPath = getAgentMdPath(projectRoot);
920
+ const shipJsonPath = getShipJsonPath(projectRoot);
921
+ let agentMd = `# Agent Role
922
+
923
+ You are the maintainer agent of this repository.
924
+
925
+ ## Goals
926
+ - Improve code quality
927
+ - Reduce bugs
928
+ - Assist humans, never override them
929
+
930
+ ## Constraints
931
+ - Never modify files without approval
932
+ - Never run shell commands unless explicitly allowed
933
+ - Always explain your intent before acting
934
+
935
+ ## Communication Style
936
+ - Concise
937
+ - Technical
938
+ - No speculation without evidence`;
939
+ let config = {
940
+ name: 'shipmyagent',
941
+ version: '1.0.0',
942
+ llm: {
943
+ provider: 'anthropic',
944
+ model: 'claude-sonnet-4-20250514',
945
+ baseUrl: 'https://api.anthropic.com/v1',
946
+ temperature: 0.7,
947
+ maxTokens: 4096,
948
+ },
949
+ permissions: {
950
+ read_repo: true,
951
+ write_repo: { requiresApproval: true },
952
+ exec_shell: { requiresApproval: true },
953
+ },
954
+ integrations: {
955
+ telegram: { enabled: false },
956
+ },
957
+ };
958
+ // 确保 .ship 目录存在
959
+ const shipDir = getShipDirPath(projectRoot);
960
+ fs.ensureDirSync(shipDir);
961
+ fs.ensureDirSync(path.join(shipDir, 'tasks'));
962
+ fs.ensureDirSync(path.join(shipDir, 'routes'));
963
+ fs.ensureDirSync(path.join(shipDir, 'approvals'));
964
+ fs.ensureDirSync(path.join(shipDir, 'logs'));
965
+ fs.ensureDirSync(path.join(shipDir, '.cache'));
966
+ // 读取 Agent.md
967
+ try {
968
+ if (fs.existsSync(agentMdPath)) {
969
+ agentMd = fs.readFileSync(agentMdPath, 'utf-8');
970
+ }
971
+ }
972
+ catch {
973
+ // 使用默认配置
974
+ }
975
+ // 读取 ship.json
976
+ try {
977
+ if (fs.existsSync(shipJsonPath)) {
978
+ config = fs.readJsonSync(shipJsonPath);
979
+ }
980
+ }
981
+ catch {
982
+ // 使用默认配置
983
+ }
984
+ return new AgentRuntime({
985
+ projectRoot,
986
+ config,
987
+ agentMd,
988
+ });
989
+ }
990
+ //# sourceMappingURL=agent.js.map