项目文件夹

文件
2026-07-13 13:04:19 +08:00

1050 行
37 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
import os, sys
import html
if sys.stdout is None: sys.stdout = open(os.devnull, "w")
if sys.stderr is None: sys.stderr = open(os.devnull, "w")
try: sys.stdout.reconfigure(errors='replace')
except: pass
try: sys.stderr.reconfigure(errors='replace')
except: pass
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
import streamlit as st
try:
from streamlit import iframe as _st_iframe # 1.56+
_embed_html = lambda html, **kw: _st_iframe(html, **{k: max(v, 1) if isinstance(v, int) else v for k, v in kw.items()})
except (ImportError, AttributeError):
from streamlit.components.v1 import html as _embed_html # ≤1.55
import time, json, re, threading, queue
from datetime import datetime
from agentmain import GeneraticAgent
st.set_page_config(page_title="Cowork", layout="wide")
# ─── Anthropic Light Theme CSS ───
ANTHROPIC_CSS = """
<style>
/* ===== Root variables ===== */
:root {
--anthropic-primary: #D4A27F;
--anthropic-primary-hover: #C4895F;
--anthropic-bg: #FAF9F6;
--anthropic-bg-secondary: #EEECE2;
--anthropic-code-bg: #F4F1EB;
--anthropic-text: #1A1714;
--anthropic-text-secondary: #6B6560;
--anthropic-border: #D5CEC5;
--anthropic-sidebar-bg: #F0EDE4;
--anthropic-accent: #CC785C;
--anthropic-success: #5A8A5E;
--anthropic-warning: #C4885A;
--anthropic-error: #C45A5A;
--anthropic-info: #5A7A8A;
--anthropic-font: 'Source Sans Pro', sans-serif;
--anthropic-mono: 'Source Code Pro', monospace;
}
/* ===== Global ===== */
body, [data-testid="stAppViewContainer"] {
background-color: var(--anthropic-bg) !important;
color: var(--anthropic-text) !important;
}
.stApp {
background-color: var(--anthropic-bg) !important;
}
/* ===== Header / Top bar ===== */
[data-testid="stHeader"], header[data-testid="stHeader"] {
background-color: var(--anthropic-bg) !important;
border-bottom: 1px solid var(--anthropic-border) !important;
}
/* Hide default Streamlit toolbar buttons (deploy, hamburger, etc.) */
[data-testid="stToolbar"] {
visibility: hidden !important;
}
[data-testid="stDecoration"],
#MainMenu {
display: none !important;
visibility: hidden !important;
}
/* Restore sidebar expand button (lives inside stToolbar) */
[data-testid="stExpandSidebarButton"],
[data-testid="stExpandSidebarButton"] * {
visibility: visible !important;
}
/* Only restore ancestor divs that contain the sidebar button */
[data-testid="stToolbar"] div:has([data-testid="stExpandSidebarButton"]) {
visibility: visible !important;
}
/* Make top-left settings/sidebar toggle darker and easier to see */
button[data-testid="stExpandSidebarButton"] {
visibility: visible !important;
background: #F4F1EA !important;
background-color: #F4F1EA !important;
border: none !important;
color: #3B2F2A !important;
border-radius: 10px !important;
box-shadow: none !important;
}
button[data-testid="stExpandSidebarButton"]:hover {
background: #EAE4D9 !important;
background-color: #EAE4D9 !important;
border-color: transparent !important;
}
button[data-testid="stExpandSidebarButton"],
button[data-testid="stExpandSidebarButton"] *,
button[data-testid="stExpandSidebarButton"] [data-testid="stIconMaterial"] {
color: #3B2F2A !important;
fill: #3B2F2A !important;
stroke: #3B2F2A !important;
}
/* Hide other toolbar buttons (deploy, etc.) */
button[kind="header"] {
visibility: hidden !important;
}
/* ===== Sidebar ===== */
[data-testid="stSidebar"], section[data-testid="stSidebar"] {
background-color: var(--anthropic-sidebar-bg) !important;
border-right: 1px solid var(--anthropic-border) !important;
}
[data-testid="stSidebar"] .stMarkdown,
[data-testid="stSidebar"] p,
[data-testid="stSidebar"] span,
[data-testid="stSidebar"] label {
color: var(--anthropic-text) !important;
}
[data-testid="stSidebar"] hr {
border-color: var(--anthropic-border) !important;
}
/* ===== Sidebar Selectbox ===== */
[data-testid="stSidebar"] [data-testid="stSelectbox"] {
width: fit-content !important;
max-width: 100% !important;
}
[data-testid="stSidebar"] [data-testid="stSelectbox"] > div {
width: fit-content !important;
max-width: 100% !important;
}
[data-testid="stSidebar"] [data-testid="stSelectbox"] label,
[data-testid="stSidebar"] .stSelectbox label {
color: var(--anthropic-text-secondary) !important;
font-size: 0.9rem !important;
font-weight: 500 !important;
}
[data-testid="stSidebar"] [data-baseweb="select"] {
width: fit-content !important;
max-width: 100% !important;
display: inline-block !important;
}
[data-testid="stSidebar"] [data-baseweb="select"] > div {
width: fit-content !important;
max-width: 100% !important;
background: #F7F3EC !important;
border: none !important;
box-shadow: none !important;
border-radius: 12px !important;
min-height: 42px !important;
padding-right: 1.6rem !important;
position: relative !important;
}
[data-testid="stSidebar"] [data-baseweb="select"] > div:hover,
[data-testid="stSidebar"] [data-baseweb="select"] > div:focus-within {
background: #EFE9DE !important;
border: none !important;
box-shadow: none !important;
}
[data-testid="stSidebar"] [data-baseweb="select"] input,
[data-testid="stSidebar"] [data-baseweb="select"] span,
[data-testid="stSidebar"] [data-baseweb="select"] div {
color: var(--anthropic-text) !important;
}
[data-testid="stSidebar"] [data-baseweb="select"] span {
white-space: nowrap !important;
}
[data-baseweb="popover"],
[data-baseweb="menu"],
[data-baseweb="popover"] > div,
[data-baseweb="popover"] [role="presentation"],
[data-baseweb="popover"] ul,
[data-baseweb="popover"] li,
[data-baseweb="popover"] [role="listbox"],
[data-baseweb="popover"] [role="option"] {
background: #F7F3EC !important;
color: var(--anthropic-text) !important;
}
[role="listbox"] {
background: #F7F3EC !important;
border: 1px solid var(--anthropic-border) !important;
border-radius: 14px !important;
box-shadow: 0 10px 30px rgba(58, 47, 42, 0.12) !important;
padding: 0.35rem !important;
color: var(--anthropic-text) !important;
}
[role="option"] {
color: var(--anthropic-text) !important;
background: transparent !important;
border-radius: 10px !important;
}
[role="option"]:hover,
[role="option"][aria-selected="true"] {
background: #EFE9DE !important;
color: var(--anthropic-text) !important;
}
/* ===== Title ===== */
h1, .stTitle, [data-testid="stHeading"] h1 {
color: var(--anthropic-text) !important;
font-weight: 600 !important;
letter-spacing: -0.02em !important;
}
/* ===== Agent name input fixed in header bar ===== */
[data-testid="stTextInput"] {
position: fixed !important;
top: 0 !important;
left: 50% !important;
transform: translateX(-50%) !important;
z-index: 999999 !important;
height: 60px !important;
display: flex !important;
align-items: center !important;
margin: 0 !important;
padding: 0 !important;
}
/* Hide the empty container left behind */
[data-testid="stMainBlockContainer"] > [data-testid="stVerticalBlock"] > [data-testid="stElementContainer"]:first-child {
height: 0 !important;
overflow: hidden !important;
margin: 0 !important;
padding: 0 !important;
}
[data-testid="stTextInput"] > div {
background-color: transparent !important;
border: none !important;
box-shadow: none !important;
padding: 0 !important;
margin: 0 !important;
position: relative !important;
}
[data-testid="stTextInput"] > label {
display: none !important;
}
[data-testid="stTextInput"] input[type="text"] {
font-size: 1.6rem !important;
font-weight: 600 !important;
letter-spacing: -0.02em !important;
color: var(--anthropic-text) !important;
background-color: var(--anthropic-bg) !important;
border: none !important;
border-radius: 0 !important;
padding: 0.3rem 1.8rem 0.3rem 0.5rem !important;
box-shadow: none !important;
width: 320px !important;
text-align: center !important;
transition: all 0.2s ease !important;
cursor: default !important;
caret-color: #1a1714 !important;
}
[data-testid="stTextInput"] input[type="text"]:hover {
background-color: var(--anthropic-bg-secondary) !important;
border-radius: 6px !important;
}
[data-testid="stTextInput"] input[type="text"]:focus {
background-color: var(--anthropic-bg-secondary) !important;
border-radius: 6px !important;
box-shadow: none !important;
cursor: text !important;
caret-color: #1a1714 !important;
}
/* Edit pencil icon - visible by default, semi-transparent on focus */
[data-testid="stTextInput"] > div::after {
content: '✎' !important;
position: absolute !important;
right: 8px !important;
top: 50% !important;
transform: translateY(-50%) !important;
font-size: 0.9rem !important;
color: var(--anthropic-text-secondary) !important;
pointer-events: none !important;
opacity: 0.6 !important;
transition: opacity 0.2s ease !important;
}
[data-testid="stTextInput"] > div:hover::after {
opacity: 0.85 !important;
}
[data-testid="stTextInput"] > div:focus-within::after {
opacity: 0 !important;
}
h2, h3, h4, h5, h6 {
color: var(--anthropic-text) !important;
font-weight: 500 !important;
}
/* ===== Buttons ===== */
.stButton > button {
background-color: var(--anthropic-bg-secondary) !important;
color: var(--anthropic-text) !important;
border: 1px solid var(--anthropic-border) !important;
border-radius: 8px !important;
padding: 0.4rem 1rem !important;
font-weight: 500 !important;
transition: all 0.2s ease !important;
}
.stButton > button:hover {
background-color: var(--anthropic-primary) !important;
color: white !important;
border-color: var(--anthropic-primary) !important;
}
.stButton > button[kind="primary"],
.stButton > button[data-testid="stBaseButton-primary"] {
background-color: var(--anthropic-primary) !important;
color: white !important;
border-color: var(--anthropic-primary) !important;
}
.stButton > button[kind="primary"]:hover,
.stButton > button[data-testid="stBaseButton-primary"]:hover {
background-color: var(--anthropic-primary-hover) !important;
border-color: var(--anthropic-primary-hover) !important;
}
/* ===== Chat input ===== */
[data-testid="stChatInput"],
[data-testid="stChatInput"] > div {
background-color: var(--anthropic-bg) !important;
border-color: var(--anthropic-border) !important;
}
[data-testid="stChatInput"] {
margin-bottom: 12px !important;
}
[data-testid="stChatInput"] textarea,
[data-testid="stChatInputTextArea"] {
color: var(--anthropic-text) !important;
background-color: var(--anthropic-bg) !important;
caret-color: #1A1714 !important;
}
[data-testid="stChatInput"] textarea::placeholder {
color: var(--anthropic-text-secondary) !important;
opacity: 0.7 !important;
}
/* Chat input container border */
[data-testid="stChatInput"] > div {
border: 1px solid var(--anthropic-border) !important;
border-radius: 12px !important;
min-height: 60px !important;
padding: 0.35rem 0.45rem 0.35rem 0.8rem !important;
align-items: center !important;
gap: 0.5rem !important;
transition: none !important;
animation: none !important;
}
[data-testid="stChatInput"] > div:focus-within {
border-color: var(--anthropic-primary) !important;
box-shadow: 0 0 0 2px rgba(212, 162, 127, 0.2) !important;
}
[data-testid="stChatInput"] textarea,
[data-testid="stChatInputTextArea"] {
min-height: 1.5rem !important;
padding: 0.35rem 0 !important;
line-height: 1.5 !important;
transition: none !important;
animation: none !important;
}
/* Chat send button */
[data-testid="stChatInput"] button,
[data-testid="stChatInputSubmitButton"] {
background-color: var(--anthropic-primary) !important;
color: white !important;
border-radius: 12px !important;
width: 60px !important;
height: 60px !important;
min-width: 60px !important;
min-height: 60px !important;
padding: 0 !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
flex-shrink: 0 !important;
transition: none !important;
animation: none !important;
}
[data-testid="stChatInput"] button svg,
[data-testid="stChatInputSubmitButton"] svg,
[data-testid="stChatInput"] button [data-testid="stIconMaterial"],
[data-testid="stChatInputSubmitButton"] [data-testid="stIconMaterial"] {
width: 1.25rem !important;
height: 1.25rem !important;
font-size: 1.25rem !important;
}
[data-testid="stChatInput"] button:hover {
background-color: var(--anthropic-primary-hover) !important;
}
/* Stop streaming button - fixed at bottom center, above chat input */
.stop-btn-anchor {
display: none !important;
}
/* Collapse the wrapper so it doesn't push chat bubbles */
[data-testid="stElementContainer"]:has(.stop-btn-anchor) {
height: 0 !important;
min-height: 0 !important;
margin: 0 !important;
padding: 0 !important;
overflow: visible !important;
}
[data-testid="stVerticalBlock"]:has(.stop-btn-anchor):not(:has([data-testid="stChatMessage"])) {
position: fixed !important;
bottom: 5.75rem !important;
left: 50% !important;
transform: translateX(-50%) !important;
z-index: 1000 !important;
width: auto !important;
background: transparent !important;
pointer-events: none !important;
gap: 0 !important;
}
[data-testid="stVerticalBlock"]:has(.stop-btn-anchor):not(:has([data-testid="stChatMessage"])) > * {
pointer-events: auto !important;
}
[data-testid="stVerticalBlock"]:has(.stop-btn-anchor):not(:has([data-testid="stChatMessage"])) [data-testid="stButton"] {
margin: 0 !important;
}
[data-testid="stVerticalBlock"]:has(.stop-btn-anchor):not(:has([data-testid="stChatMessage"])) [data-testid="stButton"] > button {
border-radius: 999px !important;
padding: 0.35rem 1.1rem !important;
min-height: 2rem !important;
font-size: 0.84rem !important;
font-weight: 500 !important;
line-height: 1 !important;
box-shadow: 0 2px 8px rgba(0,0,0,0.12) !important;
white-space: nowrap !important;
}
[data-testid="stVerticalBlock"]:has(.stop-btn-anchor):not(:has([data-testid="stChatMessage"])) [data-testid="stButton"] > button[kind="primary"],
[data-testid="stVerticalBlock"]:has(.stop-btn-anchor):not(:has([data-testid="stChatMessage"])) [data-testid="stButton"] > button[data-testid="stBaseButton-primary"] {
background-color: rgba(212, 162, 127, 0.95) !important;
border-color: rgba(212, 162, 127, 0.95) !important;
}
[data-testid="stVerticalBlock"]:has(.stop-btn-anchor):not(:has([data-testid="stChatMessage"])) [data-testid="stButton"] > button:hover {
transform: translateY(-1px) !important;
box-shadow: 0 3px 12px rgba(0,0,0,0.15) !important;
}
/* ===== Chat messages ===== */
[data-testid="stChatMessage"] {
background-color: var(--anthropic-bg) !important;
border: none !important;
border-radius: 12px !important;
padding: 1rem 1.2rem !important;
margin-bottom: 0.5rem !important;
}
/* Assistant messages - clean white like Anthropic */
[data-testid="stChatMessage"]:has([data-testid="stChatMessageAvatarAssistant"]) {
background-color: var(--anthropic-bg) !important;
}
/* User messages - subtle bordered box */
[data-testid="stChatMessage"]:has([data-testid="stChatMessageAvatarUser"]) {
background-color: var(--anthropic-bg) !important;
border: 1px solid var(--anthropic-border) !important;
border-radius: 12px !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04) !important;
}
/* Chat message text */
[data-testid="stChatMessage"] p,
[data-testid="stChatMessage"] .stMarkdown {
color: var(--anthropic-text) !important;
line-height: 1.6 !important;
}
/* Message timestamp */
.msg-timestamp {
text-align: left;
font-size: 0.73rem;
color: var(--anthropic-text-secondary);
margin-top: -0.3rem;
margin-bottom: 0.2rem;
opacity: 0.55;
font-family: var(--anthropic-mono);
letter-spacing: 0.02em;
}
/* ===== Chat avatars ===== */
[data-testid="stChatMessageAvatarContainer"] {
width: 36px !important;
height: 36px !important;
}
[data-testid="stChatMessageAvatarContainer"] > div,
[data-testid*="stChatMessageAvatar"],
[data-testid*="chatAvatar"] {
width: 36px !important;
height: 36px !important;
border-radius: 50% !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
overflow: hidden !important;
}
/* User avatar - warm brown gradient */
[data-testid*="stChatMessageAvatar"]:has(svg),
[data-testid*="chatAvatar"][data-testid*="user"],
[data-testid*="stChatMessageAvatar"][data-testid*="User"],
[data-testid*="stChatMessageAvatar"][data-testid*="user"] {
background: linear-gradient(145deg, #D8B08A 0%, #B98259 100%) !important;
border: 1px solid rgba(150, 102, 67, 0.22) !important;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.34), 0 2px 6px rgba(104, 76, 54, 0.10) !important;
}
/* Assistant avatar - cream gradient */
[data-testid*="chatAvatar"][data-testid*="assistant"],
[data-testid*="stChatMessageAvatar"][data-testid*="Assistant"],
[data-testid*="stChatMessageAvatar"][data-testid*="assistant"],
[data-testid="stChatMessageAvatarContainer"] > div {
background: linear-gradient(145deg, #F6F1E9 0%, #E5D7C7 100%) !important;
border: 1px solid rgba(187, 165, 141, 0.50) !important;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.72), 0 2px 6px rgba(104, 76, 54, 0.08) !important;
}
/* ===== Inline code (not inside pre/code blocks) ===== */
:not(pre) > code {
background-color: var(--anthropic-code-bg) !important;
border: 1px solid var(--anthropic-border) !important;
border-radius: 4px !important;
padding: 0.15em 0.4em !important;
font-size: 0.9em !important;
color: var(--anthropic-text) !important;
}
/* ===== Code blocks (pre) ===== */
pre, .stCodeBlock, .stCodeBlock pre {
background-color: var(--anthropic-code-bg) !important;
border: 1px solid var(--anthropic-border) !important;
border-radius: 8px !important;
}
/* Code inside pre blocks: no extra border/background */
pre code,
.stCodeBlock code,
[data-testid="stChatMessage"] pre code,
[data-testid="stChatMessage"] .stCodeBlock code {
background-color: transparent !important;
border: none !important;
padding: 0 !important;
font-size: inherit !important;
color: var(--anthropic-text) !important;
}
/* ===== Toast / Alerts ===== */
[data-testid="stToast"] {
background-color: var(--anthropic-bg-secondary) !important;
border: 1px solid var(--anthropic-border) !important;
border-radius: 8px !important;
color: var(--anthropic-text) !important;
}
/* ===== Captions ===== */
.stCaption, [data-testid="stCaptionContainer"] {
color: var(--anthropic-text-secondary) !important;
}
/* ===== Divider ===== */
[data-testid="stHorizontalBlock"] hr,
hr {
border-color: var(--anthropic-border) !important;
}
/* ===== Scrollbar ===== */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: var(--anthropic-bg);
}
::-webkit-scrollbar-thumb {
background: var(--anthropic-border);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--anthropic-text-secondary);
}
/* ===== Links ===== */
a {
color: var(--anthropic-accent) !important;
}
a:hover {
color: var(--anthropic-primary-hover) !important;
}
/* ===== Error/Warning/Info/Success ===== */
[data-testid="stAlert"] {
border-radius: 8px !important;
}
/* ===== Bottom padding for chat ===== */
[data-testid="stBottomBlockContainer"] {
background-color: var(--anthropic-bg) !important;
}
/* ===== Gear icon to open sidebar ===== */
#sidebar-gear-toggle {
position: fixed !important;
top: 12px !important;
left: 12px !important;
z-index: 999999 !important;
width: 36px !important;
height: 36px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
font-size: 1.3rem !important;
color: var(--anthropic-text-secondary) !important;
background: var(--anthropic-bg-secondary) !important;
border: 1px solid var(--anthropic-border) !important;
border-radius: 8px !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
opacity: 0.7 !important;
user-select: none !important;
}
#sidebar-gear-toggle:hover {
opacity: 1 !important;
color: var(--anthropic-primary) !important;
border-color: var(--anthropic-primary) !important;
transform: rotate(30deg) !important;
}
/* Hide gear when sidebar is open */
body:has([data-testid="stSidebar"][aria-expanded="true"]) #sidebar-gear-toggle {
display: none !important;
}
</style>
"""
ANTHROPIC_SELECTBOX_SCRIPT = """
<div></div>
<script>
(function() {
const hostWin = window.parent;
const doc = hostWin.document;
const LABEL_TEXT = '备用链路';
const EXTRA_WIDTH = 56;
const TIMER_KEY = '__anthropicSelectboxFixedWidthTimer';
const FONT_LABELS = {
'100': '标准(100%',
'112.5': '偏大(112.5%',
'125': '更大(125%',
'137.5': '超大(137.5%'
};
function measureTextWidth(text, sourceEl) {
const canvas = hostWin.__anthropicSelectboxMeasureCanvas || (hostWin.__anthropicSelectboxMeasureCanvas = doc.createElement('canvas'));
const ctx = canvas.getContext('2d');
const style = sourceEl ? hostWin.getComputedStyle(sourceEl) : null;
const font = style ? `${style.fontWeight} ${style.fontSize} ${style.fontFamily}` : '400 14px sans-serif';
ctx.font = font;
return Math.ceil(ctx.measureText(text || '').width);
}
function ensureSidebarSettingsTitle() {
const sidebar = doc.querySelector('[data-testid="stSidebar"]');
if (!sidebar) return;
const collapseBtn = sidebar.querySelector('button[kind="header"], [data-testid="stSidebarCollapseButton"] button, [data-testid="stSidebarCollapseButton"]');
if (!collapseBtn || !collapseBtn.parentElement) return;
let title = doc.getElementById('custom-sidebar-settings-title');
if (!title) {
title = doc.createElement('span');
title.id = 'custom-sidebar-settings-title';
title.textContent = '设置';
title.style.cssText = 'font-size:14px;font-weight:600;color:rgb(38,39,48);margin-right:8px;line-height:1;display:inline-flex;align-items:center;white-space:nowrap;';
}
if (collapseBtn.previousElementSibling !== title) {
collapseBtn.parentElement.insertBefore(title, collapseBtn);
}
}
function applyLiveFontPreview() {
const sidebar = doc.querySelector('[data-testid="stSidebar"]');
if (!sidebar) return;
const sliderLabel = Array.from(sidebar.querySelectorAll('label, p')).find((el) => el.textContent && el.textContent.trim() === '字体大小');
if (!sliderLabel) return;
const container = sliderLabel.closest('[data-testid="stWidgetLabel"]')?.parentElement?.parentElement || sliderLabel.closest('[data-testid="stSlider"]') || sliderLabel.closest('div');
if (!container) return;
const input = container.querySelector('input[type="range"]');
if (!input) return;
const caption = container.querySelector('[data-testid="stCaptionContainer"] p, p');
const updateFont = () => {
const raw = parseFloat(input.value);
if (!Number.isFinite(raw)) return;
doc.documentElement.style.setProperty('font-size', raw + '%', 'important');
if (caption) {
const key = String(raw % 1 === 0 ? raw.toFixed(0) : raw);
caption.textContent = FONT_LABELS[key] || `${raw.toFixed(1)}%`;
}
};
if (input.dataset.liveFontBound !== '1') {
input.addEventListener('input', updateFont);
input.addEventListener('change', updateFont);
input.dataset.liveFontBound = '1';
}
updateFont();
}
function applyFixedWidth() {
const sidebar = doc.querySelector('[data-testid="stSidebar"]');
if (!sidebar) return;
const boxes = sidebar.querySelectorAll('[data-testid="stSelectbox"]');
boxes.forEach((box) => {
const labelNode = box.querySelector('label [data-testid="stMarkdownContainer"] p, label p');
if (!labelNode || labelNode.textContent.trim() !== LABEL_TEXT) return;
const selectRoot = box.querySelector('[data-baseweb="select"]');
const trigger = selectRoot && selectRoot.firstElementChild;
const maxLabelNode = box.querySelector('[data-testid="sidebar-llm-max-label"]');
const text = ((maxLabelNode && maxLabelNode.textContent) || '').trim();
if (!selectRoot || !trigger || !text) return;
const textWidth = measureTextWidth(text, trigger);
const targetWidth = Math.min(sidebar.clientWidth - 32, Math.max(96, textWidth + EXTRA_WIDTH));
const valueWrap = trigger.firstElementChild;
const arrowWrap = valueWrap && valueWrap.nextElementSibling;
const valueNode = valueWrap && valueWrap.querySelector('[value]');
box.style.setProperty('width', targetWidth + 'px', 'important');
box.style.setProperty('max-width', targetWidth + 'px', 'important');
box.style.setProperty('flex', '0 0 ' + targetWidth + 'px', 'important');
selectRoot.style.setProperty('width', targetWidth + 'px', 'important');
selectRoot.style.setProperty('min-width', targetWidth + 'px', 'important');
selectRoot.style.setProperty('max-width', targetWidth + 'px', 'important');
trigger.style.setProperty('width', targetWidth + 'px', 'important');
trigger.style.setProperty('min-width', targetWidth + 'px', 'important');
trigger.style.setProperty('max-width', targetWidth + 'px', 'important');
trigger.style.setProperty('padding-right', '0px', 'important');
trigger.style.setProperty('justify-content', 'flex-start', 'important');
trigger.style.setProperty('box-sizing', 'border-box', 'important');
if (valueWrap) {
valueWrap.style.setProperty('flex', '1 1 auto', 'important');
valueWrap.style.setProperty('min-width', '0px', 'important');
valueWrap.style.setProperty('max-width', 'calc(100% - 24px)', 'important');
valueWrap.style.setProperty('padding-right', '4px', 'important');
}
if (valueNode) {
valueNode.style.setProperty('max-width', '100%', 'important');
}
if (arrowWrap) {
arrowWrap.style.setProperty('margin-left', 'auto', 'important');
arrowWrap.style.setProperty('padding-right', '0px', 'important');
arrowWrap.style.setProperty('width', '24px', 'important');
arrowWrap.style.setProperty('min-width', '24px', 'important');
arrowWrap.style.setProperty('display', 'flex', 'important');
arrowWrap.style.setProperty('justify-content', 'flex-end', 'important');
arrowWrap.style.setProperty('align-items', 'center', 'important');
arrowWrap.style.setProperty('overflow', 'visible', 'important');
}
});
ensureSidebarSettingsTitle();
applyLiveFontPreview();
}
if (hostWin[TIMER_KEY]) {
hostWin.clearInterval(hostWin[TIMER_KEY]);
}
hostWin[TIMER_KEY] = hostWin.setInterval(applyFixedWidth, 300);
hostWin.setTimeout(applyFixedWidth, 60);
hostWin.setTimeout(applyFixedWidth, 300);
hostWin.setTimeout(applyFixedWidth, 1000);
applyFixedWidth();
})();
</script>
"""
@st.cache_resource
def init():
agent = GeneraticAgent()
if agent.llmclient is None:
st.error("⚠️ 未配置任何可用的 LLM 接口,请在 mykey.py 中添加 sider_cookie 或 oai_apikey+oai_apibase 等信息后重启。")
st.stop()
else:
threading.Thread(target=agent.run, daemon=True).start()
return agent
def build_dynamic_font_css(scale_percent: float) -> str:
root_percent = max(100.0, min(200.0, float(scale_percent)))
rem_scale = root_percent / 100.0
return f"""
<style id="dynamic-font-scale-style">
:root, html, body, [data-testid="stAppViewContainer"], .stApp {{
font-size: {root_percent:.1f}% !important;
}}
body, [data-testid="stAppViewContainer"], .stApp {{
--app-font-scale: {rem_scale:.3f};
}}
[data-testid="stAppViewContainer"], .stApp, .stApp p, .stApp li, .stApp label,
.stApp div[data-testid="stMarkdownContainer"], .stApp textarea, .stApp input,
.stApp button, .stApp [data-testid="stChatMessageContent"], .stApp .stCaption {{
font-size: calc(1rem * var(--app-font-scale, 1)) !important;
}}
</style>
"""
def build_dynamic_font_update_script(scale_percent: float) -> str:
css = json.dumps(build_dynamic_font_css(scale_percent))
return f"""
<script>
(() => {{
const cssText = {css};
const parser = new DOMParser();
const parsed = parser.parseFromString(cssText, 'text/html');
const nextStyle = parsed.querySelector('#dynamic-font-scale-style');
if (!nextStyle) return;
const hostDoc = window.parent && window.parent.document ? window.parent.document : document;
const existing = hostDoc.querySelector('#dynamic-font-scale-style');
if (existing) {{
existing.textContent = nextStyle.textContent;
}} else {{
hostDoc.head.appendChild(nextStyle);
}}
}})();
</script>
"""
def build_header_agent_badge_script() -> str:
return """
<script>
(() => {
const hostWin = window.parent || window;
const hostDoc = hostWin.document || document;
const BADGE_ID = 'generic-agent-header-badge';
const STYLE_ID = 'generic-agent-header-badge-style';
const ensureStyle = () => {
if (hostDoc.getElementById(STYLE_ID)) return;
const style = hostDoc.createElement('style');
style.id = STYLE_ID;
style.textContent = `
#${BADGE_ID} {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
display: inline-flex;
align-items: center;
justify-content: center;
white-space: nowrap;
font-size: 2.75rem;
font-weight: 600;
line-height: 1.2;
color: #000000;
padding: 0;
border-radius: 0;
background: transparent;
border: none;
box-shadow: none;
pointer-events: none;
z-index: 20;
}
`;
hostDoc.head.appendChild(style);
};
const findHeaderRoot = () => {
const candidates = [
'header[data-testid="stHeader"]',
'[data-testid="stHeader"]',
'header',
];
for (const selector of candidates) {
const root = hostDoc.querySelector(selector);
if (root) return root;
}
return null;
};
const ensureBadge = () => {
ensureStyle();
const headerRoot = findHeaderRoot();
if (!headerRoot) return;
headerRoot.style.position = 'relative';
let badge = hostDoc.getElementById(BADGE_ID);
if (!badge) {
badge = hostDoc.createElement('div');
badge.id = BADGE_ID;
badge.textContent = 'Generic Agent';
}
if (badge.parentElement !== headerRoot) {
headerRoot.appendChild(badge);
}
const titleEl = hostDoc.querySelector('h1');
if (titleEl) {
const titleStyle = hostWin.getComputedStyle(titleEl);
badge.style.fontSize = titleStyle.fontSize;
badge.style.fontWeight = titleStyle.fontWeight;
badge.style.lineHeight = titleStyle.lineHeight;
badge.style.fontFamily = titleStyle.fontFamily;
badge.style.letterSpacing = titleStyle.letterSpacing;
badge.style.color = '#000000';
}
};
if (hostWin.__genericAgentHeaderBadgeTimer) {
hostWin.clearInterval(hostWin.__genericAgentHeaderBadgeTimer);
}
hostWin.__genericAgentHeaderBadgeTimer = hostWin.setInterval(ensureBadge, 500);
hostWin.setTimeout(ensureBadge, 80);
hostWin.setTimeout(ensureBadge, 400);
ensureBadge();
})();
</script>
"""
agent = init()
def init_session_state():
for key, value in {
'agent_name': 'GenericAgent', 'streaming': False, 'stopping': False, 'display_queue': None,
'partial_response': '', 'reply_ts': '', 'current_prompt': '', 'selected_llm_idx': agent.llm_no,
'autonomous_enabled': False, 'messages': [],
}.items(): st.session_state.setdefault(key, value)
init_session_state()
# Inject Anthropic theme
st.markdown(ANTHROPIC_CSS, unsafe_allow_html=True)
st.markdown(build_dynamic_font_css(110.0), unsafe_allow_html=True)
_embed_html(ANTHROPIC_SELECTBOX_SCRIPT, height=0, width=0)
_embed_html(build_header_agent_badge_script(), height=0, width=0)
st.session_state.agent_name = 'Generic Agent'
with st.chat_message("assistant"):
st.markdown(f'<div class="msg-timestamp">{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</div>', unsafe_allow_html=True)
st.write("欢迎使用GenericAgent~")
@st.fragment
def render_sidebar():
llm_options, current_idx = agent.list_llms(), agent.llm_no
st.session_state.selected_llm_idx = current_idx
llm_labels = {idx: f"{idx}: {(name or '').strip()}" for idx, name, _ in llm_options}
st.caption(f"当前使用的LLM为:{current_idx}: {agent.get_llm_name()}", help="可在下方选择链路")
st.markdown(f'<div data-testid="sidebar-llm-max-label" style="display:none">{html.escape(max(llm_labels.values(), key=len, default=""))}</div>', unsafe_allow_html=True)
selected_idx = st.selectbox("选择链路:", [idx for idx, _, _ in llm_options], index=next((i for i, (idx, _, _) in enumerate(llm_options) if idx == current_idx), 0), format_func=llm_labels.get, key="sidebar_llm_select")
if selected_idx != current_idx:
agent.next_llm(selected_idx)
st.session_state.selected_llm_idx = selected_idx
st.toast(f"已切换到备用链路:{llm_labels[selected_idx]}")
st.rerun()
st.divider()
if st.button("重新注入System Prompt"):
agent.llmclient.last_tools = ''
st.toast("下次将重新注入System Prompt")
with st.sidebar: render_sidebar()
def start_agent_task(prompt):
st.session_state.display_queue = agent.put_task(prompt, source="user")
st.session_state.streaming, st.session_state.stopping, st.session_state.partial_response = True, False, ''
st.session_state.reply_ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
st.session_state.current_prompt = prompt
def poll_agent_output(max_items=20):
q = st.session_state.display_queue
if q is None:
st.session_state.streaming = False
return False
done = False
for _ in range(max_items):
try:
item = q.get_nowait()
except queue.Empty:
break
if 'next' in item: st.session_state.partial_response = item['next']
if 'done' in item:
st.session_state.partial_response = item['done']
done = True
break
if done: st.session_state.streaming = st.session_state.stopping = False; st.session_state.display_queue = None
return done
def _get_response_segments(text):
return [p for p in re.split(r'(?=\*\*LLM Running \(Turn \d+\) \.\.\.\*\*)', text) if p.strip()] or [text]
def render_message(role, content, ts='', unsafe_allow_html=True):
with st.chat_message(role):
if ts: st.markdown(f'<div class="msg-timestamp">{ts}</div>', unsafe_allow_html=True)
st.markdown(content, unsafe_allow_html=unsafe_allow_html)
def finish_streaming_message():
reply_ts = st.session_state.reply_ts
st.session_state.messages.extend({"role": "assistant", "content": seg, "time": reply_ts} for seg in _get_response_segments(st.session_state.partial_response))
st.session_state.last_reply_time = int(time.time())
st.session_state.partial_response = st.session_state.reply_ts = st.session_state.current_prompt = ''
def render_streaming_area():
if not st.session_state.streaming: return
with st.container():
st.markdown('<span class="stop-btn-anchor"></span>', unsafe_allow_html=True)
if st.button("⏹️ 停止生成", type="primary"):
agent.abort(); st.session_state.stopping = True; st.toast("已发送停止信号"); st.rerun()
reply_ts = st.session_state.reply_ts
with st.empty().container():
segments = _get_response_segments(st.session_state.partial_response)
for i, seg in enumerate(segments): render_message("assistant", seg + ("" if i < len(segments) - 1 else "▌"), ts=reply_ts, unsafe_allow_html=False)
if poll_agent_output(): finish_streaming_message()
else: time.sleep(0.2)
st.rerun()
for msg in st.session_state.messages: render_message(msg["role"], msg["content"], ts=msg.get("time", ""), unsafe_allow_html=True)
if st.session_state.streaming: render_streaming_area()
if prompt := st.chat_input("请输入指令", disabled=st.session_state.streaming):
st.session_state.messages.append({"role": "user", "content": prompt, "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")})
start_agent_task(prompt)
st.rerun()