auto: 일일 백업 2026-10-08 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,8 @@ DEFAULT_PORT = 18793
|
||||
AGENTS = [('main', '클로', '🦞'), ('stock', '레이', ''), ('budget', '골디', '📒')]
|
||||
AGENT_IDS = {a[0] for a in AGENTS}
|
||||
SESSION_TOOL = ROOT / 'workspace' / 'skills' / 'claude-code-session' / 'scripts' / 'session_tool.py'
|
||||
# Claude Code 세션 바로가기 (session_tool 프로필 이름, 표시명, 이모지). workdir 은 프로필에서 읽는다
|
||||
CLAUDE_TARGETS = [('openclaw', '코디', '🛠'), ('develop2025', 'Averno', '🌋')]
|
||||
|
||||
SESSION_TTL = 30 * 86400
|
||||
OTP_TTL = 300
|
||||
@@ -240,14 +242,17 @@ def _cody_url(name: str) -> str | None:
|
||||
return m[-1] if m else None
|
||||
|
||||
|
||||
def _open_cody() -> dict:
|
||||
def _open_cody(profile: str = 'openclaw') -> dict:
|
||||
if profile not in {t[0] for t in CLAUDE_TARGETS}:
|
||||
return {'error': '알 수 없는 대상'}
|
||||
with _cody_lock:
|
||||
st = _session_tool()
|
||||
running = [x for x in st._list_active_sessions() if x['running'] and Path(x['workdir']) == ROOT]
|
||||
workdir = Path(st._load_state()['profiles'][profile]['workdir'])
|
||||
running = [x for x in st._list_active_sessions() if x['running'] and Path(x['workdir']) == workdir]
|
||||
if running:
|
||||
name, reused = running[-1]['session'], True
|
||||
else:
|
||||
r = subprocess.run([sys.executable, str(SESSION_TOOL), 'session', 'open', 'openclaw'],
|
||||
r = subprocess.run([sys.executable, str(SESSION_TOOL), 'session', 'open', profile],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
import re
|
||||
m = re.search(r'세션 시작됨: (\S+)', r.stdout)
|
||||
@@ -472,18 +477,19 @@ textarea{font-size:13px;padding:7px 10px;border-radius:16px}
|
||||
|
||||
def _app_page() -> bytes:
|
||||
agents = json.dumps([{'id': a, 'name': n, 'emoji': e} for a, n, e in AGENTS], ensure_ascii=False)
|
||||
targets = json.dumps(CLAUDE_TARGETS, ensure_ascii=False)
|
||||
body = f'''<header><div class="title" id="title"></div>
|
||||
<button class="menu-btn" id="menuBtn" aria-label="메뉴">⋯</button>
|
||||
<div class="menu" id="menu"><button id="statusBtn">상태 보기</button><button id="resetBtn">새 대화 시작</button>
|
||||
<form method="post" action="/logout" style="margin:0"><button>로그아웃</button></form></div></header>
|
||||
<main id="log"></main>
|
||||
<div class="cody-box" id="codyBox"><div class="cody-card"><b>🛠 코디</b><div class="msg2" id="codyMsg"></div>
|
||||
<div class="cody-box" id="codyBox"><div class="cody-card"><b id="codyTitle"></b><div class="msg2" id="codyMsg"></div>
|
||||
<a id="codyLink" href="#" target="_blank" rel="noopener">Claude 앱에서 열기</a><button id="codyClose">닫기</button></div></div>
|
||||
<div class="typing" id="typing"></div>
|
||||
<footer><div class="agent-pick"><button class="agent-btn" id="agentBtn" aria-label="에이전트 선택"><span id="agentEmoji"></span><span class="dot"></span></button>
|
||||
<div class="agent-list" id="agentList"></div></div><textarea id="input" rows="1" placeholder="메시지"></textarea>
|
||||
<button id="send" aria-label="전송">➤</button></footer>
|
||||
<script>window.AGENTS={agents};</script><script src="/app.js"></script>'''
|
||||
<script>window.AGENTS={agents};window.CT={targets};</script><script src="/app.js"></script>'''
|
||||
return _page('채팅', body, APP_CSS)
|
||||
|
||||
|
||||
@@ -495,7 +501,7 @@ if(vv){const fit=()=>{const m=document.getElementById('log'),stick=m&&m.scrollHe
|
||||
document.body.style.height=vv.height+'px';document.body.style.top=vv.offsetTop+'px';
|
||||
if(stick)m.scrollTop=m.scrollHeight}; // 맨 아래를 보던 중이면 키보드가 올라와도 마지막 메시지를 가리지 않게
|
||||
vv.addEventListener('resize',fit);vv.addEventListener('scroll',fit);fit()}
|
||||
const A=window.AGENTS, $=s=>document.querySelector(s);
|
||||
const CT=window.CT,A=window.AGENTS, $=s=>document.querySelector(s);
|
||||
const log=$('#log'), input=$('#input'), send=$('#send'), typing=$('#typing');
|
||||
let cur=localStorage.getItem('cw_agent')||'main', status={}, lastSeen={}, timer=null;
|
||||
try{lastSeen=JSON.parse(localStorage.getItem('cw_seen')||'{}')}catch(e){}
|
||||
@@ -523,7 +529,7 @@ function renderTabs(){
|
||||
$('#agentEmoji').textContent=ca.emoji;let anyUn=false;
|
||||
$('#agentList').innerHTML=A.map(a=>{const st=status[a.id]||{};const un=a.id!==cur&&st.last&&st.last!==lastSeen[a.id];if(un)anyUn=true;
|
||||
return '<button class="agent-item'+(a.id===cur?' on':'')+(un?' unread':'')+'" data-id="'+a.id+'">'+a.emoji+' '+a.name+'<span class="dot"></span><span class="st">'+(st.pending?'입력 중…':'')+'</span></button>'}).join('')
|
||||
+'<button class="agent-item cody" data-cody="1">🛠 코디<span class="st">Claude 앱</span></button>';
|
||||
+CT.map((t,i)=>'<button class="agent-item'+(i?'':' cody')+'" data-cody="'+t[0]+'" data-label="'+t[2]+' '+t[1]+'">'+t[2]+' '+t[1]+'<span class="st">Claude 앱</span></button>').join('');
|
||||
$('#agentBtn').classList.toggle('unread',anyUn);
|
||||
}
|
||||
function renderLog(items){
|
||||
@@ -554,7 +560,7 @@ function doSend(choice){
|
||||
function autosize(){input.style.height='auto';input.style.height=Math.min(input.scrollHeight,window.innerHeight*0.4)+'px'}
|
||||
$('#agentBtn').addEventListener('click',e=>{e.stopPropagation();$('#menu').classList.remove('open');$('#agentList').classList.toggle('open')});
|
||||
$('#agentList').addEventListener('click',e=>{const b=e.target.closest('.agent-item');if(!b)return;e.stopPropagation();$('#agentList').classList.remove('open');
|
||||
if(b.dataset.cody){openCody();return}
|
||||
if(b.dataset.cody){openCody(b.dataset.cody,b.dataset.label);return}
|
||||
if(b.dataset.id!==cur){cur=b.dataset.id;localStorage.setItem('cw_agent',cur);load()}});
|
||||
send.addEventListener('click',()=>doSend());
|
||||
log.addEventListener('click',e=>{const b=e.target.closest('.choice');if(b&&!b.disabled)doSend(b.dataset.choice)});
|
||||
@@ -564,9 +570,9 @@ $('#menuBtn').addEventListener('click',e=>{e.stopPropagation();$('#menu').classL
|
||||
document.addEventListener('click',()=>{$('#menu').classList.remove('open');$('#agentList').classList.remove('open')});
|
||||
$('#statusBtn').addEventListener('click',()=>doSend('/status'));
|
||||
// 코디: 세션을 띄우고(떠 있으면 재사용) 주소를 받아 버튼으로 준다 — iOS 는 사용자가 직접 누른 링크여야 앱으로 넘긴다
|
||||
function openCody(){const box=$('#codyBox'),msg=$('#codyMsg'),a=$('#codyLink');
|
||||
function openCody(profile,label){const box=$('#codyBox'),msg=$('#codyMsg'),a=$('#codyLink');$('#codyTitle').textContent=label;
|
||||
msg.textContent='세션 준비 중… (처음 띄우면 10초쯤 걸립니다)';a.style.display='none';box.classList.add('open');
|
||||
api('/api/cody',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'}).then(d=>{
|
||||
api('/api/cody',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({profile})}).then(d=>{
|
||||
if(d.error){msg.textContent=d.error;return}
|
||||
msg.textContent=(d.reused?'떠 있는 세션':'새 세션')+' '+d.session+(d.url.includes('/session_')?' — 진행 중인 대화로 엽니다':' — 이 환경에서 새 대화를 시작합니다');
|
||||
a.href=d.url;a.style.display='block'}).catch(()=>{msg.textContent='요청 실패'})}
|
||||
@@ -736,7 +742,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
except Exception:
|
||||
return self._json({'error': 'bad json'}, 400)
|
||||
if path == '/api/cody':
|
||||
return self._json(_open_cody())
|
||||
return self._json(_open_cody(data.get('profile') or 'openclaw'))
|
||||
ag = data.get('agent')
|
||||
if ag not in AGENT_IDS:
|
||||
return self._json({'error': 'agent'}, 400)
|
||||
|
||||
Reference in New Issue
Block a user