@@ -23,7 +23,7 @@ st.set_page_config(page_title="Cowork", layout="wide")
def init ( ) :
def init ( ) :
agent = GeneraticAgent ( )
agent = GeneraticAgent ( )
if agent . llmclient is None :
if agent . llmclient is None :
st . error ( " ⚠️ 未配置任何可用的 LLM 接口, 请设置mykey.py。 " )
st . error ( " ⚠️ 사용 가능한 LLM 인터페이스가 구성되지 않았습니다. mykey.py를 설정하세요. " )
st . stop ( )
st . stop ( )
else : threading . Thread ( target = agent . run , daemon = True ) . start ( )
else : threading . Thread ( target = agent . run , daemon = True ) . start ( )
return agent
return agent
@@ -37,23 +37,23 @@ if 'autonomous_enabled' not in st.session_state: st.session_state.autonomous_ena
@st.fragment
@st.fragment
def render_sidebar ( ) :
def render_sidebar ( ) :
current_idx = agent . llm_no
current_idx = agent . llm_no
st . caption ( f " LLM Core: { current_idx } : { agent . get_llm_name ( ) } " , help = " 点击切换备用链路 " )
st . caption ( f " LLM Core: { current_idx } : { agent . get_llm_name ( ) } " , help = " 백업 링크 전환하려면 클릭하세요 " )
last_reply_time = st . session_state . get ( ' last_reply_time ' , 0 )
last_reply_time = st . session_state . get ( ' last_reply_time ' , 0 )
if last_reply_time > 0 :
if last_reply_time > 0 :
st . caption ( f " 空闲时间 : { int ( time . time ( ) ) - last_reply_time } 秒 " , help = " 当超过30分钟未收到回复时, 系统会自动任务 " )
st . caption ( f " 대기 시간 : { int ( time . time ( ) ) - last_reply_time } 초 " , help = " 30분 이상 응답이 없으면 시스템이 자동으로 작업을 시작합니다 " )
if st . button ( " 切换备用链路 " ) :
if st . button ( " 백업 링크 전환 " ) :
agent . next_llm ( ) ; st . rerun ( scope = " fragment " )
agent . next_llm ( ) ; st . rerun ( scope = " fragment " )
if st . button ( " 强行停止任务 " ) :
if st . button ( " 작업 강제 중지 " ) :
agent . abort ( ) ; st . toast ( " 已发送停止信号 " ) ; st . rerun ( )
agent . abort ( ) ; st . toast ( " 중지 신호가 전송되었습니다 " ) ; st . rerun ( )
if st . button ( " 重新注入工具 " ) :
if st . button ( " 도구 재주입 " ) :
agent . llmclient . last_tools = ' '
agent . llmclient . last_tools = ' '
try :
try :
hist_path = os . path . join ( script_dir , ' .. ' , ' assets ' , ' tool_usable_history.json ' )
hist_path = os . path . join ( script_dir , ' .. ' , ' assets ' , ' tool_usable_history.json ' )
with open ( hist_path , ' r ' , encoding = ' utf-8 ' ) as f : tool_hist = json . load ( f )
with open ( hist_path , ' r ' , encoding = ' utf-8 ' ) as f : tool_hist = json . load ( f )
agent . llmclient . backend . history . extend ( tool_hist )
agent . llmclient . backend . history . extend ( tool_hist )
st . toast ( f " 已重新注入工具,追加了 { len ( tool_hist ) } 条示范记录 " )
st . toast ( f " 도구가 재주입되었습니다. { len ( tool_hist ) } 개의 예시 기록이 추가되었습니다 " )
except Exception as e : st . toast ( f " 注入工具示范失败 : { e } " )
except Exception as e : st . toast ( f " 도구 예시 주입 실패 : { e } " )
if st . button ( " 🐱 桌面宠物 " ) :
if st . button ( " 🐱 데스크톱 펫 " ) :
kwargs = { ' creationflags ' : 0x08 } if sys . platform == ' win32 ' else { }
kwargs = { ' creationflags ' : 0x08 } if sys . platform == ' win32 ' else { }
pet_script = os . path . join ( script_dir , ' desktop_pet_v2.pyw ' )
pet_script = os . path . join ( script_dir , ' desktop_pet_v2.pyw ' )
if not os . path . exists ( pet_script ) : pet_script = os . path . join ( script_dir , ' desktop_pet.pyw ' )
if not os . path . exists ( pet_script ) : pet_script = os . path . join ( script_dir , ' desktop_pet.pyw ' )
@@ -68,31 +68,31 @@ def render_sidebar():
def _pet_hook ( ctx ) :
def _pet_hook ( ctx ) :
parts = [ f " Turn { ctx . get ( ' turn ' , ' ? ' ) } " ]
parts = [ f " Turn { ctx . get ( ' turn ' , ' ? ' ) } " ]
if ctx . get ( ' summary ' ) : parts . append ( ctx [ ' summary ' ] )
if ctx . get ( ' summary ' ) : parts . append ( ctx [ ' summary ' ] )
if ctx . get ( ' exit_reason ' ) : parts . append ( ' 任务已完成 ' )
if ctx . get ( ' exit_reason ' ) : parts . append ( ' 작업 완료 ' )
_pet_req ( f ' msg= { quote ( chr ( 10 ) . join ( parts ) ) } ' )
_pet_req ( f ' msg= { quote ( chr ( 10 ) . join ( parts ) ) } ' )
if ctx . get ( ' exit_reason ' ) : _pet_req ( ' state=idle ' )
if ctx . get ( ' exit_reason ' ) : _pet_req ( ' state=idle ' )
agent . _turn_end_hooks [ ' pet ' ] = _pet_hook
agent . _turn_end_hooks [ ' pet ' ] = _pet_hook
st . toast ( " 桌面宠物已启动 " )
st . toast ( " 데스크톱 펫이 시작되었습니다 " )
st . divider ( )
st . divider ( )
if st . button ( " 开始空闲自主行动 " ) :
if st . button ( " 대기 중 자율 행동 시작 " ) :
st . session_state . last_reply_time = int ( time . time ( ) ) - 1800
st . session_state . last_reply_time = int ( time . time ( ) ) - 1800
st . toast ( " 已将上次回复时间设为1800秒前 " ) ; st . rerun ( )
st . toast ( " 마지막 응답 시간을 1800초 전으로 설정했습니다 " ) ; st . rerun ( )
if st . session_state . autonomous_enabled :
if st . session_state . autonomous_enabled :
if st . button ( " ⏸️ 禁止自主行动 " ) :
if st . button ( " ⏸️ 자율 행동 금지 " ) :
st . session_state . autonomous_enabled = False
st . session_state . autonomous_enabled = False
st . toast ( " ⏸️ 已禁止自主行动 " ) ; st . rerun ( )
st . toast ( " ⏸️ 자율 행동이 금지되었습니다 " ) ; st . rerun ( )
st . caption ( " 🟢 自主行动运行中, 会在你离开它30分钟后自动进行 " )
st . caption ( " 🟢 자율 행동이 실행 중입니다. 30분 후 자동으로 진행됩니다 " )
else :
else :
if st . button ( " ▶️ 允许自主行动 " , type = " primary " ) :
if st . button ( " ▶️ 자율 행동 허용 " , type = " primary " ) :
st . session_state . autonomous_enabled = True
st . session_state . autonomous_enabled = True
st . toast ( " ✅ 已允许自主行动 " ) ; st . rerun ( )
st . toast ( " ✅ 자율 행동이 허용되었습니다 " ) ; st . rerun ( )
st . caption ( " 🔴 自主行动已停止 " )
st . caption ( " 🔴 자율 행동이 중지되었습니다 " )
with st . sidebar : render_sidebar ( )
with st . sidebar : render_sidebar ( )
def fold_turns ( text ) :
def fold_turns ( text ) :
""" Return list of segments: [ { ' type ' : ' text ' , ' content ' :...}, { ' type ' : ' fold ' , ' title ' :..., ' content ' :...}] """
""" Return list of segments: [ { ' type ' : ' text ' , ' content ' :...}, { ' type ' : ' fold ' , ' title ' :..., ' content ' :...}] """
# 先把4+反引号块替换为占位符, 避免误切子agent嵌套的 LLM Running
# 먼저 4개 이상의 백틱 블록을 플레이스홀더로 교체하여 하위 에이전트 중첩 LLM Running이 잘못 잘리는 것을 방지합니다.
_ph = [ ]
_ph = [ ]
safe = re . sub ( r ' ` { 4,}.*?` { 4,} ' , lambda m : ( _ph . append ( m . group ( 0 ) ) , f ' \x00 PH { len ( _ph ) - 1 } \x00 ' ) [ 1 ] , text , flags = re . DOTALL )
safe = re . sub ( r ' ` { 4,}.*?` { 4,} ' , lambda m : ( _ph . append ( m . group ( 0 ) ) , f ' \x00 PH { len ( _ph ) - 1 } \x00 ' ) [ 1 ] , text , flags = re . DOTALL )
parts = re . split ( r ' ( \ **LLM Running \ (Turn \ d+ \ ) \ . \ . \ . \ * \ **) ' , safe )
parts = re . split ( r ' ( \ **LLM Running \ (Turn \ d+ \ ) \ . \ . \ . \ * \ **) ' , safe )
@@ -118,9 +118,9 @@ def fold_turns(text):
else : segments . append ( { ' type ' : ' text ' , ' content ' : marker + content } )
else : segments . append ( { ' type ' : ' text ' , ' content ' : marker + content } )
return segments
return segments
def render_segments ( segments , suffix = ' ' ) :
def render_segments ( segments , suffix = ' ' ) :
# 整块重画:调用方用 slot.container() 包裹,保证 DOM 路径稳定、跨 rerun 对齐(消除"灰色重影")。
# 전체 다시 그리기: 호출자가 slot.container()로 감싸서 DOM 경로가 안정적이고 rerun 간 정렬이 보장되도록 함 ("회색 잔상" 제거).
# heartbeat 空转时 segments 不变 → Streamlit 后端 diff 无变化 → 前端零闪烁;
# heartbeat 대기 시 segments 변경 없음 → Streamlit 백엔드 diff 변화 없음 → 프론트엔드 깜빡임 제로;
# 但 container/markdown 本身是 API 调用, StopException 仍会被抛出( abort 照常起作用)。
# 단 container/markdown 자체는 API 호출이므로 StopException은 여전히 발생함 (abort는 정상 작동).
for seg in segments :
for seg in segments :
if seg [ ' type ' ] == ' fold ' :
if seg [ ' type ' ] == ' fold ' :
with st . expander ( seg [ ' title ' ] , expanded = False ) : st . markdown ( seg [ ' content ' ] )
with st . expander ( seg [ ' title ' ] , expanded = False ) : st . markdown ( seg [ ' content ' ] )
@@ -145,7 +145,7 @@ def agent_backend_stream(prompt):
if " messages " not in st . session_state : st . session_state . messages = [ ]
if " messages " not in st . session_state : st . session_state . messages = [ ]
for msg in st . session_state . messages :
for msg in st . session_state . messages :
with st . chat_message ( msg [ " role " ] ) :
with st . chat_message ( msg [ " role " ] ) :
# 用 slot=st.empty() + with slot.container(): ... 的外壳, DOM 路径和流式渲染完全一致,跨 rerun 对齐
# slot=st.empty() + with slot.container(): ... 외부 껍질을 사용하여 DOM 경로와 스트리밍 렌더링이 완전히 일치하고 rerun 간 정렬됨
slot = st . empty ( )
slot = st . empty ( )
with slot . container ( ) :
with slot . container ( ) :
if msg [ " role " ] == " assistant " : render_segments ( fold_turns ( msg [ " content " ] ) )
if msg [ " role " ] == " assistant " : render_segments ( fold_turns ( msg [ " content " ] ) )
@@ -221,7 +221,7 @@ if prompt := st.chat_input("any task?"):
while frozen < n_done :
while frozen < n_done :
with live . container ( ) : render_segments ( [ segs [ frozen ] ] )
with live . container ( ) : render_segments ( [ segs [ frozen ] ] )
live = st . empty ( ) ; frozen + = 1
live = st . empty ( ) ; frozen + = 1
with live . container ( ) : render_segments ( [ segs [ - 1 ] ] , suffix = CURSOR ) # live 区域
with live . container ( ) : render_segments ( [ segs [ - 1 ] ] , suffix = CURSOR ) # 라이브 영역
segs = fold_turns ( response )
segs = fold_turns ( response )
for i in range ( frozen , len ( segs ) ) :
for i in range ( frozen , len ( segs ) ) :
with live . container ( ) : render_segments ( [ segs [ i ] ] )
with live . container ( ) : render_segments ( [ segs [ i ] ] )