feat(bot/extension/watcher): Discord 아티팩트 알림 개선 — 파일 첨부 전송, truncation 확대, 동적 .md 감시
This commit is contained in:
130
bot.py
130
bot.py
@@ -491,34 +491,49 @@ class GravityBot(commands.Bot):
|
|||||||
event_label = "생성" if event.event_type == EventType.FILE_CREATED else "업데이트"
|
event_label = "생성" if event.event_type == EventType.FILE_CREATED else "업데이트"
|
||||||
|
|
||||||
full_content = event.content.strip()
|
full_content = event.content.strip()
|
||||||
CHUNK_SIZE = 4000 # Discord embed desc limit is 4096
|
if not full_content:
|
||||||
|
full_content = "(빈 파일)"
|
||||||
|
|
||||||
# Split into chunks for long content
|
FILE_ATTACH_THRESHOLD = 4000 # Above this, send as file attachment
|
||||||
chunks = []
|
|
||||||
while full_content:
|
|
||||||
chunks.append(full_content[:CHUNK_SIZE])
|
|
||||||
full_content = full_content[CHUNK_SIZE:]
|
|
||||||
|
|
||||||
if not chunks:
|
if len(full_content) > FILE_ATTACH_THRESHOLD:
|
||||||
chunks = ["(빈 파일)"]
|
# Long content → summary embed + file attachment
|
||||||
|
# Extract first meaningful paragraph for summary
|
||||||
|
summary_lines = []
|
||||||
|
for line in full_content.split('\n'):
|
||||||
|
if line.strip():
|
||||||
|
summary_lines.append(line.strip())
|
||||||
|
if len('\n'.join(summary_lines)) > 300:
|
||||||
|
break
|
||||||
|
summary = '\n'.join(summary_lines[:5])
|
||||||
|
if len(summary) > 500:
|
||||||
|
summary = summary[:500] + '...'
|
||||||
|
|
||||||
# First chunk with title
|
|
||||||
embed = discord.Embed(
|
|
||||||
title=f"{label} ({event_label}됨)",
|
|
||||||
description=chunks[0],
|
|
||||||
color=discord.Color.blue(),
|
|
||||||
timestamp=datetime.now(timezone.utc),
|
|
||||||
)
|
|
||||||
embed.set_footer(text=f"Session: {event.conversation_id[:8]}")
|
|
||||||
await channel.send(embed=embed)
|
|
||||||
|
|
||||||
# Additional chunks if content is long
|
|
||||||
for i, chunk in enumerate(chunks[1:], 2):
|
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title=f"{label} (계속 {i}/{len(chunks)})",
|
title=f"{label} ({event_label}됨)",
|
||||||
description=chunk,
|
description=f"{summary}\n\n📎 *전체 내용은 첨부 파일을 확인하세요* ({len(full_content):,}자)",
|
||||||
color=discord.Color.blue(),
|
color=discord.Color.blue(),
|
||||||
|
timestamp=datetime.now(timezone.utc),
|
||||||
)
|
)
|
||||||
|
embed.set_footer(text=f"Session: {event.conversation_id[:8]}")
|
||||||
|
|
||||||
|
# Create in-memory file attachment
|
||||||
|
import io
|
||||||
|
file_bytes = full_content.encode('utf-8')
|
||||||
|
discord_file = discord.File(
|
||||||
|
io.BytesIO(file_bytes),
|
||||||
|
filename=event.file_name,
|
||||||
|
)
|
||||||
|
await channel.send(embed=embed, file=discord_file)
|
||||||
|
else:
|
||||||
|
# Short content → inline embed (original behavior)
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"{label} ({event_label}됨)",
|
||||||
|
description=full_content,
|
||||||
|
color=discord.Color.blue(),
|
||||||
|
timestamp=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
embed.set_footer(text=f"Session: {event.conversation_id[:8]}")
|
||||||
await channel.send(embed=embed)
|
await channel.send(embed=embed)
|
||||||
|
|
||||||
# ─── Approval Scanner ────────────────────────────────────────────
|
# ─── Approval Scanner ────────────────────────────────────────────
|
||||||
@@ -823,31 +838,80 @@ class GravityBot(commands.Bot):
|
|||||||
data = json.loads(f.read_text(encoding="utf-8-sig"))
|
data = json.loads(f.read_text(encoding="utf-8-sig"))
|
||||||
project = data.get("project_name", Config.PROJECT_NAME)
|
project = data.get("project_name", Config.PROJECT_NAME)
|
||||||
content = data.get("content", "")
|
content = data.get("content", "")
|
||||||
|
attached_files = data.get("attached_files", [])
|
||||||
|
|
||||||
if content:
|
if content or attached_files:
|
||||||
channel = await self._get_channel(project)
|
channel = await self._get_channel(project)
|
||||||
if channel:
|
if channel:
|
||||||
# Split long content
|
import io
|
||||||
CHUNK = 4000
|
|
||||||
chunks = [content[i:i+CHUNK] for i in range(0, len(content), CHUNK)]
|
# ── Send attached files (from Extension's writeChatSnapshotWithFiles) ──
|
||||||
for i, chunk in enumerate(chunks):
|
discord_files = []
|
||||||
title = "💬 AI 대화 내용" if i == 0 else f"💬 (계속 {i+1}/{len(chunks)})"
|
for af in attached_files:
|
||||||
|
af_name = af.get("name", "document.md")
|
||||||
|
af_content = af.get("content", "")
|
||||||
|
if af_content:
|
||||||
|
discord_files.append(discord.File(
|
||||||
|
io.BytesIO(af_content.encode("utf-8")),
|
||||||
|
filename=af_name,
|
||||||
|
))
|
||||||
|
|
||||||
|
FILE_ATTACH_THRESHOLD = 4000
|
||||||
|
if len(content) > FILE_ATTACH_THRESHOLD:
|
||||||
|
# Long chat content → summary embed + file attachment
|
||||||
|
summary = content[:500].rsplit('\n', 1)[0]
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title=title,
|
title="💬 AI 대화 내용",
|
||||||
description=chunk,
|
description=f"{summary}\n\n📎 *전체 내용은 첨부 파일 참조* ({len(content):,}자)",
|
||||||
|
color=discord.Color.purple(),
|
||||||
|
timestamp=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
# Add content itself as file attachment
|
||||||
|
discord_files.append(discord.File(
|
||||||
|
io.BytesIO(content.encode("utf-8")),
|
||||||
|
filename="chat_message.md",
|
||||||
|
))
|
||||||
|
try:
|
||||||
|
await channel.send(embed=embed, files=discord_files)
|
||||||
|
except discord.NotFound:
|
||||||
|
logger.warning(f"Channel deleted for {project}, re-creating...")
|
||||||
|
self.project_channels.pop(project, None)
|
||||||
|
channel = await self._get_channel(project)
|
||||||
|
if channel:
|
||||||
|
# Re-create files (discord.File consumed after send)
|
||||||
|
discord_files2 = []
|
||||||
|
for af in attached_files:
|
||||||
|
af_name = af.get("name", "document.md")
|
||||||
|
af_content = af.get("content", "")
|
||||||
|
if af_content:
|
||||||
|
discord_files2.append(discord.File(
|
||||||
|
io.BytesIO(af_content.encode("utf-8")),
|
||||||
|
filename=af_name,
|
||||||
|
))
|
||||||
|
discord_files2.append(discord.File(
|
||||||
|
io.BytesIO(content.encode("utf-8")),
|
||||||
|
filename="chat_message.md",
|
||||||
|
))
|
||||||
|
await channel.send(embed=embed, files=discord_files2)
|
||||||
|
else:
|
||||||
|
# Short content → inline embed (original)
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="💬 AI 대화 내용",
|
||||||
|
description=content,
|
||||||
color=discord.Color.purple(),
|
color=discord.Color.purple(),
|
||||||
timestamp=datetime.now(timezone.utc),
|
timestamp=datetime.now(timezone.utc),
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await channel.send(embed=embed)
|
await channel.send(
|
||||||
|
embed=embed,
|
||||||
|
files=discord_files if discord_files else discord.utils.MISSING,
|
||||||
|
)
|
||||||
except discord.NotFound:
|
except discord.NotFound:
|
||||||
# Channel was deleted — invalidate cache and retry once
|
|
||||||
logger.warning(f"Channel deleted for {project}, re-creating...")
|
logger.warning(f"Channel deleted for {project}, re-creating...")
|
||||||
self.project_channels.pop(project, None)
|
self.project_channels.pop(project, None)
|
||||||
channel = await self._get_channel(project)
|
channel = await self._get_channel(project)
|
||||||
if channel:
|
if channel:
|
||||||
await channel.send(embed=embed)
|
await channel.send(embed=embed)
|
||||||
break
|
|
||||||
|
|
||||||
f.unlink() # Cleanup
|
f.unlink() # Cleanup
|
||||||
except (json.JSONDecodeError, OSError) as e:
|
except (json.JSONDecodeError, OSError) as e:
|
||||||
|
|||||||
@@ -333,10 +333,11 @@ class RemoteTransport(BridgeTransport):
|
|||||||
"conversation_id": conv_id, "project_name": project,
|
"conversation_id": conv_id, "project_name": project,
|
||||||
})
|
})
|
||||||
|
|
||||||
async def asend_chat(self, project: str, content: str) -> None:
|
async def asend_chat(self, project: str, content: str, *, attached_files: list[dict] | None = None) -> None:
|
||||||
await self._arequest_retry("POST", "/api/chat", {
|
payload: dict = {"project_name": project, "content": content}
|
||||||
"project_name": project, "content": content,
|
if attached_files:
|
||||||
})
|
payload["attached_files"] = attached_files
|
||||||
|
await self._arequest_retry("POST", "/api/chat", payload)
|
||||||
|
|
||||||
async def asend_event(self, event_data: dict) -> None:
|
async def asend_event(self, event_data: dict) -> None:
|
||||||
await self._arequest_retry("POST", "/api/event", event_data)
|
await self._arequest_retry("POST", "/api/event", event_data)
|
||||||
|
|||||||
@@ -308,9 +308,11 @@ class CollectorBridge:
|
|||||||
data = json.loads(f.read_text(encoding="utf-8-sig"))
|
data = json.loads(f.read_text(encoding="utf-8-sig"))
|
||||||
project = data.get("project_name", self.project_name)
|
project = data.get("project_name", self.project_name)
|
||||||
content = data.get("content", "")
|
content = data.get("content", "")
|
||||||
if content:
|
attached_files = data.get("attached_files", [])
|
||||||
await self.remote.asend_chat(project, content)
|
if content or attached_files:
|
||||||
logger.info(f"[COLLECTOR] → Gateway: chat snapshot len={len(content)}")
|
await self.remote.asend_chat(project, content, attached_files=attached_files)
|
||||||
|
af_info = f" +{len(attached_files)} files" if attached_files else ""
|
||||||
|
logger.info(f"[COLLECTOR] → Gateway: chat snapshot len={len(content)}{af_info}")
|
||||||
f.unlink() # Cleanup after forwarding
|
f.unlink() # Cleanup after forwarding
|
||||||
except (json.JSONDecodeError, OSError) as e:
|
except (json.JSONDecodeError, OSError) as e:
|
||||||
logger.warning(f"[COLLECTOR] bad chat snapshot {f.name}: {e}")
|
logger.warning(f"[COLLECTOR] bad chat snapshot {f.name}: {e}")
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ class Config:
|
|||||||
"walkthrough.md",
|
"walkthrough.md",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Extension-based monitoring: any file with these extensions in brain/{conv}/ is watched
|
||||||
|
WATCHED_EXTENSIONS: set = {".md"}
|
||||||
|
|
||||||
# Discord message limits
|
# Discord message limits
|
||||||
DISCORD_MSG_LIMIT: int = 2000
|
DISCORD_MSG_LIMIT: int = 2000
|
||||||
DISCORD_EMBED_DESC_LIMIT: int = 4096
|
DISCORD_EMBED_DESC_LIMIT: int = 4096
|
||||||
|
|||||||
5
docs/devlog/2026-03-13.md
Normal file
5
docs/devlog/2026-03-13.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Devlog — 2026-03-13
|
||||||
|
|
||||||
|
| # | 시간 | 작업 | 커밋 | 상태 |
|
||||||
|
|---|------|------|------|------|
|
||||||
|
| 001 | 08:56 | Discord 아티팩트 알림 개선 — truncation 확대, 파일 첨부 전송, 동적 .md 감시 | `pending` | ✅ |
|
||||||
@@ -149,6 +149,31 @@ function writeChatSnapshot(text) {
|
|||||||
console.log(`Gravity Bridge: snapshot write error: ${e.message}`);
|
console.log(`Gravity Bridge: snapshot write error: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function writeChatSnapshotWithFiles(text, files) {
|
||||||
|
try {
|
||||||
|
const snapshotDir = path.join(bridgePath, 'chat_snapshots');
|
||||||
|
if (!fs.existsSync(snapshotDir)) {
|
||||||
|
fs.mkdirSync(snapshotDir, { recursive: true });
|
||||||
|
}
|
||||||
|
const id = Date.now().toString();
|
||||||
|
const data = {
|
||||||
|
id,
|
||||||
|
project_name: projectName,
|
||||||
|
content: text,
|
||||||
|
attached_files: files,
|
||||||
|
timestamp: Date.now() / 1000,
|
||||||
|
};
|
||||||
|
const filePath = path.join(snapshotDir, `${id}.json`);
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
||||||
|
logToFile(`[SNAPSHOT] written ${id}.json (${text.length} chars, ${files.length} files)`);
|
||||||
|
if (activeSessionId) {
|
||||||
|
writeRegistration(activeSessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(`Gravity Bridge: snapshot+files write error: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
// ─── Command File Watcher (Discord → Antigravity) ───
|
// ─── Command File Watcher (Discord → Antigravity) ───
|
||||||
function processCommandFile(filePath) {
|
function processCommandFile(filePath) {
|
||||||
try {
|
try {
|
||||||
@@ -1821,8 +1846,8 @@ function setupMonitor() {
|
|||||||
if (text.length > 10) {
|
if (text.length > 10) {
|
||||||
lastResponseCaptureStep = actualIdx;
|
lastResponseCaptureStep = actualIdx;
|
||||||
logToFile(`[RT-CAPTURE] step=${actualIdx} (${text.length} chars)`);
|
logToFile(`[RT-CAPTURE] step=${actualIdx} (${text.length} chars)`);
|
||||||
const truncated = text.length > 1800
|
const truncated = text.length > 3500
|
||||||
? text.substring(0, 1800) + '\n\n_(이하 생략)_'
|
? text.substring(0, 3500) + '\n\n_(이하 생략)_'
|
||||||
: text;
|
: text;
|
||||||
writeChatSnapshot(`💬 **AI 응답**\n\n${truncated}`);
|
writeChatSnapshot(`💬 **AI 응답**\n\n${truncated}`);
|
||||||
break;
|
break;
|
||||||
@@ -2129,7 +2154,12 @@ function setupMonitor() {
|
|||||||
if (notifyStep) {
|
if (notifyStep) {
|
||||||
if (notifyStep.stepIndex > lastNotifyStepIndex) {
|
if (notifyStep.stepIndex > lastNotifyStepIndex) {
|
||||||
lastNotifyStepIndex = notifyStep.stepIndex;
|
lastNotifyStepIndex = notifyStep.stepIndex;
|
||||||
const content = notifyStep.step?.notifyUser?.notificationContent || '';
|
const notifyData = notifyStep.step?.notifyUser || {};
|
||||||
|
const content = notifyData.notificationContent || '';
|
||||||
|
// Log full structure once for schema discovery
|
||||||
|
if (pollCount <= 3 || notifyStep.stepIndex <= lastNotifyStepIndex + 1) {
|
||||||
|
logToFile(`[NOTIFY-STEP] keys=[${Object.keys(notifyData).join(',')}]`);
|
||||||
|
}
|
||||||
logToFile(`[NOTIFY-STEP] NEW step=${notifyStep.stepIndex} content=${content.length} chars`);
|
logToFile(`[NOTIFY-STEP] NEW step=${notifyStep.stepIndex} content=${content.length} chars`);
|
||||||
// Filter: relay all non-empty notifications
|
// Filter: relay all non-empty notifications
|
||||||
if (content.length > 10) {
|
if (content.length > 10) {
|
||||||
@@ -2138,6 +2168,35 @@ function setupMonitor() {
|
|||||||
else if (content.length > 0) {
|
else if (content.length > 0) {
|
||||||
logToFile(`[NOTIFY-STEP] skipped (too short: ${content.length} chars): ${content}`);
|
logToFile(`[NOTIFY-STEP] skipped (too short: ${content.length} chars): ${content}`);
|
||||||
}
|
}
|
||||||
|
// ── PathsToReview: read and relay referenced artifact files ──
|
||||||
|
const pathsToReview = notifyData.pathsToReview
|
||||||
|
|| notifyData.paths_to_review
|
||||||
|
|| notifyData.filePaths
|
||||||
|
|| [];
|
||||||
|
if (pathsToReview.length > 0) {
|
||||||
|
logToFile(`[NOTIFY-STEP] PathsToReview: ${pathsToReview.length} files`);
|
||||||
|
for (const filePath of pathsToReview.slice(0, 5)) {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
const fileName = path.basename(filePath);
|
||||||
|
const MAX_ARTIFACT_SIZE = 8000;
|
||||||
|
const truncatedContent = fileContent.length > MAX_ARTIFACT_SIZE
|
||||||
|
? fileContent.substring(0, MAX_ARTIFACT_SIZE) + '\n\n_(이하 생략)_'
|
||||||
|
: fileContent;
|
||||||
|
// Write as snapshot with attached_files for bot to send as Discord file
|
||||||
|
writeChatSnapshotWithFiles(`📎 **문서: ${fileName}** (${Math.round(fileContent.length / 1024)}KB)`, [{ name: fileName, content: truncatedContent }]);
|
||||||
|
logToFile(`[NOTIFY-STEP] relayed artifact: ${fileName} (${fileContent.length} chars)`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logToFile(`[NOTIFY-STEP] artifact not found: ${filePath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
logToFile(`[NOTIFY-STEP] artifact read error: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (pollCount <= 5) {
|
else if (pollCount <= 5) {
|
||||||
@@ -2277,8 +2336,8 @@ function setupMonitor() {
|
|||||||
textContent = typeof s.rawOutput === 'string' ? s.rawOutput : JSON.stringify(s.rawOutput);
|
textContent = typeof s.rawOutput === 'string' ? s.rawOutput : JSON.stringify(s.rawOutput);
|
||||||
if (textContent.length > 10) {
|
if (textContent.length > 10) {
|
||||||
logToFile(`[RESPONSE-CAPTURE] found ${sType} (${textContent.length} chars) at step ${offset + ri}`);
|
logToFile(`[RESPONSE-CAPTURE] found ${sType} (${textContent.length} chars) at step ${offset + ri}`);
|
||||||
const truncated = textContent.length > 1800
|
const truncated = textContent.length > 3500
|
||||||
? textContent.substring(0, 1800) + '\n\n_(이하 생략)_'
|
? textContent.substring(0, 3500) + '\n\n_(이하 생략)_'
|
||||||
: textContent;
|
: textContent;
|
||||||
writeChatSnapshot(`💬 **AI 응답**\n\n${truncated}`);
|
writeChatSnapshot(`💬 **AI 응답**\n\n${truncated}`);
|
||||||
break;
|
break;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -113,6 +113,27 @@ function writeChatSnapshot(text: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function writeChatSnapshotWithFiles(text: string, files: Array<{name: string, content: string}>) {
|
||||||
|
try {
|
||||||
|
const snapshotDir = path.join(bridgePath, 'chat_snapshots');
|
||||||
|
if (!fs.existsSync(snapshotDir)) { fs.mkdirSync(snapshotDir, { recursive: true }); }
|
||||||
|
const id = Date.now().toString();
|
||||||
|
const data = {
|
||||||
|
id,
|
||||||
|
project_name: projectName,
|
||||||
|
content: text,
|
||||||
|
attached_files: files,
|
||||||
|
timestamp: Date.now() / 1000,
|
||||||
|
};
|
||||||
|
const filePath = path.join(snapshotDir, `${id}.json`);
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
||||||
|
logToFile(`[SNAPSHOT] written ${id}.json (${text.length} chars, ${files.length} files)`);
|
||||||
|
if (activeSessionId) { writeRegistration(activeSessionId); }
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log(`Gravity Bridge: snapshot+files write error: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ─── Command File Watcher (Discord → Antigravity) ───
|
// ─── Command File Watcher (Discord → Antigravity) ───
|
||||||
|
|
||||||
@@ -1823,8 +1844,8 @@ function setupMonitor() {
|
|||||||
if (text.length > 10) {
|
if (text.length > 10) {
|
||||||
lastResponseCaptureStep = actualIdx;
|
lastResponseCaptureStep = actualIdx;
|
||||||
logToFile(`[RT-CAPTURE] step=${actualIdx} (${text.length} chars)`);
|
logToFile(`[RT-CAPTURE] step=${actualIdx} (${text.length} chars)`);
|
||||||
const truncated = text.length > 1800
|
const truncated = text.length > 3500
|
||||||
? text.substring(0, 1800) + '\n\n_(이하 생략)_'
|
? text.substring(0, 3500) + '\n\n_(이하 생략)_'
|
||||||
: text;
|
: text;
|
||||||
writeChatSnapshot(`💬 **AI 응답**\n\n${truncated}`);
|
writeChatSnapshot(`💬 **AI 응답**\n\n${truncated}`);
|
||||||
break;
|
break;
|
||||||
@@ -2116,7 +2137,12 @@ function setupMonitor() {
|
|||||||
if (notifyStep) {
|
if (notifyStep) {
|
||||||
if (notifyStep.stepIndex > lastNotifyStepIndex) {
|
if (notifyStep.stepIndex > lastNotifyStepIndex) {
|
||||||
lastNotifyStepIndex = notifyStep.stepIndex;
|
lastNotifyStepIndex = notifyStep.stepIndex;
|
||||||
const content = notifyStep.step?.notifyUser?.notificationContent || '';
|
const notifyData = notifyStep.step?.notifyUser || {} as any;
|
||||||
|
const content = notifyData.notificationContent || '';
|
||||||
|
// Log full structure once for schema discovery
|
||||||
|
if (pollCount <= 3 || notifyStep.stepIndex <= lastNotifyStepIndex + 1) {
|
||||||
|
logToFile(`[NOTIFY-STEP] keys=[${Object.keys(notifyData).join(',')}]`);
|
||||||
|
}
|
||||||
logToFile(`[NOTIFY-STEP] NEW step=${notifyStep.stepIndex} content=${content.length} chars`);
|
logToFile(`[NOTIFY-STEP] NEW step=${notifyStep.stepIndex} content=${content.length} chars`);
|
||||||
// Filter: relay all non-empty notifications
|
// Filter: relay all non-empty notifications
|
||||||
if (content.length > 10) {
|
if (content.length > 10) {
|
||||||
@@ -2124,6 +2150,37 @@ function setupMonitor() {
|
|||||||
} else if (content.length > 0) {
|
} else if (content.length > 0) {
|
||||||
logToFile(`[NOTIFY-STEP] skipped (too short: ${content.length} chars): ${content}`);
|
logToFile(`[NOTIFY-STEP] skipped (too short: ${content.length} chars): ${content}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── PathsToReview: read and relay referenced artifact files ──
|
||||||
|
const pathsToReview: string[] = notifyData.pathsToReview
|
||||||
|
|| notifyData.paths_to_review
|
||||||
|
|| notifyData.filePaths
|
||||||
|
|| [];
|
||||||
|
if (pathsToReview.length > 0) {
|
||||||
|
logToFile(`[NOTIFY-STEP] PathsToReview: ${pathsToReview.length} files`);
|
||||||
|
for (const filePath of pathsToReview.slice(0, 5)) {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
const fileName = path.basename(filePath);
|
||||||
|
const MAX_ARTIFACT_SIZE = 8000;
|
||||||
|
const truncatedContent = fileContent.length > MAX_ARTIFACT_SIZE
|
||||||
|
? fileContent.substring(0, MAX_ARTIFACT_SIZE) + '\n\n_(이하 생략)_'
|
||||||
|
: fileContent;
|
||||||
|
// Write as snapshot with attached_files for bot to send as Discord file
|
||||||
|
writeChatSnapshotWithFiles(
|
||||||
|
`📎 **문서: ${fileName}** (${Math.round(fileContent.length/1024)}KB)`,
|
||||||
|
[{ name: fileName, content: truncatedContent }]
|
||||||
|
);
|
||||||
|
logToFile(`[NOTIFY-STEP] relayed artifact: ${fileName} (${fileContent.length} chars)`);
|
||||||
|
} else {
|
||||||
|
logToFile(`[NOTIFY-STEP] artifact not found: ${filePath}`);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
logToFile(`[NOTIFY-STEP] artifact read error: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (pollCount <= 5) {
|
} else if (pollCount <= 5) {
|
||||||
logToFile(`[NOTIFY-STEP] null (no notify step in session)`);
|
logToFile(`[NOTIFY-STEP] null (no notify step in session)`);
|
||||||
@@ -2251,8 +2308,8 @@ function setupMonitor() {
|
|||||||
if (!textContent && s?.rawOutput) textContent = typeof s.rawOutput === 'string' ? s.rawOutput : JSON.stringify(s.rawOutput);
|
if (!textContent && s?.rawOutput) textContent = typeof s.rawOutput === 'string' ? s.rawOutput : JSON.stringify(s.rawOutput);
|
||||||
if (textContent.length > 10) {
|
if (textContent.length > 10) {
|
||||||
logToFile(`[RESPONSE-CAPTURE] found ${sType} (${textContent.length} chars) at step ${offset + ri}`);
|
logToFile(`[RESPONSE-CAPTURE] found ${sType} (${textContent.length} chars) at step ${offset + ri}`);
|
||||||
const truncated = textContent.length > 1800
|
const truncated = textContent.length > 3500
|
||||||
? textContent.substring(0, 1800) + '\n\n_(이하 생략)_'
|
? textContent.substring(0, 3500) + '\n\n_(이하 생략)_'
|
||||||
: textContent;
|
: textContent;
|
||||||
writeChatSnapshot(`💬 **AI 응답**\n\n${truncated}`);
|
writeChatSnapshot(`💬 **AI 응답**\n\n${truncated}`);
|
||||||
break;
|
break;
|
||||||
|
|||||||
10
gateway.py
10
gateway.py
@@ -157,9 +157,10 @@ class GatewayAPI:
|
|||||||
data = await request.json()
|
data = await request.json()
|
||||||
project = data.get("project_name", "")
|
project = data.get("project_name", "")
|
||||||
content = data.get("content", "")
|
content = data.get("content", "")
|
||||||
|
attached_files = data.get("attached_files", [])
|
||||||
|
|
||||||
if not project or not content:
|
if not project or (not content and not attached_files):
|
||||||
return web.json_response({"ok": False, "error": "project_name and content required"}, status=400)
|
return web.json_response({"ok": False, "error": "project_name and content/attached_files required"}, status=400)
|
||||||
|
|
||||||
# Write to chat_snapshots dir for bot's scanner
|
# Write to chat_snapshots dir for bot's scanner
|
||||||
snap_dir = self.bot.bridge.transport.bridge_dir / "chat_snapshots" if hasattr(self.bot.bridge.transport, 'bridge_dir') else None
|
snap_dir = self.bot.bridge.transport.bridge_dir / "chat_snapshots" if hasattr(self.bot.bridge.transport, 'bridge_dir') else None
|
||||||
@@ -172,12 +173,15 @@ class GatewayAPI:
|
|||||||
"content": content,
|
"content": content,
|
||||||
"timestamp": time.time(),
|
"timestamp": time.time(),
|
||||||
}
|
}
|
||||||
|
if attached_files:
|
||||||
|
snap_data["attached_files"] = attached_files
|
||||||
(snap_dir / f"{snap_id}.json").write_text(
|
(snap_dir / f"{snap_id}.json").write_text(
|
||||||
json.dumps(snap_data, ensure_ascii=False, indent=2),
|
json.dumps(snap_data, ensure_ascii=False, indent=2),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"[GATEWAY] chat received: project={project} len={len(content)}")
|
af_info = f" +{len(attached_files)} files" if attached_files else ""
|
||||||
|
logger.info(f"[GATEWAY] chat received: project={project} len={len(content)}{af_info}")
|
||||||
return web.json_response({"ok": True})
|
return web.json_response({"ok": True})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[GATEWAY] chat error: {e}")
|
logger.error(f"[GATEWAY] chat error: {e}")
|
||||||
|
|||||||
20
watcher.py
20
watcher.py
@@ -110,8 +110,14 @@ class BrainEventHandler(FileSystemEventHandler):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def _is_watched_file(self, file_name: str) -> bool:
|
def _is_watched_file(self, file_name: str) -> bool:
|
||||||
"""Strict filter: only watch primary artifact files."""
|
"""Filter: watch primary artifact files + any file matching watched extensions."""
|
||||||
return file_name in Config.WATCHED_FILES
|
if file_name in Config.WATCHED_FILES:
|
||||||
|
return True
|
||||||
|
# Extension-based matching (e.g., any .md file in conversation dir)
|
||||||
|
ext = Path(file_name).suffix
|
||||||
|
if ext and ext in Config.WATCHED_EXTENSIONS:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def _emit(self, event: BrainEvent):
|
def _emit(self, event: BrainEvent):
|
||||||
self.loop.call_soon_threadsafe(self.event_queue.put_nowait, event)
|
self.loop.call_soon_threadsafe(self.event_queue.put_nowait, event)
|
||||||
@@ -142,9 +148,17 @@ class BrainEventHandler(FileSystemEventHandler):
|
|||||||
if not conv_id:
|
if not conv_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Exclude files in .system_generated subdirectory (AG internal logs)
|
||||||
|
try:
|
||||||
|
relative = path.relative_to(Config.BRAIN_PATH / conv_id)
|
||||||
|
if '.system_generated' in relative.parts:
|
||||||
|
return
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
file_name = path.name
|
file_name = path.name
|
||||||
|
|
||||||
# STRICT filter: only primary artifacts
|
# Filter: watched files by name or extension
|
||||||
if not self._is_watched_file(file_name):
|
if not self._is_watched_file(file_name):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user