release: v2.0.7 - Enhanced Telegram Reporting & File Visibility (2026-05-14)
This commit is contained in:
+117
-1
@@ -33,6 +33,7 @@ import type { ProjectTemplateId } from './scaffolder/templates';
|
||||
import { TelegramHttpClient } from './integrations/telegram/telegramClient';
|
||||
import { TelegramBot } from './integrations/telegram/telegramBot';
|
||||
import { AIService } from './core/services';
|
||||
import type { CompanyState } from './features/company';
|
||||
import { SettingsPanelProvider } from './features/settings/settingsPanelProvider';
|
||||
import { resolveScopeForAgent, openKnowledgeMapEditor } from './skills/agentKnowledgeMap';
|
||||
import { getBrainTokenIndex } from './retrieval';
|
||||
@@ -232,6 +233,83 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
};
|
||||
|
||||
/** Telegram has a 4096-char per-message limit. Split on paragraph/sentence boundaries to keep replies readable. */
|
||||
/**
|
||||
* Cheap heuristic: does the message look like a *work order* the user
|
||||
* wants the company to execute? Triggers company-turn routing.
|
||||
*
|
||||
* Conservative matches only — we'd rather miss a borderline case
|
||||
* (user retries with clearer wording) than mis-route a question
|
||||
* into a company turn (which spends LLM calls + writes to disk).
|
||||
*
|
||||
* Positive signals:
|
||||
* • Explicit dispatch prefix: "CEO한테", "회사한테", "팀한테"
|
||||
* • Korean imperative verbs at sentence end: 만들어/해줘/작성해줘/
|
||||
* 짜줘/구현해/만들어줘/돌려줘/실행해줘/분석해줘/정리해줘
|
||||
* • English imperatives: "make X", "build X", "create X", "implement"
|
||||
*
|
||||
* Negative signals (override → treat as question, not order):
|
||||
* • Ends with "?" — pure question
|
||||
* • Contains "알려줘 / 어디 / 뭐야 / what / where" — informational
|
||||
*/
|
||||
function _looksLikeWorkOrder(text: string): boolean {
|
||||
const t = (text || '').trim();
|
||||
if (!t) return false;
|
||||
// Explicit dispatch prefix wins regardless of other signals.
|
||||
if (/(CEO|회사|팀)\s*(한테|에게|보고|에)/i.test(t)) return true;
|
||||
// Strong informational signals — *not* a work order.
|
||||
if (/[??]$/.test(t)) return false;
|
||||
if (/(어디(에|에서|야)|뭐야|얼마|언제|왜|^(누구|어떻게|뭐))/i.test(t)) return false;
|
||||
// Korean imperative tails (한국어 청유·명령형 종결).
|
||||
if (/(만들어|짜줘|작성해|구현해|돌려줘|실행해|분석해|정리해|보고해|해줘|짜봐|만들어줘)/i.test(t)) return true;
|
||||
// English imperative leads.
|
||||
if (/^\s*(make|build|create|implement|run|analyze|generate|write|fix|add|remove)\b/i.test(t)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a `[COMPANY CONTEXT]` block describing the workspace, the
|
||||
* current company state, and the most recent session directory. Lets
|
||||
* the bot answer questions like "어디에 저장했어?" by reading its own
|
||||
* mirror history *plus* the resolved absolute path on disk.
|
||||
*
|
||||
* Returns '' when company mode is off, so the prompt stays minimal
|
||||
* for users who only use the Telegram bot for RAG-chat.
|
||||
*/
|
||||
function _buildTelegramCompanyContext(state: CompanyState, ctx: vscode.ExtensionContext): string {
|
||||
if (!state.enabled) return '';
|
||||
const ws = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '';
|
||||
const lines: string[] = [`[COMPANY CONTEXT]`];
|
||||
lines.push(`회사명: ${state.companyName || '1인 기업'}`);
|
||||
if (ws) lines.push(`작업 폴더 (워크스페이스 루트): ${ws}`);
|
||||
// Surface the most recent session dir so follow-up questions
|
||||
// ("폴더 어디에 있어?", "방금 만든 파일 경로") have a concrete answer.
|
||||
const latestSession = _latestCompanySessionDir(ctx);
|
||||
if (latestSession) {
|
||||
lines.push(`최근 작업 세션 폴더: ${latestSession}`);
|
||||
lines.push(`(이 안에 _brief.md, _report.md, 각 에이전트별 산출물이 저장됨)`);
|
||||
}
|
||||
lines.push('');
|
||||
lines.push('당신의 역할: 이 회사의 비서(Secretary). 사용자(사장님)의 질문에 답할 때 위 경로 정보를 *그대로* 활용하세요.');
|
||||
lines.push('"실제 파일 시스템에 접근할 수 없다" 같은 답변은 잘못된 것입니다 — 위 경로가 실제 시스템 경로입니다.');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/** Find the newest `<workspace>/.astra/company/sessions/<ts>/` directory, or '' if none. */
|
||||
function _latestCompanySessionDir(ctx: vscode.ExtensionContext): string {
|
||||
try {
|
||||
const ws = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
const baseDir = ws
|
||||
? path.join(ws, '.astra', 'company', 'sessions')
|
||||
: path.join(ctx.globalStorageUri.fsPath, 'company', 'sessions');
|
||||
if (!fs.existsSync(baseDir)) return '';
|
||||
const dirs = fs.readdirSync(baseDir)
|
||||
.filter((n) => fs.statSync(path.join(baseDir, n)).isDirectory())
|
||||
.sort()
|
||||
.reverse();
|
||||
return dirs[0] ? path.join(baseDir, dirs[0]) : '';
|
||||
} catch { return ''; }
|
||||
}
|
||||
|
||||
const chunkTelegramMessage = (text: string, max = 4000): string[] => {
|
||||
if (text.length <= max) return [text];
|
||||
const out: string[] = [];
|
||||
@@ -269,6 +347,36 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
preview: text.length > 80 ? text.slice(0, 80) + '…' : text,
|
||||
});
|
||||
|
||||
// ── 1인 기업 모드 라우팅 ────────────────────────────────────────
|
||||
// When company mode is on AND the message looks like a work
|
||||
// *order* (imperative verbs like "만들어줘 / 해줘 / 작성해줘" or
|
||||
// an explicit "CEO한테 …" prefix), route through the company
|
||||
// dispatcher instead of the simple RAG-chat path. The dispatcher
|
||||
// emits a Telegram mirror at the end, so the user gets a proper
|
||||
// report back. This fixes the previous symptom where the bot
|
||||
// refused to "deliver messages to CEO" — that was a routing gap,
|
||||
// not a missing capability.
|
||||
const { readCompanyState } = await import('./features/company');
|
||||
const companyState = readCompanyState(context);
|
||||
if (companyState.enabled && _looksLikeWorkOrder(text)) {
|
||||
const { appendTelegramMessage } = await import('./integrations/telegram/conversationHistory');
|
||||
appendTelegramMessage({ chatId, role: 'user', text, kind: 'user' });
|
||||
logInfo('Telegram: routing to company turn.', { chatId, preview: text.slice(0, 60) });
|
||||
// Fire-and-forget: the dispatcher's secretary mirror sends
|
||||
// the final report. We return an immediate ack so the user
|
||||
// sees the bot acknowledged the order.
|
||||
void (async () => {
|
||||
try {
|
||||
await provider!._runCompanyTurn(text);
|
||||
} catch (e: any) {
|
||||
logError('Telegram → company turn failed.', { error: e?.message ?? String(e) });
|
||||
}
|
||||
})();
|
||||
const ack = '🧭 CEO에게 전달했어요. 작업 끝나면 보고드릴게요.';
|
||||
appendTelegramMessage({ chatId, role: 'assistant', text: ack, kind: 'reply' });
|
||||
return ack;
|
||||
}
|
||||
|
||||
// Per-chat agent override → global default → mapping default.
|
||||
const perChatAgents = cfg.get<Record<string, string>>('telegram.agentByChatId', {}) || {};
|
||||
const perChatAgent = perChatAgents[String(chatId)];
|
||||
@@ -305,7 +413,15 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
}
|
||||
|
||||
const systemPrompt = buildTelegramSystemPrompt(!!contextBlock);
|
||||
// Build the system prompt with company-aware context. The bot
|
||||
// is the *secretary* of a virtual company when company mode is
|
||||
// on — telling it so lets it answer "where did you save X?"
|
||||
// properly instead of falling back to "I don't have file
|
||||
// system access".
|
||||
const companyContextBlock = _buildTelegramCompanyContext(companyState, context);
|
||||
const systemPrompt = buildTelegramSystemPrompt(!!contextBlock)
|
||||
+ (companyContextBlock ? `\n\n${companyContextBlock}` : '');
|
||||
|
||||
// Per-chat conversation history — without this every inbound
|
||||
// is a fresh turn, so the user "tells the bot something" and
|
||||
// it gets immediately forgotten. We inline the recent N
|
||||
|
||||
Reference in New Issue
Block a user