imtoagent 0.3.17 → 0.3.19

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/index.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  // ============================================================
4
4
 
5
5
  import * as fs from 'fs';
6
+ import * as os from 'os';
6
7
  import * as path from 'path';
7
8
 
8
9
  // ===== 重启信号文件路径(统一固定,不依赖 getDataDir) =====
@@ -306,6 +307,8 @@ class Bot {
306
307
  sessions: Map<string, ChatSession> = new Map();
307
308
  commands: Map<string, CommandHandler> = new Map();
308
309
  adapter: AgentAdapter;
310
+ /** 正在执行的任务的取消信号(chatId → AbortController) */
311
+ activeControllers: Map<string, AbortController> = new Map();
309
312
 
310
313
  constructor(cfg: BotConfig, globalConfig: any) {
311
314
  this.id = cfg.id || cfg.name; // 后向兼容:无 id 时用 name
@@ -313,7 +316,7 @@ class Bot {
313
316
  this.backend = cfg.backend;
314
317
  this.appId = cfg.appId;
315
318
  this.appSecret = cfg.appSecret;
316
- this.defaultCwd = cfg.cwd || globalConfig.system?.defaultProjectDir || '/Users/keyi/Projects';
319
+ this.defaultCwd = cfg.cwd || globalConfig.system?.defaultProjectDir || path.join(os.homedir(), 'Projects');
317
320
  this.config = globalConfig;
318
321
 
319
322
  // Bot 级模型配置
@@ -466,7 +469,7 @@ class Bot {
466
469
  let out = '📋 **CC Quick Commands**\n\n';
467
470
  out += '/status — Status\n/info — Config\n/stats — Stats\n';
468
471
  out += '/model — Model Switch\n/providers — Providers\n';
469
- out += '/dir — Directory\n/clear — Clear\n';
472
+ out += '/dir — Directory\n/clear — Clear\n/stop — Stop current task\n';
470
473
  if (this.backend === 'claude') out += '/mode — Permission\n';
471
474
  else if (this.backend === 'codex') out += '/mode — Mode(auto/plan)\n';
472
475
  out += '/memory — Overview\n/soul — Soul\n/reload — Reload';
@@ -495,6 +498,13 @@ class Bot {
495
498
  return '✅ No active conversation';
496
499
  });
497
500
 
501
+ cmd('/stop', ({ chatId }) => {
502
+ const ctrl = this.activeControllers.get(chatId);
503
+ if (!ctrl) return '✅ 没有正在执行的任务';
504
+ ctrl.abort();
505
+ return '⏹️ 任务已取消';
506
+ });
507
+
498
508
  cmd('/model', ({ args }) => {
499
509
  const raw = args.trim();
500
510
  if (!raw) {
@@ -678,6 +688,8 @@ class Bot {
678
688
  // ===== 消息处理 — SDK 完整接入 =====
679
689
  async handleMessage(chatId: string, text: string, userId: string, attachments?: MessageAttachment[]) {
680
690
  activeRequests++;
691
+ const controller = new AbortController();
692
+ this.activeControllers.set(chatId, controller);
681
693
  try {
682
694
  // 限流
683
695
  const rlResult = checkRateLimit(chatId);
@@ -715,6 +727,7 @@ class Bot {
715
727
  sendProgress: async (t: string) => this.sendProgress(chatId, t),
716
728
  sendBlocks: async (blocks) => this.sendFormattedReplyDirect(chatId, blocks),
717
729
  imCaps: this.im.getCapabilities(),
730
+ cancelSignal: controller.signal,
718
731
  }, this.adapter, this.id);
719
732
 
720
733
  // Agent 自主重启信号检测
@@ -728,6 +741,7 @@ class Bot {
728
741
  console.error(`[${this.name}] handleMessage error: ${e.message}`);
729
742
  await this.reply(chatId, `❌ ${e.message}`);
730
743
  } finally {
744
+ this.activeControllers.delete(chatId);
731
745
  activeRequests--;
732
746
  }
733
747
  }
@@ -861,7 +875,7 @@ async function main() {
861
875
  process.exit(0);
862
876
  }
863
877
 
864
- const DEFAULT_PROJECT_DIR = config.system?.defaultProjectDir || '/Users/keyi/Projects';
878
+ const DEFAULT_PROJECT_DIR = config.system?.defaultProjectDir || path.join(os.homedir(), 'Projects');
865
879
 
866
880
  if (config.modelAliases) sharedState.modelAliases = config.modelAliases;
867
881
  const { providers: _providers, defaultModel: DEFAULT_MODEL_SPEC } = loadProviders();
@@ -64,7 +64,8 @@ async function ocSendPrompt(
64
64
  initialText: string,
65
65
  system: string,
66
66
  defaultModel: { providerID: string; modelID: string },
67
- onTool?: (name: string, args: Record<string, any>) => void
67
+ onTool?: (name: string, args: Record<string, any>) => void,
68
+ cancelSignal?: AbortSignal
68
69
  ): Promise<{ response: string; toolCalls: Array<{ name: string; summary: string }> }> {
69
70
  const MAX_TURNS = 50;
70
71
  const TURN_TIMEOUT = 300_000; // 5 min per turn
@@ -92,15 +93,29 @@ async function ocSendPrompt(
92
93
 
93
94
  const ac = new AbortController();
94
95
  const timer = setTimeout(() => ac.abort(), TURN_TIMEOUT);
95
- const res = await fetch(`${serverUrl}/session/${sessionId}/message`, {
96
- method: 'POST',
97
- headers: { 'Content-Type': 'application/json' },
98
- body: JSON.stringify(body),
99
- signal: ac.signal,
100
- }).finally(() => clearTimeout(timer));
101
- if (!res.ok) throw new Error(`oc send prompt (turn ${turn}): ${res.status} ${await res.text()}`);
102
-
103
- const data: OcMessage = await res.json();
96
+ // 组合外部取消信号(/stop)+ 本层超时
97
+ const signal = cancelSignal ? AbortSignal.any([ac.signal, cancelSignal]) : ac.signal;
98
+ let data: OcMessage;
99
+ try {
100
+ const res = await fetch(`${serverUrl}/session/${sessionId}/message`, {
101
+ method: 'POST',
102
+ headers: { 'Content-Type': 'application/json' },
103
+ body: JSON.stringify(body),
104
+ signal,
105
+ }).finally(() => clearTimeout(timer));
106
+ if (!res.ok) throw new Error(`oc send prompt (turn ${turn}): ${res.status} ${await res.text()}`);
107
+ data = await res.json();
108
+ } catch (err: any) {
109
+ if (err?.name === 'AbortError') {
110
+ // 外部取消(/stop)优先于超时
111
+ if (cancelSignal?.aborted) {
112
+ console.log('[OpenCodeAdapter] ⏹️ Task cancelled by user (/stop)');
113
+ return { response: '⏹️ 任务已取消', toolCalls: allToolCalls };
114
+ }
115
+ throw err; // 超时等其它 AbortError 继续抛出
116
+ }
117
+ throw err;
118
+ }
104
119
  let hasToolCall = false;
105
120
  let hasText = false;
106
121
 
@@ -209,7 +224,8 @@ export class OpenCodeAdapter implements AgentAdapter {
209
224
  // onTool 回调:适配器层不依赖外部 ctx,直接记日志
210
225
  (name, args) => {
211
226
  // 工具调用日志由 Runtime 层统一格式化
212
- }
227
+ },
228
+ input.cancelSignal
213
229
  );
214
230
 
215
231
  return {
@@ -265,19 +281,34 @@ export async function startOpenCodeServer(): Promise<void> {
265
281
 
266
282
  console.log(`[OpenCodeAdapter] Using opencode binary: ${ocBinPath}`);
267
283
 
268
- const child = Bun.spawn(
269
- [ocBinPath, 'serve', '--port', String(OC_PORT), '--hostname', '127.0.0.1'],
270
- {
271
- cwd: getDataDir(),
272
- env: {
273
- ...process.env,
274
- // 环形通信无需真实 key,但 OpenCode 的 Anthropic provider 要求此变量存在
275
- ANTHROPIC_API_KEY: 'imtoagent-local',
284
+ const dataDir = getDataDir();
285
+ if (!fs.existsSync(dataDir)) {
286
+ console.log(`[OpenCodeAdapter] Creating data directory: ${dataDir}`);
287
+ fs.mkdirSync(dataDir, { recursive: true });
288
+ }
289
+ console.log(`[OpenCodeAdapter] Working directory: ${dataDir}`);
290
+
291
+ let child;
292
+ try {
293
+ child = Bun.spawn(
294
+ [ocBinPath, 'serve', '--port', String(OC_PORT), '--hostname', '127.0.0.1'],
295
+ {
296
+ cwd: dataDir,
297
+ env: {
298
+ ...process.env,
299
+ // 环形通信无需真实 key,但 OpenCode 的 Anthropic provider 要求此变量存在
300
+ ANTHROPIC_API_KEY: 'imtoagent-local',
301
+ },
302
+ stdout: 'pipe',
303
+ stderr: 'pipe',
276
304
  },
277
- stdout: 'pipe',
278
- stderr: 'pipe',
279
- },
280
- );
305
+ );
306
+ } catch (err: any) {
307
+ console.error(`[OpenCodeAdapter] Failed to start opencode serve: ${err.message}`);
308
+ console.error(` Binary: ${ocBinPath}`);
309
+ console.error(` Work dir: ${dataDir} (exists: ${fs.existsSync(dataDir)})`);
310
+ throw new Error(`opencode serve failed: ${err.message}`);
311
+ }
281
312
 
282
313
  // 后台收集日志
283
314
  (async () => {
@@ -156,6 +156,7 @@ export class AgentRuntime {
156
156
  workingDir: ctx.workingDir,
157
157
  systemPrompt: ctx.systemPrompt,
158
158
  model: ctx.model,
159
+ cancelSignal: ctx.cancelSignal,
159
160
  };
160
161
 
161
162
  const output = await adapter.handleMessage(input);
@@ -113,6 +113,8 @@ export interface AgentInput {
113
113
  workingDir: string;
114
114
  systemPrompt?: string;
115
115
  model: string;
116
+ /** 外部取消信号(用于 /stop 等中断命令) */
117
+ cancelSignal?: AbortSignal;
116
118
  }
117
119
 
118
120
  /** Agent 输出 */
@@ -165,6 +167,8 @@ export interface MessageContext {
165
167
  sendBlocks?: (blocks: UnifiedBlock[]) => Promise<void>;
166
168
  /** IM 能力声明,用于 parseToBlocks 正确解析 */
167
169
  imCaps?: IMCapabilities;
170
+ /** 外部取消信号(用于 /stop 等中断命令) */
171
+ cancelSignal?: AbortSignal;
168
172
  }
169
173
 
170
174
  /** Session 管理器 */
@@ -39,8 +39,8 @@ export function getDataDir(): string {
39
39
  // ====== 第 1 步:查找已有的配置文件 ======
40
40
  const candidates: { dir: string; label: string }[] = [];
41
41
 
42
- // IMTOAGENT_HOME(优先检查,但不强制)
43
- if (envHome && fs.existsSync(path.join(envHome, 'config.json'))) {
42
+ // IMTOAGENT_HOME(优先检查,但不强制;目录不存在则忽略)
43
+ if (envHome && fs.existsSync(envHome) && fs.existsSync(path.join(envHome, 'config.json'))) {
44
44
  candidates.push({ dir: envHome, label: 'IMTOAGENT_HOME' });
45
45
  }
46
46
 
@@ -83,7 +83,7 @@ function initDataDir(dotDir: string, envHome: string): string {
83
83
  let sourceDir: string | null = null;
84
84
  let sourceLabel = '';
85
85
 
86
- if (envHome && fs.existsSync(path.join(envHome, 'config.json'))) {
86
+ if (envHome && fs.existsSync(envHome) && fs.existsSync(path.join(envHome, 'config.json'))) {
87
87
  sourceDir = envHome;
88
88
  sourceLabel = 'IMTOAGENT_HOME';
89
89
  } else if (fs.existsSync(path.join(process.cwd(), 'config.json'))) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imtoagent",
3
- "version": "0.3.17",
3
+ "version": "0.3.19",
4
4
  "description": "IM ↔ Agent 统一网关 — 飞书/Telegram/微信/企业微信对接 Claude Code/Codex/OpenCode",
5
5
  "type": "module",
6
6
  "bin": {