@wu529778790/open-im 1.10.2-beta.5 → 1.10.2-beta.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/adapters/claude-sdk-adapter.d.ts +4 -0
- package/dist/adapters/claude-sdk-adapter.js +18 -2
- package/dist/adapters/registry.js +2 -1
- package/dist/commands/handler.d.ts +5 -1
- package/dist/commands/handler.js +80 -41
- package/dist/commands/normalize-command.test.d.ts +1 -0
- package/dist/commands/normalize-command.test.js +17 -0
- package/dist/config/types.d.ts +6 -0
- package/dist/config-web.js +1 -0
- package/dist/config.js +14 -0
- package/dist/index.js +2 -5
- package/dist/platform/handle-text-flow.d.ts +2 -1
- package/dist/platform/handle-text-flow.js +8 -2
- package/dist/platform/handle-text-flow.test.js +3 -1
- package/dist/qq/client.js +11 -2
- package/dist/telegram/message-sender.js +1 -0
- package/package.json +1 -1
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
* 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN
|
|
10
10
|
*/
|
|
11
11
|
import type { ToolAdapter, RunCallbacks, RunOptions, RunHandle } from './tool-adapter.interface.js';
|
|
12
|
+
/**
|
|
13
|
+
* 由 initAdapters 根据配置调用。ttlMinutes≤0 时关闭空闲回收(仍受 MAX_ACTIVE_SESSIONS 限制)。
|
|
14
|
+
*/
|
|
15
|
+
export declare function configureClaudeSdkSessionIdle(ttlMinutes: number): void;
|
|
12
16
|
export declare class ClaudeSDKAdapter implements ToolAdapter {
|
|
13
17
|
readonly toolId = "claude-sdk";
|
|
14
18
|
/**
|
|
@@ -53,16 +53,32 @@ const activeStreams = new Set();
|
|
|
53
53
|
const sessionLastUsed = new Map();
|
|
54
54
|
// 跟踪正在执行任务的 session ID,防止空闲清理误杀运行中的长任务
|
|
55
55
|
const runningSessions = new Set();
|
|
56
|
-
|
|
56
|
+
let sessionIdleTtlMs = 30 * 60 * 1000; // 默认 30 分钟未使用则清理
|
|
57
|
+
let sessionIdleCleanupDisabled = false;
|
|
57
58
|
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 每 5 分钟检查一次
|
|
58
59
|
const MAX_ACTIVE_SESSIONS = 100;
|
|
59
60
|
let sessionSeq = 0;
|
|
61
|
+
/**
|
|
62
|
+
* 由 initAdapters 根据配置调用。ttlMinutes≤0 时关闭空闲回收(仍受 MAX_ACTIVE_SESSIONS 限制)。
|
|
63
|
+
*/
|
|
64
|
+
export function configureClaudeSdkSessionIdle(ttlMinutes) {
|
|
65
|
+
if (ttlMinutes <= 0) {
|
|
66
|
+
sessionIdleCleanupDisabled = true;
|
|
67
|
+
log.info('Claude SDK: idle session cleanup disabled (sessionIdleTtlMinutes=0)');
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
sessionIdleCleanupDisabled = false;
|
|
71
|
+
sessionIdleTtlMs = ttlMinutes * 60 * 1000;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
60
74
|
const cleanupInterval = setInterval(() => {
|
|
75
|
+
if (sessionIdleCleanupDisabled)
|
|
76
|
+
return;
|
|
61
77
|
const now = Date.now();
|
|
62
78
|
for (const [id, lastUsed] of sessionLastUsed) {
|
|
63
79
|
if (runningSessions.has(id))
|
|
64
80
|
continue; // 跳过正在运行任务的 session
|
|
65
|
-
if (now - lastUsed >
|
|
81
|
+
if (now - lastUsed > sessionIdleTtlMs) {
|
|
66
82
|
const session = activeSessions.get(id);
|
|
67
83
|
if (session) {
|
|
68
84
|
try {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getConfiguredAiCommands } from '../config.js';
|
|
2
|
-
import { ClaudeSDKAdapter } from './claude-sdk-adapter.js';
|
|
2
|
+
import { ClaudeSDKAdapter, configureClaudeSdkSessionIdle } from './claude-sdk-adapter.js';
|
|
3
3
|
import { CodexAdapter } from './codex-adapter.js';
|
|
4
4
|
import { CodeBuddyAdapter } from './codebuddy-adapter.js';
|
|
5
5
|
import { createLogger } from '../logger.js';
|
|
@@ -10,6 +10,7 @@ export function initAdapters(config) {
|
|
|
10
10
|
for (const aiCommand of getConfiguredAiCommands(config)) {
|
|
11
11
|
if (aiCommand === 'claude') {
|
|
12
12
|
log.info('Claude Agent SDK adapter enabled');
|
|
13
|
+
configureClaudeSdkSessionIdle(config.claudeSessionIdleTtlMinutes);
|
|
13
14
|
adapters.set('claude', new ClaudeSDKAdapter());
|
|
14
15
|
continue;
|
|
15
16
|
}
|
|
@@ -15,10 +15,14 @@ export interface CommandHandlerDeps {
|
|
|
15
15
|
getRunningTasksSize: () => number;
|
|
16
16
|
}
|
|
17
17
|
export type ClaudeRequestHandler = (userId: string, chatId: string, prompt: string, workDir: string, convId?: string, threadCtx?: ThreadContext, replyToMessageId?: string) => Promise<void>;
|
|
18
|
+
export declare function normalizeSlashCommandForDispatch(text: string): string;
|
|
18
19
|
export declare class CommandHandler {
|
|
19
20
|
private deps;
|
|
20
21
|
constructor(deps: CommandHandlerDeps);
|
|
21
|
-
|
|
22
|
+
private replySender;
|
|
23
|
+
dispatch(text: string, chatId: string, userId: string, platform: Platform, _handleClaudeRequest: ClaudeRequestHandler,
|
|
24
|
+
/** 若提供,本条消息的斜杠命令回复走此 sender(须与 handleTextFlow 的 sendTextReply 一致,如带 msgId)。 */
|
|
25
|
+
senderOverride?: MessageSender): Promise<boolean>;
|
|
22
26
|
private handleHelp;
|
|
23
27
|
private handleSessions;
|
|
24
28
|
private handleResume;
|
package/dist/commands/handler.js
CHANGED
|
@@ -3,41 +3,79 @@ import { escapePathForMarkdown } from '../shared/utils.js';
|
|
|
3
3
|
import { TERMINAL_ONLY_COMMANDS } from '../constants.js';
|
|
4
4
|
import { createLogger } from '../logger.js';
|
|
5
5
|
const log = createLogger('Commands');
|
|
6
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
6
7
|
import { execFile } from 'node:child_process';
|
|
7
8
|
import { readdirSync } from 'node:fs';
|
|
8
9
|
import { dirname, join } from 'node:path';
|
|
10
|
+
/**
|
|
11
|
+
* Telegram 群聊等场景下命令常为 `/new@BotName`,需与 `/new` 等价。
|
|
12
|
+
* 仅去掉「第一个」命令词上的 `@suffix`,保留 `/resume 1` 等参数。
|
|
13
|
+
*/
|
|
14
|
+
/** 并发 dispatch 时,用 AsyncLocalStorage 绑定「本条消息」的 sender(如 WorkBuddy 需 msgId)。 */
|
|
15
|
+
const commandReplySender = new AsyncLocalStorage();
|
|
16
|
+
function mergeMessageSender(override, base) {
|
|
17
|
+
return {
|
|
18
|
+
sendTextReply: (chatId, text, threadCtx) => override.sendTextReply(chatId, text, threadCtx),
|
|
19
|
+
sendDirectorySelection: override.sendDirectorySelection ?? base.sendDirectorySelection,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function normalizeSlashCommandForDispatch(text) {
|
|
23
|
+
const trimmed = text.trim();
|
|
24
|
+
if (!trimmed.startsWith("/") || !trimmed.includes("@"))
|
|
25
|
+
return trimmed;
|
|
26
|
+
const firstSpace = trimmed.indexOf(" ");
|
|
27
|
+
const firstSegment = firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace);
|
|
28
|
+
if (!firstSegment.includes("@"))
|
|
29
|
+
return trimmed;
|
|
30
|
+
const at = firstSegment.indexOf("@");
|
|
31
|
+
const baseCmd = firstSegment.slice(0, at);
|
|
32
|
+
if (firstSpace === -1)
|
|
33
|
+
return baseCmd;
|
|
34
|
+
return `${baseCmd}${trimmed.slice(firstSpace)}`;
|
|
35
|
+
}
|
|
9
36
|
export class CommandHandler {
|
|
10
37
|
deps;
|
|
11
38
|
constructor(deps) {
|
|
12
39
|
this.deps = deps;
|
|
13
40
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
replySender() {
|
|
42
|
+
return commandReplySender.getStore() ?? this.deps.sender;
|
|
43
|
+
}
|
|
44
|
+
async dispatch(text, chatId, userId, platform, _handleClaudeRequest,
|
|
45
|
+
/** 若提供,本条消息的斜杠命令回复走此 sender(须与 handleTextFlow 的 sendTextReply 一致,如带 msgId)。 */
|
|
46
|
+
senderOverride) {
|
|
47
|
+
const runBody = async () => {
|
|
48
|
+
const t = normalizeSlashCommandForDispatch(text);
|
|
49
|
+
if (platform === 'telegram' && t === '/start') {
|
|
50
|
+
await this.replySender().sendTextReply(chatId, '欢迎使用 open-im AI CLI 桥接!\n\n发送消息与 AI 交互,输入 /help 查看帮助。');
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
if (t === '/help')
|
|
54
|
+
return this.handleHelp(chatId);
|
|
55
|
+
if (t === '/new')
|
|
56
|
+
return this.handleNew(chatId, userId);
|
|
57
|
+
if (t === '/sessions' || t === '/resume')
|
|
58
|
+
return this.handleSessions(chatId, userId, platform);
|
|
59
|
+
if (t.startsWith('/resume '))
|
|
60
|
+
return this.handleResume(chatId, userId, t.slice(8).trim(), platform);
|
|
61
|
+
if (t === '/pwd')
|
|
62
|
+
return this.handlePwd(chatId, userId);
|
|
63
|
+
if (t === '/status')
|
|
64
|
+
return this.handleStatus(chatId, userId, platform);
|
|
65
|
+
if (t === '/cd' || t.startsWith('/cd ')) {
|
|
66
|
+
return this.handleCd(chatId, userId, t.slice(3).trim(), platform);
|
|
67
|
+
}
|
|
68
|
+
const cmd = t.split(/\s+/)[0];
|
|
69
|
+
if (TERMINAL_ONLY_COMMANDS.has(cmd)) {
|
|
70
|
+
await this.replySender().sendTextReply(chatId, `${cmd} 命令仅在终端可用。`);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
};
|
|
75
|
+
if (senderOverride) {
|
|
76
|
+
return commandReplySender.run(mergeMessageSender(senderOverride, this.deps.sender), runBody);
|
|
39
77
|
}
|
|
40
|
-
return
|
|
78
|
+
return runBody();
|
|
41
79
|
}
|
|
42
80
|
async handleHelp(chatId) {
|
|
43
81
|
const help = [
|
|
@@ -51,14 +89,14 @@ export class CommandHandler {
|
|
|
51
89
|
'/cd <路径> - 切换工作目录',
|
|
52
90
|
'/pwd - 当前工作目录',
|
|
53
91
|
].join('\n');
|
|
54
|
-
await this.
|
|
92
|
+
await this.replySender().sendTextReply(chatId, help);
|
|
55
93
|
return true;
|
|
56
94
|
}
|
|
57
95
|
async handleSessions(chatId, userId, _platform) {
|
|
58
96
|
const history = this.deps.sessionManager.listConvHistory(userId);
|
|
59
97
|
const active = this.deps.sessionManager.getActiveConvInfo(userId);
|
|
60
98
|
if (history.length === 0 && !active) {
|
|
61
|
-
await this.
|
|
99
|
+
await this.replySender().sendTextReply(chatId, '📋 暂无会话记录。');
|
|
62
100
|
return true;
|
|
63
101
|
}
|
|
64
102
|
const lines = ['📋 会话列表:', ''];
|
|
@@ -70,40 +108,40 @@ export class CommandHandler {
|
|
|
70
108
|
lines.push(`▸ ${num}. ${active.convId} · ${active.totalTurns}轮(当前)`);
|
|
71
109
|
}
|
|
72
110
|
lines.push('', '使用 /resume <序号> 恢复历史会话');
|
|
73
|
-
await this.
|
|
111
|
+
await this.replySender().sendTextReply(chatId, lines.join('\n'));
|
|
74
112
|
return true;
|
|
75
113
|
}
|
|
76
114
|
async handleResume(chatId, userId, arg, _platform) {
|
|
77
115
|
const index = parseInt(arg, 10);
|
|
78
116
|
if (isNaN(index) || index < 1) {
|
|
79
|
-
await this.
|
|
117
|
+
await this.replySender().sendTextReply(chatId, '用法: /resume <序号>\n\n使用 /sessions 查看会话列表。');
|
|
80
118
|
return true;
|
|
81
119
|
}
|
|
82
120
|
const history = this.deps.sessionManager.listConvHistory(userId);
|
|
83
121
|
if (index > history.length) {
|
|
84
|
-
await this.
|
|
122
|
+
await this.replySender().sendTextReply(chatId, `序号 ${index} 无效,共 ${history.length} 个历史会话。`);
|
|
85
123
|
return true;
|
|
86
124
|
}
|
|
87
125
|
const entry = history[index - 1];
|
|
88
126
|
const ok = this.deps.sessionManager.resumeConv(userId, entry.convId);
|
|
89
127
|
if (ok) {
|
|
90
|
-
await this.
|
|
128
|
+
await this.replySender().sendTextReply(chatId, `✅ 已恢复会话 ${index} (${entry.convId}),共 ${entry.totalTurns}轮对话。\n继续发消息即可。`);
|
|
91
129
|
}
|
|
92
130
|
else {
|
|
93
|
-
await this.
|
|
131
|
+
await this.replySender().sendTextReply(chatId, '❌ 恢复会话失败,请重试。');
|
|
94
132
|
}
|
|
95
133
|
return true;
|
|
96
134
|
}
|
|
97
135
|
async handleNew(chatId, userId) {
|
|
98
136
|
const ok = this.deps.sessionManager.newSession(userId);
|
|
99
|
-
await this.
|
|
137
|
+
await this.replySender().sendTextReply(chatId, ok
|
|
100
138
|
? '✅ AI 会话已重置,下一条消息将使用全新上下文。'
|
|
101
139
|
: '当前没有活动会话。');
|
|
102
140
|
return true;
|
|
103
141
|
}
|
|
104
142
|
async handlePwd(chatId, userId) {
|
|
105
143
|
const workDir = this.deps.sessionManager.getWorkDir(userId);
|
|
106
|
-
await this.
|
|
144
|
+
await this.replySender().sendTextReply(chatId, `当前工作目录: ${escapePathForMarkdown(workDir)}`);
|
|
107
145
|
return true;
|
|
108
146
|
}
|
|
109
147
|
async handleStatus(chatId, userId, platform) {
|
|
@@ -120,28 +158,29 @@ export class CommandHandler {
|
|
|
120
158
|
`工作目录: ${escapePathForMarkdown(workDir)}`,
|
|
121
159
|
`会话: ${sessionId ?? '无'}`,
|
|
122
160
|
];
|
|
123
|
-
await this.
|
|
161
|
+
await this.replySender().sendTextReply(chatId, lines.join('\n'));
|
|
124
162
|
return true;
|
|
125
163
|
}
|
|
126
164
|
async handleCd(chatId, userId, dir, _platform) {
|
|
127
165
|
// 如果 dir 为空,显示目录选择界面
|
|
128
166
|
if (!dir) {
|
|
129
167
|
const currentDir = this.deps.sessionManager.getWorkDir(userId);
|
|
130
|
-
|
|
131
|
-
|
|
168
|
+
const s = this.replySender();
|
|
169
|
+
if (s.sendDirectorySelection) {
|
|
170
|
+
await s.sendDirectorySelection(chatId, currentDir, userId);
|
|
132
171
|
}
|
|
133
172
|
else {
|
|
134
|
-
await
|
|
173
|
+
await s.sendTextReply(chatId, `当前目录: ${escapePathForMarkdown(currentDir)}\n使用 /cd <路径> 切换`);
|
|
135
174
|
}
|
|
136
175
|
return true;
|
|
137
176
|
}
|
|
138
177
|
try {
|
|
139
178
|
const resolved = await this.deps.sessionManager.setWorkDir(userId, dir);
|
|
140
|
-
await this.
|
|
179
|
+
await this.replySender().sendTextReply(chatId, `📁 工作目录已切换到: ${escapePathForMarkdown(resolved)}\n\n` +
|
|
141
180
|
`🔄 AI 会话已重置,下一条消息将使用全新上下文。`);
|
|
142
181
|
}
|
|
143
182
|
catch (err) {
|
|
144
|
-
await this.
|
|
183
|
+
await this.replySender().sendTextReply(chatId, err instanceof Error ? err.message : String(err));
|
|
145
184
|
}
|
|
146
185
|
return true;
|
|
147
186
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { normalizeSlashCommandForDispatch } from "./handler.js";
|
|
3
|
+
describe("normalizeSlashCommandForDispatch", () => {
|
|
4
|
+
it("strips @bot suffix from first segment", () => {
|
|
5
|
+
expect(normalizeSlashCommandForDispatch("/new@my_open_im_bot")).toBe("/new");
|
|
6
|
+
expect(normalizeSlashCommandForDispatch(" /help@BotName ")).toBe("/help");
|
|
7
|
+
});
|
|
8
|
+
it("preserves arguments after command", () => {
|
|
9
|
+
expect(normalizeSlashCommandForDispatch("/resume@bot 2")).toBe("/resume 2");
|
|
10
|
+
expect(normalizeSlashCommandForDispatch("/cd@bot /tmp/foo")).toBe("/cd /tmp/foo");
|
|
11
|
+
});
|
|
12
|
+
it("leaves non-command or no-at unchanged", () => {
|
|
13
|
+
expect(normalizeSlashCommandForDispatch("/new")).toBe("/new");
|
|
14
|
+
expect(normalizeSlashCommandForDispatch("hello @user")).toBe("hello @user");
|
|
15
|
+
expect(normalizeSlashCommandForDispatch("plain")).toBe("plain");
|
|
16
|
+
});
|
|
17
|
+
});
|
package/dist/config/types.d.ts
CHANGED
|
@@ -29,6 +29,10 @@ export interface Config {
|
|
|
29
29
|
/** Codex 访问 chatgpt.com 的代理(如 http://127.0.0.1:7890) */
|
|
30
30
|
codexProxy?: string;
|
|
31
31
|
claudeWorkDir: string;
|
|
32
|
+
/**
|
|
33
|
+
* Claude SDK 进程内会话空闲多久后回收(分钟)。0 表示关闭空闲回收(仍受适配器内 MAX_ACTIVE_SESSIONS 限制)。默认 30。
|
|
34
|
+
*/
|
|
35
|
+
claudeSessionIdleTtlMinutes: number;
|
|
32
36
|
claudeModel?: string;
|
|
33
37
|
/** 是否跳过 AI 工具的权限确认(默认 true) */
|
|
34
38
|
skipPermissions?: boolean;
|
|
@@ -137,6 +141,8 @@ export interface FileToolClaude {
|
|
|
137
141
|
cliPath?: string;
|
|
138
142
|
workDir?: string;
|
|
139
143
|
skipPermissions?: boolean;
|
|
144
|
+
/** 空闲会话回收间隔(分钟),0 表示关闭 */
|
|
145
|
+
sessionIdleTtlMinutes?: number;
|
|
140
146
|
/** HTTP/HTTPS 代理,用于访问 Claude API(如 http://127.0.0.1:7890) */
|
|
141
147
|
proxy?: string;
|
|
142
148
|
/** Claude API 配置(优先级:环境变量 > tools.claude.env > ~/.claude/settings.json) */
|
package/dist/config-web.js
CHANGED
package/dist/config.js
CHANGED
|
@@ -219,6 +219,19 @@ export function loadConfig() {
|
|
|
219
219
|
const skipPermissions = process.env.OPEN_IM_SKIP_PERMISSIONS === 'false'
|
|
220
220
|
? false
|
|
221
221
|
: (tc.skipPermissions ?? true);
|
|
222
|
+
const envIdleRaw = process.env.OPEN_IM_CLAUDE_SESSION_IDLE_TTL_MINUTES;
|
|
223
|
+
const envIdleParsed = envIdleRaw !== undefined && envIdleRaw !== '' ? Number.parseInt(envIdleRaw, 10) : NaN;
|
|
224
|
+
const fileIdle = tc.sessionIdleTtlMinutes;
|
|
225
|
+
let claudeSessionIdleTtlMinutes;
|
|
226
|
+
if (Number.isFinite(envIdleParsed)) {
|
|
227
|
+
claudeSessionIdleTtlMinutes = Math.max(0, envIdleParsed);
|
|
228
|
+
}
|
|
229
|
+
else if (typeof fileIdle === 'number' && Number.isFinite(fileIdle)) {
|
|
230
|
+
claudeSessionIdleTtlMinutes = Math.max(0, fileIdle);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
claudeSessionIdleTtlMinutes = 30;
|
|
234
|
+
}
|
|
222
235
|
// 6. 校验 Claude API 凭证(SDK 模式需要)
|
|
223
236
|
// 支持:官方 API Key、Auth Token、或自定义 API(第三方模型等,BASE_URL + token)
|
|
224
237
|
if (aiCommand === 'claude') {
|
|
@@ -442,6 +455,7 @@ export function loadConfig() {
|
|
|
442
455
|
claudeProxy,
|
|
443
456
|
codexProxy,
|
|
444
457
|
claudeWorkDir,
|
|
458
|
+
claudeSessionIdleTtlMinutes,
|
|
445
459
|
claudeModel: process.env.ANTHROPIC_MODEL,
|
|
446
460
|
skipPermissions,
|
|
447
461
|
logDir,
|
package/dist/index.js
CHANGED
|
@@ -104,11 +104,8 @@ async function sendLifecycleNotification(platform, message) {
|
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
106
106
|
log.info(`[${platform}] Sending lifecycle notification to chatId=${chatId}`);
|
|
107
|
-
await mod.sendNotification(chatId, message)
|
|
108
|
-
|
|
109
|
-
}).catch((err) => {
|
|
110
|
-
log.warn(`Failed to send ${platform} notification:`, err);
|
|
111
|
-
});
|
|
107
|
+
await mod.sendNotification(chatId, message);
|
|
108
|
+
log.info(`[${platform}] Lifecycle notification sent successfully`);
|
|
112
109
|
}
|
|
113
110
|
function buildStartupMessage(platform, appVersion, aiCommand, defaultWorkDir, sessionManager) {
|
|
114
111
|
let sessionDir;
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* 1. Access control check → deny with error message
|
|
8
8
|
* 2. setActiveChatId(platform, chatId)
|
|
9
9
|
* 3. setChatUser(chatId, userId, platform)
|
|
10
|
-
* 4. Command dispatch via commandHandler.dispatch()
|
|
10
|
+
* 4. Command dispatch via commandHandler.dispatch(..., commandSender)
|
|
11
|
+
* (与 sendTextReply 同源,保证 WorkBuddy 等平台的斜杠命令带上 msgId)
|
|
11
12
|
* 5. If not handled: empty text → return, otherwise enqueue AI request
|
|
12
13
|
* 6. Handle queue-full notification (rejected / queued)
|
|
13
14
|
*
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* 1. Access control check → deny with error message
|
|
8
8
|
* 2. setActiveChatId(platform, chatId)
|
|
9
9
|
* 3. setChatUser(chatId, userId, platform)
|
|
10
|
-
* 4. Command dispatch via commandHandler.dispatch()
|
|
10
|
+
* 4. Command dispatch via commandHandler.dispatch(..., commandSender)
|
|
11
|
+
* (与 sendTextReply 同源,保证 WorkBuddy 等平台的斜杠命令带上 msgId)
|
|
11
12
|
* 5. If not handled: empty text → return, otherwise enqueue AI request
|
|
12
13
|
* 6. Handle queue-full notification (rejected / queued)
|
|
13
14
|
*
|
|
@@ -61,7 +62,12 @@ export async function handleTextFlow(params) {
|
|
|
61
62
|
replyToMessageId,
|
|
62
63
|
});
|
|
63
64
|
};
|
|
64
|
-
const
|
|
65
|
+
const commandSender = {
|
|
66
|
+
sendTextReply: async (chatId, text, _threadCtx) => {
|
|
67
|
+
await sendTextReply(chatId, text);
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
const handled = await ctx.commandHandler.dispatch(text, chatId, userId, platform, dispatchHandler, commandSender);
|
|
65
71
|
if (handled) {
|
|
66
72
|
return true;
|
|
67
73
|
}
|
|
@@ -87,7 +87,9 @@ describe('handleTextFlow', () => {
|
|
|
87
87
|
expect(setActiveChatId).toHaveBeenCalledWith('telegram', 'chat-1');
|
|
88
88
|
expect(setChatUser).toHaveBeenCalledWith('chat-1', 'user-1', 'telegram');
|
|
89
89
|
// Command dispatch was called with correct args
|
|
90
|
-
expect(dispatch).toHaveBeenCalledWith('/help', 'chat-1', 'user-1', 'telegram', expect.any(Function)
|
|
90
|
+
expect(dispatch).toHaveBeenCalledWith('/help', 'chat-1', 'user-1', 'telegram', expect.any(Function), expect.objectContaining({
|
|
91
|
+
sendTextReply: expect.any(Function),
|
|
92
|
+
}));
|
|
91
93
|
// Does NOT enqueue (sendTextReply not called for queue messages)
|
|
92
94
|
expect(sendTextReply).not.toHaveBeenCalled();
|
|
93
95
|
});
|
package/dist/qq/client.js
CHANGED
|
@@ -254,11 +254,20 @@ async function connectWebSocket(config, handler) {
|
|
|
254
254
|
settle(() => { }); // 清理 ready timeout
|
|
255
255
|
clearTimers();
|
|
256
256
|
ws = null;
|
|
257
|
-
|
|
257
|
+
const reasonStr = reason.toString();
|
|
258
|
+
if (code === 4009) {
|
|
259
|
+
log.info(`QQ gateway session idle timeout (4009), reconnecting… (${reasonStr})`);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
log.info(`QQ gateway closed: ${code} ${reasonStr}`);
|
|
263
|
+
}
|
|
258
264
|
if (stopped)
|
|
259
265
|
return;
|
|
260
|
-
|
|
266
|
+
// 4009 仅为长连接会话过期,HTTP access_token 仍有效,勿清空 tokenState
|
|
267
|
+
if (code === 4004 || code === 4006 || code === 4007) {
|
|
261
268
|
tokenState = null;
|
|
269
|
+
}
|
|
270
|
+
if (code === 4004 || code === 4006 || code === 4007 || code === 4009) {
|
|
262
271
|
sessionId = null;
|
|
263
272
|
seq = null;
|
|
264
273
|
}
|