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 +17 -3
- package/modules/agent/opencode-adapter.ts +54 -23
- package/modules/core/runtime.ts +1 -0
- package/modules/core/types.ts +4 -0
- package/modules/utils/paths.ts +3 -3
- package/package.json +1 -1
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 || '
|
|
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 || '
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
269
|
-
|
|
270
|
-
{
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
278
|
-
|
|
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 () => {
|
package/modules/core/runtime.ts
CHANGED
package/modules/core/types.ts
CHANGED
|
@@ -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/modules/utils/paths.ts
CHANGED
|
@@ -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'))) {
|