Compare commits

..

2 Commits

Author SHA1 Message Date
claude-bot
2d4b136c00 docs(readme): document Gitea deployment and operational adaptations 2026-06-01 20:44:09 +09:00
claude-bot
7197099db5 fix(discord): escape prose markdown without clobbering structured output 2026-06-01 20:44:03 +09:00
3 changed files with 29 additions and 4 deletions

View File

@@ -11,6 +11,8 @@ EJClaw는 Discord 위에서 동작하는 Tribunal 멀티에이전트 개발 보
원본은 [qwibitai/nanoclaw](https://github.com/qwibitai/nanoclaw)에서 출발했지만, 현재는 EJClaw의 Discord/paired-runtime 구조에 맞게 독립적으로 유지되고 있습니다.
이 Gitea 저장소는 GitHub `main`을 베이스로 삼고, 운영 환경에서 검증된 로컬 변경(아래 "운영 적용 사항")을 현재 코드 구조에 맞게 재구현해 얹은 배포본입니다.
## 개요
- 단일 `ejclaw` 서비스가 owner / reviewer / arbiter 세 역할과 세 Discord 봇을 함께 관리합니다.
@@ -32,6 +34,16 @@ EJClaw는 Discord 위에서 동작하는 Tribunal 멀티에이전트 개발 보
- `assign_room` 기반 명시적 room assignment
- Bun + SQLite 기반 빠른 런타임
## 운영 적용 사항
GitHub `main` 베이스 위에 운영 중 검증된 변경을 현재 코드 구조에 맞춰 재구현해 반영했습니다.
- Discord 출력 마크다운 보존: 일반 문장의 `**bold**`, `~~strike~~`, 백틱 등 emphasis 마커를 렌더링 대신 원문 그대로 표시(백슬래시 이스케이프). 코드블록과 구조화 출력(EJClaw JSON, 링크→인라인 코드)은 의도대로 보존하고, 이스케이프는 Discord 전송 직전 한 번만 적용됩니다.
- 2000자 분할 시 코드블록 보존: 코드 펜스(triple backtick)를 인식해 청크 경계에서 코드블록이 깨지지 않도록 분할(서러게이트 페어 안전 처리 포함).
- 사용량 윈도우 정렬 프라이머: Codex/Claude 초기화 시점을 맞추기 위한 정렬 신호를 조건 없이 발사.
- 리뷰어 무응답 방지: 리뷰어가 verdict 없이 연속 실패할 때 재시도 상한(기본 2회)으로 핑퐁 루프를 끊고 arbiter/사용자로 에스컬레이션.
- 대시보드: 등록된 방 별칭을 Discord 채널명과 분리해 보존.
## Tribunal 시스템
| 역할 | 현재 기본값 | 설명 |
@@ -123,7 +135,7 @@ Discord ──► SQLite (WAL) ──► GroupQueue ──┬──► Owner (ho
### 설치
```bash
git clone https://github.com/phj1081/EJClaw.git
git clone https://git.tkrmagid.kr/claude-bot/EJClaw.git
cd EJClaw
bun install
bun run build:all

View File

@@ -1,6 +1,7 @@
import path from 'path';
import { normalizeAgentOutput } from '../agent-protocol.js';
import { neutralizeStrayMarkdown, sanitizeForOutbound } from '../router.js';
import type { OutboundAttachment } from '../types.js';
const LOCAL_MARKDOWN_LINK_RE = /\[[^\]\n]*\]\((\/[^)\n]+)\)/g;
@@ -46,7 +47,17 @@ export function prepareDiscordOutbound(
const structuredOutput =
normalized.output?.visibility === 'public' ? normalized.output : null;
const outboundText = structuredOutput?.text ?? normalized.result ?? text;
const cleanText = sanitizeLocalMarkdownLinks(outboundText);
// Redact/strip before escaping so secret patterns (which contain `_`) still
// match, then escape stray Discord markdown in free-form agent prose so
// emphasis markers (**bold**, ~~strike~~, …) render literally. Author-
// controlled structured envelopes are intentional, so their text is left
// untouched. Escaping runs before sanitizeLocalMarkdownLinks so the inline
// code it deliberately produces is preserved.
const safeText = sanitizeForOutbound(outboundText);
const renderedText = structuredOutput
? safeText
: neutralizeStrayMarkdown(safeText);
const cleanText = sanitizeLocalMarkdownLinks(renderedText);
const attachments =
optionAttachments && optionAttachments.length > 0
? optionAttachments

View File

@@ -15,7 +15,7 @@ import { CACHE_DIR } from '../config.js';
import { getEnv } from '../env.js';
import { logger } from '../logger.js';
import { validateOutboundAttachments } from '../outbound-attachments.js';
import { formatOutbound } from '../router.js';
import { sanitizeForOutbound } from '../router.js';
import { hasReviewerLease } from '../service-routing.js';
import type {
DeleteRecentMessagesByContentOptions,
@@ -528,7 +528,9 @@ export class DiscordChannel implements Channel {
for (const [name, id] of Object.entries(mentionMap)) {
cleaned = cleaned.replace(new RegExp(`@${name}`, 'g'), `<@${id}>`);
}
cleaned = formatOutbound(cleaned);
// Markdown escaping already happened in prepareDiscordOutbound (before
// the deliberate link→inline-code step). Here we only strip/redact.
cleaned = sanitizeForOutbound(cleaned);
// Discord has a 2000 character limit per message and 10 attachments per message
const MAX_LENGTH = 2000;