imtoagent 0.3.17 → 0.3.18

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
@@ -306,6 +306,8 @@ class Bot {
306
306
  sessions: Map<string, ChatSession> = new Map();
307
307
  commands: Map<string, CommandHandler> = new Map();
308
308
  adapter: AgentAdapter;
309
+ /** 正在执行的任务的取消信号(chatId → AbortController) */
310
+ activeControllers: Map<string, AbortController> = new Map();
309
311
 
310
312
  constructor(cfg: BotConfig, globalConfig: any) {
311
313
  this.id = cfg.id || cfg.name; // 后向兼容:无 id 时用 name
@@ -466,7 +468,7 @@ class Bot {
466
468
  let out = '📋 **CC Quick Commands**\n\n';
467
469
  out += '/status — Status\n/info — Config\n/stats — Stats\n';
468
470
  out += '/model — Model Switch\n/providers — Providers\n';
469
- out += '/dir — Directory\n/clear — Clear\n';
471
+ out += '/dir — Directory\n/clear — Clear\n/stop — Stop current task\n';
470
472
  if (this.backend === 'claude') out += '/mode — Permission\n';
471
473
  else if (this.backend === 'codex') out += '/mode — Mode(auto/plan)\n';
472
474
  out += '/memory — Overview\n/soul — Soul\n/reload — Reload';
@@ -495,6 +497,13 @@ class Bot {
495
497
  return '✅ No active conversation';
496
498
  });
497
499
 
500
+ cmd('/stop', ({ chatId }) => {
501
+ const ctrl = this.activeControllers.get(chatId);
502
+ if (!ctrl) return '✅ 没有正在执行的任务';
503
+ ctrl.abort();
504
+ return '⏹️ 任务已取消';
505
+ });
506
+
498
507
  cmd('/model', ({ args }) => {
499
508
  const raw = args.trim();
500
509
  if (!raw) {
@@ -678,6 +687,8 @@ class Bot {
678
687
  // ===== 消息处理 — SDK 完整接入 =====
679
688
  async handleMessage(chatId: string, text: string, userId: string, attachments?: MessageAttachment[]) {
680
689
  activeRequests++;
690
+ const controller = new AbortController();
691
+ this.activeControllers.set(chatId, controller);
681
692
  try {
682
693
  // 限流
683
694
  const rlResult = checkRateLimit(chatId);
@@ -715,6 +726,7 @@ class Bot {
715
726
  sendProgress: async (t: string) => this.sendProgress(chatId, t),
716
727
  sendBlocks: async (blocks) => this.sendFormattedReplyDirect(chatId, blocks),
717
728
  imCaps: this.im.getCapabilities(),
729
+ cancelSignal: controller.signal,
718
730
  }, this.adapter, this.id);
719
731
 
720
732
  // Agent 自主重启信号检测
@@ -728,6 +740,7 @@ class Bot {
728
740
  console.error(`[${this.name}] handleMessage error: ${e.message}`);
729
741
  await this.reply(chatId, `❌ ${e.message}`);
730
742
  } finally {
743
+ this.activeControllers.delete(chatId);
731
744
  activeRequests--;
732
745
  }
733
746
  }
@@ -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 {
@@ -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 管理器 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imtoagent",
3
- "version": "0.3.17",
3
+ "version": "0.3.18",
4
4
  "description": "IM ↔ Agent 统一网关 — 飞书/Telegram/微信/企业微信对接 Claude Code/Codex/OpenCode",
5
5
  "type": "module",
6
6
  "bin": {