companionbot 0.12.2 → 0.13.1
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/ai/claude.js +14 -137
- package/dist/config/constants.js +0 -65
- package/dist/telegram/handlers/messages.js +15 -181
- package/package.json +1 -1
package/dist/ai/claude.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import Anthropic
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
2
|
import { tools, executeTool } from "../tools/index.js";
|
|
3
3
|
import { MAX_TOOL_ITERATIONS, TOOL_INPUT_SUMMARY_LENGTH, TOOL_OUTPUT_SUMMARY_LENGTH, } from "../utils/constants.js";
|
|
4
4
|
import { withRetry, withTimeout, } from "../utils/retry.js";
|
|
@@ -86,7 +86,8 @@ export function calculateTokenBudgets(modelId, thinkingLevel, inputTokens) {
|
|
|
86
86
|
}
|
|
87
87
|
return { maxTokens, thinkingBudget };
|
|
88
88
|
}
|
|
89
|
-
export async function chat(messages, systemPrompt, modelId = "sonnet",
|
|
89
|
+
export async function chat(messages, systemPrompt, modelId = "sonnet", _thinkingLevel // 사용 안 함 (non-streaming에서 에러 발생)
|
|
90
|
+
) {
|
|
90
91
|
const client = getClient();
|
|
91
92
|
const modelConfig = MODELS[modelId];
|
|
92
93
|
const toolsUsed = [];
|
|
@@ -111,10 +112,10 @@ export async function chat(messages, systemPrompt, modelId = "sonnet", thinkingL
|
|
|
111
112
|
}
|
|
112
113
|
return total;
|
|
113
114
|
};
|
|
114
|
-
//
|
|
115
|
+
// 토큰 계산 (thinking 비활성화 - non-streaming에서 에러 발생)
|
|
115
116
|
const inputTokens = estimateInputTokens();
|
|
116
|
-
const
|
|
117
|
-
console.log(`[Chat] model=${modelId},
|
|
117
|
+
const maxTokens = 8192;
|
|
118
|
+
console.log(`[Chat] model=${modelId}, input~${inputTokens}, maxTokens=${maxTokens}`);
|
|
118
119
|
// API 요청 파라미터 빌드 (도구 루프에서도 동일하게 사용)
|
|
119
120
|
const buildRequestParams = () => {
|
|
120
121
|
const params = {
|
|
@@ -126,13 +127,6 @@ export async function chat(messages, systemPrompt, modelId = "sonnet", thinkingL
|
|
|
126
127
|
if (systemPrompt) {
|
|
127
128
|
params.system = systemPrompt;
|
|
128
129
|
}
|
|
129
|
-
// thinking 활성화 (budget > 0인 경우)
|
|
130
|
-
if (thinkingBudget > 0) {
|
|
131
|
-
params.thinking = {
|
|
132
|
-
type: "enabled",
|
|
133
|
-
budget_tokens: thinkingBudget,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
130
|
return params;
|
|
137
131
|
};
|
|
138
132
|
let response;
|
|
@@ -214,132 +208,15 @@ export async function chat(messages, systemPrompt, modelId = "sonnet", thinkingL
|
|
|
214
208
|
};
|
|
215
209
|
}
|
|
216
210
|
/**
|
|
217
|
-
* 스마트 채팅 -
|
|
218
|
-
*
|
|
219
|
-
* 전략:
|
|
220
|
-
* - 먼저 스트리밍으로 시도
|
|
221
|
-
* - 도구 호출이 감지되면 (stop_reason === "tool_use") 기존 chat()으로 폴백
|
|
222
|
-
* - 스트리밍은 최종 텍스트 응답에만 사용
|
|
211
|
+
* 스마트 채팅 - chat()의 단순 래퍼
|
|
223
212
|
*
|
|
224
|
-
*
|
|
225
|
-
* 스트리밍 중 에러 발생 시 적절한 에러 메시지를 반환하거나 예외를 전파함
|
|
213
|
+
* 도구 사용 여부를 별도로 반환하여 호출자가 구분할 수 있게 함
|
|
226
214
|
*/
|
|
227
|
-
export async function chatSmart(messages, systemPrompt, modelId, thinkingLevel = "medium"
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
:
|
|
232
|
-
|
|
233
|
-
if (!callbacks.onChunk) {
|
|
234
|
-
const result = await chat(messages, systemPrompt, modelId, thinkingLevel);
|
|
235
|
-
return { text: result.text, usedTools: result.toolsUsed.length > 0, toolsUsed: result.toolsUsed };
|
|
236
|
-
}
|
|
237
|
-
const client = getClient();
|
|
238
|
-
const modelConfig = MODELS[modelId];
|
|
239
|
-
// 메시지를 API 형식으로 변환
|
|
240
|
-
const apiMessages = messages.map((m) => ({
|
|
241
|
-
role: m.role,
|
|
242
|
-
content: m.content,
|
|
243
|
-
}));
|
|
244
|
-
// 입력 토큰 추정
|
|
245
|
-
let inputTokens = 0;
|
|
246
|
-
if (systemPrompt) {
|
|
247
|
-
inputTokens += Math.ceil(systemPrompt.length / 3);
|
|
248
|
-
}
|
|
249
|
-
for (const msg of apiMessages) {
|
|
250
|
-
const content = typeof msg.content === "string"
|
|
251
|
-
? msg.content
|
|
252
|
-
: JSON.stringify(msg.content);
|
|
253
|
-
inputTokens += Math.ceil(content.length / 3);
|
|
254
|
-
}
|
|
255
|
-
// 동적 토큰 budget 계산
|
|
256
|
-
const { maxTokens, thinkingBudget } = calculateTokenBudgets(modelId, thinkingLevel, inputTokens);
|
|
257
|
-
console.log(`[ChatSmart] model=${modelId}, thinking=${thinkingLevel}, input~${inputTokens}, maxTokens=${maxTokens}, budget=${thinkingBudget}`);
|
|
258
|
-
// 스트리밍 요청 파라미터
|
|
259
|
-
const params = {
|
|
260
|
-
model: modelConfig.id,
|
|
261
|
-
max_tokens: maxTokens,
|
|
262
|
-
messages: apiMessages,
|
|
263
|
-
tools: tools,
|
|
264
|
-
stream: true,
|
|
215
|
+
export async function chatSmart(messages, systemPrompt, modelId, thinkingLevel = "medium") {
|
|
216
|
+
const result = await chat(messages, systemPrompt, modelId, thinkingLevel);
|
|
217
|
+
return {
|
|
218
|
+
text: result.text,
|
|
219
|
+
usedTools: result.toolsUsed.length > 0,
|
|
220
|
+
toolsUsed: result.toolsUsed
|
|
265
221
|
};
|
|
266
|
-
if (systemPrompt) {
|
|
267
|
-
params.system = systemPrompt;
|
|
268
|
-
}
|
|
269
|
-
// Thinking 활성화 (스트리밍에서도 지원)
|
|
270
|
-
if (thinkingBudget > 0) {
|
|
271
|
-
params.thinking = {
|
|
272
|
-
type: "enabled",
|
|
273
|
-
budget_tokens: thinkingBudget,
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
let accumulated = "";
|
|
277
|
-
let streamingStarted = false;
|
|
278
|
-
try {
|
|
279
|
-
const stream = client.messages.stream(params);
|
|
280
|
-
// 스트리밍 이벤트 처리
|
|
281
|
-
stream.on("text", async (text) => {
|
|
282
|
-
streamingStarted = true;
|
|
283
|
-
accumulated += text;
|
|
284
|
-
try {
|
|
285
|
-
await callbacks.onChunk(text, accumulated);
|
|
286
|
-
}
|
|
287
|
-
catch (err) {
|
|
288
|
-
// editMessageText 실패 등은 무시하고 계속
|
|
289
|
-
console.warn("[Stream] Chunk callback error (ignored):", err);
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
// 스트림 완료 대기
|
|
293
|
-
const finalMessage = await stream.finalMessage();
|
|
294
|
-
const stopReason = finalMessage.stop_reason;
|
|
295
|
-
// 도구 호출이 필요한 경우 - 일반 chat으로 폴백
|
|
296
|
-
// 주의: chat()은 내부에서 withRetry를 사용하므로 여기서 추가 재시도 불필요
|
|
297
|
-
if (stopReason === "tool_use") {
|
|
298
|
-
console.log("[Stream] Tool use detected, falling back to chat()");
|
|
299
|
-
// 도구 이름 추출하여 콜백 호출
|
|
300
|
-
const toolUseBlocks = finalMessage.content.filter((block) => block.type === "tool_use");
|
|
301
|
-
const toolNames = toolUseBlocks.map(t => t.name);
|
|
302
|
-
if (callbacks.onToolStart && toolNames.length > 0) {
|
|
303
|
-
try {
|
|
304
|
-
await callbacks.onToolStart(toolNames);
|
|
305
|
-
}
|
|
306
|
-
catch (err) {
|
|
307
|
-
console.warn("[Stream] Tool start callback error (ignored):", err);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
// 도구 사용 시 thinking 비활성화 (API 에러 방지)
|
|
311
|
-
const result = await chat(messages, systemPrompt, modelId, "off");
|
|
312
|
-
return { text: result.text, usedTools: true, toolsUsed: result.toolsUsed };
|
|
313
|
-
}
|
|
314
|
-
// 성공적으로 스트리밍 완료
|
|
315
|
-
return { text: accumulated, usedTools: false, toolsUsed: [] };
|
|
316
|
-
}
|
|
317
|
-
catch (error) {
|
|
318
|
-
// 스트리밍 시작 전 에러 (연결 실패 등) - 재시도 가능
|
|
319
|
-
if (!streamingStarted && error instanceof APIError) {
|
|
320
|
-
// Rate limit 또는 서버 에러는 withRetry로 재시도
|
|
321
|
-
if (error.status === 429 || error.status >= 500) {
|
|
322
|
-
console.log(`[Stream] Pre-stream error (${error.status}), retrying with withRetry...`);
|
|
323
|
-
return await withRetry(async () => {
|
|
324
|
-
// 재시도 시 일반 chat 사용 (스트리밍 대신, thinking 비활성화)
|
|
325
|
-
const result = await chat(messages, systemPrompt, modelId, "off");
|
|
326
|
-
return { text: result.text, usedTools: false, toolsUsed: result.toolsUsed };
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
// 스트리밍 중 에러 - 재시도 불가 (이미 청크가 전송됨)
|
|
331
|
-
if (streamingStarted) {
|
|
332
|
-
console.error("[Stream] Error during streaming (cannot retry):", error);
|
|
333
|
-
// 이미 일부 텍스트가 전송됐으므로, 에러 메시지를 추가하거나 부분 결과 반환
|
|
334
|
-
if (accumulated.length > 0) {
|
|
335
|
-
return {
|
|
336
|
-
text: accumulated + "\n\n(응답 생성 중 오류 발생)",
|
|
337
|
-
usedTools: false,
|
|
338
|
-
toolsUsed: []
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
// 그 외 에러는 전파
|
|
343
|
-
throw error;
|
|
344
|
-
}
|
|
345
222
|
}
|
package/dist/config/constants.js
CHANGED
|
@@ -70,19 +70,6 @@ export const MEMORY = {
|
|
|
70
70
|
// 텔레그램/UI 관련 설정
|
|
71
71
|
// ============================================
|
|
72
72
|
export const TELEGRAM = {
|
|
73
|
-
/** 스트리밍 업데이트 간격 - 적응형 (밀리초) */
|
|
74
|
-
STREAM_UPDATE_INTERVAL_MS: 500, // 레거시 호환용
|
|
75
|
-
/** 스트리밍 적응형 간격 설정 */
|
|
76
|
-
STREAM_INTERVAL: {
|
|
77
|
-
/** 첫 번째 업데이트 (즉시) */
|
|
78
|
-
FIRST_MS: 0,
|
|
79
|
-
/** 초기 빠른 업데이트 (처음 5회) */
|
|
80
|
-
FAST_MS: 200,
|
|
81
|
-
/** 이후 일반 간격 */
|
|
82
|
-
NORMAL_MS: 400,
|
|
83
|
-
/** 빠른 업데이트 횟수 */
|
|
84
|
-
FAST_COUNT: 5,
|
|
85
|
-
},
|
|
86
73
|
/** 텔레그램 메시지 최대 길이 */
|
|
87
74
|
MAX_MESSAGE_LENGTH: 4096,
|
|
88
75
|
/** 최대 이미지 크기 (바이트) - 10MB */
|
|
@@ -91,60 +78,8 @@ export const TELEGRAM = {
|
|
|
91
78
|
MAX_URL_FETCH: 3,
|
|
92
79
|
/** 캘린더 미리보기 이벤트 수 */
|
|
93
80
|
CALENDAR_PREVIEW_COUNT: 3,
|
|
94
|
-
/** 스트리밍 UI 아이콘 */
|
|
95
|
-
STREAM_ICONS: {
|
|
96
|
-
THINKING: "💭",
|
|
97
|
-
TYPING: "▌",
|
|
98
|
-
TOOL: "🔧",
|
|
99
|
-
DONE: "",
|
|
100
|
-
},
|
|
101
81
|
/** Typing indicator 자동 갱신 간격 (밀리초) - 텔레그램은 5초 후 만료 */
|
|
102
82
|
TYPING_REFRESH_MS: 4000,
|
|
103
|
-
/** 도구별 친화적 상태 메시지 */
|
|
104
|
-
TOOL_STATUS_MESSAGES: {
|
|
105
|
-
// 검색/정보 조회
|
|
106
|
-
web_search: { icon: "🔍", text: "웹에서 검색하는 중", estimate: "5-10초" },
|
|
107
|
-
web_fetch: { icon: "📄", text: "웹페이지 읽는 중", estimate: "3-5초" },
|
|
108
|
-
get_weather: { icon: "🌤️", text: "날씨 확인 중", estimate: "2-3초" },
|
|
109
|
-
memory_search: { icon: "🧠", text: "기억 검색 중", estimate: "1-2초" },
|
|
110
|
-
memory_reindex: { icon: "🧠", text: "기억 재색인 중", estimate: "10-30초" },
|
|
111
|
-
// 파일 작업
|
|
112
|
-
read_file: { icon: "📖", text: "파일 읽는 중", estimate: "1초" },
|
|
113
|
-
write_file: { icon: "✍️", text: "파일 쓰는 중", estimate: "1초" },
|
|
114
|
-
edit_file: { icon: "✏️", text: "파일 수정 중", estimate: "1초" },
|
|
115
|
-
list_directory: { icon: "📁", text: "폴더 살펴보는 중", estimate: "1초" },
|
|
116
|
-
// 명령어 실행
|
|
117
|
-
run_command: { icon: "⚡", text: "명령어 실행 중", estimate: "변동" },
|
|
118
|
-
list_sessions: { icon: "📋", text: "세션 목록 확인 중", estimate: "1초" },
|
|
119
|
-
get_session_log: { icon: "📜", text: "로그 가져오는 중", estimate: "1초" },
|
|
120
|
-
kill_session: { icon: "🛑", text: "세션 종료 중", estimate: "1초" },
|
|
121
|
-
// 일정/리마인더
|
|
122
|
-
get_calendar_events: { icon: "📅", text: "일정 확인 중", estimate: "2-3초" },
|
|
123
|
-
add_calendar_event: { icon: "📅", text: "일정 추가 중", estimate: "2-3초" },
|
|
124
|
-
delete_calendar_event: { icon: "📅", text: "일정 삭제 중", estimate: "2초" },
|
|
125
|
-
set_reminder: { icon: "⏰", text: "알림 설정 중", estimate: "1초" },
|
|
126
|
-
list_reminders: { icon: "⏰", text: "알림 목록 확인 중", estimate: "1초" },
|
|
127
|
-
cancel_reminder: { icon: "⏰", text: "알림 취소 중", estimate: "1초" },
|
|
128
|
-
// 브리핑/하트비트
|
|
129
|
-
control_briefing: { icon: "☀️", text: "브리핑 설정 중", estimate: "1초" },
|
|
130
|
-
send_briefing_now: { icon: "☀️", text: "브리핑 준비 중", estimate: "5-10초" },
|
|
131
|
-
control_heartbeat: { icon: "💓", text: "하트비트 설정 중", estimate: "1초" },
|
|
132
|
-
run_heartbeat_check: { icon: "💓", text: "체크 실행 중", estimate: "3-5초" },
|
|
133
|
-
// 서브에이전트
|
|
134
|
-
spawn_agent: { icon: "🤖", text: "서브에이전트 생성 중", estimate: "2-3초" },
|
|
135
|
-
list_agents: { icon: "🤖", text: "에이전트 목록 확인 중", estimate: "1초" },
|
|
136
|
-
cancel_agent: { icon: "🤖", text: "에이전트 취소 중", estimate: "1초" },
|
|
137
|
-
// Cron
|
|
138
|
-
add_cron: { icon: "🕐", text: "예약 작업 추가 중", estimate: "1초" },
|
|
139
|
-
list_crons: { icon: "🕐", text: "예약 작업 확인 중", estimate: "1초" },
|
|
140
|
-
remove_cron: { icon: "🕐", text: "예약 작업 삭제 중", estimate: "1초" },
|
|
141
|
-
toggle_cron: { icon: "🕐", text: "예약 작업 설정 중", estimate: "1초" },
|
|
142
|
-
run_cron: { icon: "🕐", text: "예약 작업 실행 중", estimate: "변동" },
|
|
143
|
-
// 기타
|
|
144
|
-
change_model: { icon: "🔄", text: "모델 변경 중", estimate: "1초" },
|
|
145
|
-
save_memory: { icon: "💾", text: "기억 저장 중", estimate: "1초" },
|
|
146
|
-
save_persona: { icon: "✨", text: "페르소나 저장 중", estimate: "2초" },
|
|
147
|
-
},
|
|
148
83
|
};
|
|
149
84
|
// ============================================
|
|
150
85
|
// 보안/토큰 관련 설정
|
|
@@ -7,29 +7,6 @@ import { extractUrls, fetchWebContent, formatUrlContent, buildSystemPrompt, } fr
|
|
|
7
7
|
import { estimateMessagesTokens } from "../../utils/tokens.js";
|
|
8
8
|
import { TOKENS, TELEGRAM } from "../../config/constants.js";
|
|
9
9
|
import { toUserFriendlyError } from "../../utils/retry.js";
|
|
10
|
-
/**
|
|
11
|
-
* 도구 이름을 친화적인 상태 메시지로 변환
|
|
12
|
-
*/
|
|
13
|
-
function getToolStatusMessage(toolName) {
|
|
14
|
-
const messages = TELEGRAM.TOOL_STATUS_MESSAGES;
|
|
15
|
-
return messages[toolName] || { icon: "🔧", text: toolName, estimate: "" };
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* 여러 도구의 상태를 친화적인 메시지로 포맷
|
|
19
|
-
*/
|
|
20
|
-
function formatToolsStatus(toolNames) {
|
|
21
|
-
if (toolNames.length === 0)
|
|
22
|
-
return "";
|
|
23
|
-
if (toolNames.length === 1) {
|
|
24
|
-
const status = getToolStatusMessage(toolNames[0]);
|
|
25
|
-
const estimateText = status.estimate ? ` (약 ${status.estimate})` : "";
|
|
26
|
-
return `${status.icon} ${status.text}...${estimateText}`;
|
|
27
|
-
}
|
|
28
|
-
// 여러 도구: 아이콘만 모아서 표시 + 첫 번째 도구 텍스트
|
|
29
|
-
const icons = toolNames.map(t => getToolStatusMessage(t).icon).join(" ");
|
|
30
|
-
const firstStatus = getToolStatusMessage(toolNames[0]);
|
|
31
|
-
return `${icons} ${firstStatus.text} 외 ${toolNames.length - 1}개...`;
|
|
32
|
-
}
|
|
33
10
|
/**
|
|
34
11
|
* Typing indicator를 주기적으로 갱신하는 클래스
|
|
35
12
|
* 텔레그램은 5초 후 typing 상태가 자동 해제되므로, 긴 작업 중 유지 필요
|
|
@@ -134,161 +111,12 @@ function splitLongMessage(text, maxLength = TELEGRAM.MAX_MESSAGE_LENGTH) {
|
|
|
134
111
|
return parts;
|
|
135
112
|
}
|
|
136
113
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* 개선사항:
|
|
140
|
-
* - 적응형 업데이트 간격 (첫 응답 빠르게, 이후 안정화)
|
|
141
|
-
* - 도구 사용 시 친화적인 상태 메시지 표시
|
|
142
|
-
* - 긴 응답 자동 분할
|
|
143
|
-
* - thinking 상태 표시
|
|
144
|
-
* - 진행 단계 표시 (생각 중 → 도구 사용 → 응답 작성)
|
|
114
|
+
* 응답을 전송 (긴 응답은 분할)
|
|
145
115
|
*/
|
|
146
|
-
async function
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
? `${STREAM_ICONS.THINKING} 생각하는 중...`
|
|
151
|
-
: `${STREAM_ICONS.THINKING} 응답 준비 중...`;
|
|
152
|
-
const placeholder = await ctx.reply(initialText);
|
|
153
|
-
const chatId = ctx.chat.id;
|
|
154
|
-
const messageId = placeholder.message_id;
|
|
155
|
-
let lastUpdate = 0; // 첫 업데이트는 즉시
|
|
156
|
-
let updateCount = 0;
|
|
157
|
-
let lastText = "";
|
|
158
|
-
let currentPhase = "thinking";
|
|
159
|
-
let toolStartTime = 0;
|
|
160
|
-
let currentToolNames = [];
|
|
161
|
-
// 적응형 간격 계산
|
|
162
|
-
const getInterval = () => {
|
|
163
|
-
if (updateCount === 0)
|
|
164
|
-
return STREAM_INTERVAL.FIRST_MS;
|
|
165
|
-
if (updateCount <= STREAM_INTERVAL.FAST_COUNT)
|
|
166
|
-
return STREAM_INTERVAL.FAST_MS;
|
|
167
|
-
return STREAM_INTERVAL.NORMAL_MS;
|
|
168
|
-
};
|
|
169
|
-
// 도구 실행 시간이 길어지면 추가 피드백
|
|
170
|
-
let toolTimeoutId = null;
|
|
171
|
-
const startToolTimeout = async () => {
|
|
172
|
-
toolTimeoutId = setTimeout(async () => {
|
|
173
|
-
if (currentPhase === "tools" && currentToolNames.length > 0) {
|
|
174
|
-
const elapsed = Math.round((Date.now() - toolStartTime) / 1000);
|
|
175
|
-
const statusMsg = formatToolsStatus(currentToolNames);
|
|
176
|
-
try {
|
|
177
|
-
await ctx.api.editMessageText(chatId, messageId, `${statusMsg}\n⏳ ${elapsed}초 경과... 조금만 기다려줘!`);
|
|
178
|
-
}
|
|
179
|
-
catch {
|
|
180
|
-
// 무시
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}, 5000); // 5초 후 추가 피드백
|
|
184
|
-
};
|
|
185
|
-
const clearToolTimeout = () => {
|
|
186
|
-
if (toolTimeoutId) {
|
|
187
|
-
clearTimeout(toolTimeoutId);
|
|
188
|
-
toolTimeoutId = null;
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
try {
|
|
192
|
-
const streamCallbacks = {
|
|
193
|
-
onChunk: async (_chunk, accumulated) => {
|
|
194
|
-
// 첫 청크 도착 시 thinking → generating 단계 전환
|
|
195
|
-
if (currentPhase === "thinking" && accumulated.length > 0) {
|
|
196
|
-
currentPhase = "generating";
|
|
197
|
-
}
|
|
198
|
-
const now = Date.now();
|
|
199
|
-
const interval = getInterval();
|
|
200
|
-
// 간격 체크 + 실제 변경 있을 때만 업데이트
|
|
201
|
-
if (now - lastUpdate >= interval && accumulated !== lastText) {
|
|
202
|
-
// 텔레그램 제한 초과 시 잘라서 표시
|
|
203
|
-
const displayText = accumulated.length > MAX_MESSAGE_LENGTH - 10
|
|
204
|
-
? accumulated.slice(0, MAX_MESSAGE_LENGTH - 20) + "\n…(계속)"
|
|
205
|
-
: accumulated;
|
|
206
|
-
try {
|
|
207
|
-
await ctx.api.editMessageText(chatId, messageId, displayText + " " + STREAM_ICONS.TYPING);
|
|
208
|
-
lastUpdate = now;
|
|
209
|
-
lastText = accumulated;
|
|
210
|
-
updateCount++;
|
|
211
|
-
}
|
|
212
|
-
catch {
|
|
213
|
-
// rate limit 등 무시 - 다음 chunk에서 재시도
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
},
|
|
217
|
-
onToolStart: async (toolNames) => {
|
|
218
|
-
currentPhase = "tools";
|
|
219
|
-
currentToolNames = toolNames;
|
|
220
|
-
toolStartTime = Date.now();
|
|
221
|
-
// 친화적인 도구 상태 메시지 생성
|
|
222
|
-
const statusMsg = formatToolsStatus(toolNames);
|
|
223
|
-
try {
|
|
224
|
-
await ctx.api.editMessageText(chatId, messageId, statusMsg);
|
|
225
|
-
}
|
|
226
|
-
catch {
|
|
227
|
-
// 무시
|
|
228
|
-
}
|
|
229
|
-
// 5초 후 추가 피드백 예약
|
|
230
|
-
startToolTimeout();
|
|
231
|
-
},
|
|
232
|
-
};
|
|
233
|
-
const result = await chatSmart(messages, systemPrompt, modelId, thinkingLevel, streamCallbacks);
|
|
234
|
-
// 도구 타임아웃 정리
|
|
235
|
-
clearToolTimeout();
|
|
236
|
-
// 도구를 사용한 경우
|
|
237
|
-
if (result.usedTools) {
|
|
238
|
-
// 도구 사용 정보를 친화적으로 표시
|
|
239
|
-
const toolSummary = result.toolsUsed.map(t => {
|
|
240
|
-
const status = getToolStatusMessage(t.name);
|
|
241
|
-
return `${status.icon} ${status.text}`;
|
|
242
|
-
}).join("\n");
|
|
243
|
-
const toolIndicator = `${toolSummary}\n\n`;
|
|
244
|
-
const finalText = toolIndicator + result.text;
|
|
245
|
-
// 긴 응답 분할 처리
|
|
246
|
-
const parts = splitLongMessage(finalText);
|
|
247
|
-
try {
|
|
248
|
-
// 첫 번째 파트로 placeholder 교체
|
|
249
|
-
await ctx.api.editMessageText(chatId, messageId, parts[0]);
|
|
250
|
-
// 추가 파트가 있으면 새 메시지로 전송
|
|
251
|
-
for (let i = 1; i < parts.length; i++) {
|
|
252
|
-
await ctx.reply(parts[i]);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
catch {
|
|
256
|
-
// 실패시 placeholder 삭제 후 새로 전송
|
|
257
|
-
try {
|
|
258
|
-
await ctx.api.deleteMessage(chatId, messageId);
|
|
259
|
-
}
|
|
260
|
-
catch { }
|
|
261
|
-
for (const part of parts) {
|
|
262
|
-
await ctx.reply(part);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
return result.text;
|
|
266
|
-
}
|
|
267
|
-
// 최종 메시지 업데이트 (커서 제거) + 긴 응답 분할
|
|
268
|
-
const parts = splitLongMessage(result.text);
|
|
269
|
-
try {
|
|
270
|
-
await ctx.api.editMessageText(chatId, messageId, parts[0]);
|
|
271
|
-
// 추가 파트 전송
|
|
272
|
-
for (let i = 1; i < parts.length; i++) {
|
|
273
|
-
await ctx.reply(parts[i]);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
catch {
|
|
277
|
-
// 이미 동일 텍스트면 에러 발생 가능 - 무시
|
|
278
|
-
}
|
|
279
|
-
return result.text;
|
|
280
|
-
}
|
|
281
|
-
catch (error) {
|
|
282
|
-
// 도구 타임아웃 정리
|
|
283
|
-
clearToolTimeout();
|
|
284
|
-
// 에러 발생 시 placeholder 삭제
|
|
285
|
-
try {
|
|
286
|
-
await ctx.api.deleteMessage(chatId, messageId);
|
|
287
|
-
}
|
|
288
|
-
catch {
|
|
289
|
-
// 삭제 실패해도 계속 진행
|
|
290
|
-
}
|
|
291
|
-
throw error; // 에러 재전파
|
|
116
|
+
async function sendResponse(ctx, text) {
|
|
117
|
+
const parts = splitLongMessage(text);
|
|
118
|
+
for (const part of parts) {
|
|
119
|
+
await ctx.reply(part);
|
|
292
120
|
}
|
|
293
121
|
}
|
|
294
122
|
/**
|
|
@@ -437,6 +265,9 @@ export function registerMessageHandlers(bot) {
|
|
|
437
265
|
}
|
|
438
266
|
// 히스토리에는 간략 버전 저장 + JSONL에 영구 저장
|
|
439
267
|
addMessage(chatId, "user", messageForHistory);
|
|
268
|
+
// Typing indicator 시작 (긴 작업 동안 유지)
|
|
269
|
+
const typingIndicator = new TypingIndicator(ctx);
|
|
270
|
+
typingIndicator.start();
|
|
440
271
|
try {
|
|
441
272
|
const systemPrompt = await buildSystemPrompt(modelId, history);
|
|
442
273
|
// API 호출용 메시지 준비 (URL 전체 내용 포함)
|
|
@@ -452,11 +283,13 @@ export function registerMessageHandlers(bot) {
|
|
|
452
283
|
};
|
|
453
284
|
}
|
|
454
285
|
}
|
|
455
|
-
//
|
|
456
|
-
const
|
|
457
|
-
|
|
286
|
+
// AI 응답 생성 (typing indicator 동안)
|
|
287
|
+
const result = await chatSmart(messagesForApi, systemPrompt, modelId, thinkingLevel);
|
|
288
|
+
typingIndicator.stop();
|
|
289
|
+
// 응답 전송 (긴 응답은 분할)
|
|
290
|
+
await sendResponse(ctx, result.text);
|
|
458
291
|
// 메모리 + JSONL에 영구 저장
|
|
459
|
-
addMessage(chatId, "assistant",
|
|
292
|
+
addMessage(chatId, "assistant", result.text);
|
|
460
293
|
// 스마트 트리밍 (요약 포함) - autoCompactIfNeeded 대체
|
|
461
294
|
const summarizeFn = async (messages) => {
|
|
462
295
|
const summaryPrompt = "다음 대화를 핵심만 3-4문장으로 요약해. 중요한 정보(이름, 선호도, 약속 등)는 반드시 포함:\n\n" +
|
|
@@ -473,6 +306,7 @@ export function registerMessageHandlers(bot) {
|
|
|
473
306
|
}
|
|
474
307
|
}
|
|
475
308
|
catch (error) {
|
|
309
|
+
typingIndicator.stop();
|
|
476
310
|
recordError();
|
|
477
311
|
// 에러를 사용자 친화적 메시지로 변환
|
|
478
312
|
const friendlyError = toUserFriendlyError(error);
|