Fix HTML streaming: accumulate chunks and render only on is_complete
- botui/chat-messages.js: HTML chunks now accumulated without rendering, only showing loading indicator. When is_complete=true, full HTML rendered at once. Text/markdown continues streaming normally. - botserver/mod.rs: Remove unused html_buffer variable - drive_monitor/monitor.rs: Change CHECK_INTERVAL_SECS from 1 to 2 - CI workflow: Fix paths to use target/fast/ instead of target/debug/ and target/release/
This commit is contained in:
parent
d206f4ad48
commit
b392d508c3
8 changed files with 71 additions and 106 deletions
|
|
@ -40,11 +40,11 @@ steps:
|
||||||
run: |
|
run: |
|
||||||
echo "=== Deploying to Stage ==="
|
echo "=== Deploying to Stage ==="
|
||||||
scp -i /home/gbuser/.ssh/id_ed25519 -o StrictHostKeyChecking=no \
|
scp -i /home/gbuser/.ssh/id_ed25519 -o StrictHostKeyChecking=no \
|
||||||
/opt/gbo/work/generalbots/target/debug/botserver \
|
/opt/gbo/work/generalbots/target/fast/botserver \
|
||||||
gbuser@system:/opt/gbo/bin/botserver-new
|
gbuser@system:/opt/gbo/bin/botserver-new
|
||||||
scp -i /home/gbuser/.ssh/id_ed25519 -o StrictHostKeyChecking=no \
|
scp -i /home/gbuser/.ssh/id_ed25519 -o StrictHostKeyChecking=no \
|
||||||
/opt/gbo/work/generalbots/target/release/botui \
|
/opt/gbo/work/generalbots/target/fast/botui \
|
||||||
gbuser@system:/opt/gbo/bin/botui-new
|
gbuser@system:/opt/gbo/bin/botui-new
|
||||||
ssh -i /home/gbuser/.ssh/id_ed25519 -o StrictHostKeyChecking=no \
|
ssh -i /home/gbuser/.ssh/id_ed25519 -o StrictHostKeyChecking=no \
|
||||||
gbuser@system \
|
gbuser@system \
|
||||||
"sudo systemctl stop botserver || true && \
|
"sudo systemctl stop botserver || true && \
|
||||||
|
|
|
||||||
|
|
@ -25,33 +25,6 @@ install_rust() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
install_sccache() {
|
|
||||||
WANT_VER="0.14.0"
|
|
||||||
CURRENT_VER=""
|
|
||||||
if command -v sccache &> /dev/null; then
|
|
||||||
CURRENT_VER=$(sccache --version 2>/dev/null | grep -oP '[\d.]+' | head -1)
|
|
||||||
fi
|
|
||||||
if [ "$CURRENT_VER" = "$WANT_VER" ]; then
|
|
||||||
echo "sccache $WANT_VER already installed"
|
|
||||||
else
|
|
||||||
echo "Upgrading sccache from ${CURRENT_VER:-none} to $WANT_VER..."
|
|
||||||
rm -f /usr/local/bin/sccache /usr/local/bin/sccache-dist
|
|
||||||
ARCH=$(uname -m)
|
|
||||||
curl -L "https://github.com/mozilla/sccache/releases/download/v${WANT_VER}/sccache-v${WANT_VER}-${ARCH}-unknown-linux-musl.tar.gz" -o /tmp/sccache.tar.gz
|
|
||||||
tar -xzf /tmp/sccache.tar.gz -C /tmp
|
|
||||||
cp "/tmp/sccache-v${WANT_VER}-${ARCH}-unknown-linux-musl/sccache" /usr/local/bin/sccache.real
|
|
||||||
chmod +x /usr/local/bin/sccache.real
|
|
||||||
rm -rf /tmp/sccache*
|
|
||||||
fi
|
|
||||||
# Install wrapper that unsets CARGO_INCREMENTAL before calling sccache.real
|
|
||||||
cat > /usr/local/bin/sccache << 'EOF'
|
|
||||||
#!/bin/bash
|
|
||||||
unset CARGO_INCREMENTAL
|
|
||||||
exec /usr/local/bin/sccache.real "$@"
|
|
||||||
EOF
|
|
||||||
chmod +x /usr/local/bin/sccache
|
|
||||||
sccache --version
|
|
||||||
}
|
|
||||||
|
|
||||||
install_mold() {
|
install_mold() {
|
||||||
if command -v mold &> /dev/null; then
|
if command -v mold &> /dev/null; then
|
||||||
|
|
@ -92,7 +65,6 @@ CARGOCONF
|
||||||
}
|
}
|
||||||
|
|
||||||
install_rust
|
install_rust
|
||||||
install_sccache
|
|
||||||
install_mold
|
install_mold
|
||||||
install_cargo_tools
|
install_cargo_tools
|
||||||
setup_cargo_config
|
setup_cargo_config
|
||||||
|
|
@ -101,7 +73,6 @@ echo ""
|
||||||
echo "✅ Dev environment ready:"
|
echo "✅ Dev environment ready:"
|
||||||
echo " Rust: $(rustc --version)"
|
echo " Rust: $(rustc --version)"
|
||||||
echo " Linker: clang + lld + mold"
|
echo " Linker: clang + lld + mold"
|
||||||
echo " Cache: sccache"
|
|
||||||
echo " Audit: cargo-audit, cargo-machete, cargo-tree"
|
echo " Audit: cargo-audit, cargo-machete, cargo-tree"
|
||||||
echo "📦 .cargo/config.toml configured"
|
echo "📦 .cargo/config.toml configured"
|
||||||
echo "⚡ Build: cargo build -p botserver --bin botserver"
|
echo "⚡ Build: cargo build -p botserver --bin botserver"
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ sudo apt update
|
||||||
sudo apt install -y \
|
sudo apt install -y \
|
||||||
clang \
|
clang \
|
||||||
lld \
|
lld \
|
||||||
|
mold \
|
||||||
build-essential \
|
build-essential \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
|
|
@ -111,7 +112,7 @@ mkdir -p ~/.cargo
|
||||||
cat >> ~/.cargo/config.toml << EOF
|
cat >> ~/.cargo/config.toml << EOF
|
||||||
[target.x86_64-unknown-linux-gnu]
|
[target.x86_64-unknown-linux-gnu]
|
||||||
linker = "clang"
|
linker = "clang"
|
||||||
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
|
rustflags = ["-C", "link-arg=-fuse-ld=mold"] # or "lld"
|
||||||
EOF
|
EOF
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -123,6 +124,7 @@ EOF
|
||||||
sudo dnf install -y \
|
sudo dnf install -y \
|
||||||
clang \
|
clang \
|
||||||
lld \
|
lld \
|
||||||
|
mold \
|
||||||
gcc \
|
gcc \
|
||||||
gcc-c++ \
|
gcc-c++ \
|
||||||
make \
|
make \
|
||||||
|
|
@ -156,7 +158,7 @@ mkdir -p ~/.cargo
|
||||||
cat >> ~/.cargo/config.toml << EOF
|
cat >> ~/.cargo/config.toml << EOF
|
||||||
[target.x86_64-unknown-linux-gnu]
|
[target.x86_64-unknown-linux-gnu]
|
||||||
linker = "clang"
|
linker = "clang"
|
||||||
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
|
rustflags = ["-C", "link-arg=-fuse-ld=mold"] # or "lld"
|
||||||
EOF
|
EOF
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -192,9 +194,9 @@ git submodule update --init --recursive
|
||||||
|
|
||||||
## Build Cache with sccache
|
## Build Cache with sccache
|
||||||
|
|
||||||
sccache caches compilation artifacts for faster rebuilds.
|
sccache (Shared Compilation Cache) caches compilation artifacts to accelerate rebuilds across different environments.
|
||||||
|
|
||||||
Install and configure:
|
### Installation & Configuration
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo install sccache
|
cargo install sccache
|
||||||
|
|
@ -204,14 +206,26 @@ compiler = "sccache"' >> ~/.cargo/config.toml
|
||||||
export RUSTC_WRAPPER=sccache
|
export RUSTC_WRAPPER=sccache
|
||||||
```
|
```
|
||||||
|
|
||||||
Verify cache hits:
|
### Ephemeral vs. Persistent Environments
|
||||||
|
|
||||||
|
The decision to use `sccache` depends heavily on your build environment:
|
||||||
|
|
||||||
|
#### 1. When to use sccache (Ephemeral/Clean CI)
|
||||||
|
Use `sccache` in environments that start with a **clean disk** for every build (e.g., standard GitHub Actions, cloud-based CI). Since the `target/` directory is lost between runs, `sccache` allows you to recover compiled artifacts from a remote bucket (S3/GCS) or global persistent cache, saving hours of dependency recompilation.
|
||||||
|
|
||||||
|
#### 2. When to avoid sccache (Persistent/Self-hosted)
|
||||||
|
If you are using a **self-hosted runner** with a **persistent `target/` directory** (like the standard General Bots production setup), Cargo's native incremental logic is usually faster than `sccache`.
|
||||||
|
- **Reason**: Cargo native incrementalism only checks file timestamps and metadata. `sccache` must calculate cryptographic hashes of every source file, which adds overhead.
|
||||||
|
- **Recommendation**: For persistent runners, rely on `CARGO_INCREMENTAL=1` and a fast linker like `mold` or `lld`.
|
||||||
|
|
||||||
|
### Monitoring and Maintenance
|
||||||
|
|
||||||
|
Verify cache hits:
|
||||||
```bash
|
```bash
|
||||||
sccache --show-stats
|
sccache --show-stats
|
||||||
```
|
```
|
||||||
|
|
||||||
Clear cache if needed:
|
Clear cache if needed:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sccache --zero-stats
|
sccache --zero-stats
|
||||||
```
|
```
|
||||||
|
|
@ -494,7 +508,7 @@ $env:PQ_LIB_DIR="C:\Program Files\PostgreSQL\15\lib"
|
||||||
|
|
||||||
### Out of Memory During Build
|
### Out of Memory During Build
|
||||||
|
|
||||||
Use sccache to cache compilations:
|
Use sccache to cache compilations (helps avoid recompilation in ephemeral CI, but does not reduce peak memory of a single crate):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo install sccache
|
cargo install sccache
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ sudo incus exec alm-ci -- tail -f /opt/gbo/logs/forgejo-runner.log | grep -E "Cl
|
||||||
| Rust compilation (incremental) | 30-60 seconds |
|
| Rust compilation (incremental) | 30-60 seconds |
|
||||||
| First build (dependencies) | Downloads ~200 crates |
|
| First build (dependencies) | Downloads ~200 crates |
|
||||||
| Deploy step | ~5 seconds |
|
| Deploy step | ~5 seconds |
|
||||||
| Total CI time | 2-6 minutes depending on cache |
|
| Total CI time | 2-4 minutes (incremental with mold) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -173,7 +173,7 @@ curl -sf https://<system-domain>/api/health && echo "OK" || echo "FAILED"
|
||||||
- **User:** gbuser (uid 1000)
|
- **User:** gbuser (uid 1000)
|
||||||
- **Workspace:** /opt/gbo/data/
|
- **Workspace:** /opt/gbo/data/
|
||||||
- **SSH deploy key:** /home/gbuser/.ssh/id_ed25519
|
- **SSH deploy key:** /home/gbuser/.ssh/id_ed25519
|
||||||
- **sccache:** /usr/local/bin/sccache (via RUSTC_WRAPPER=sccache)
|
- **Cache:** Native Cargo incremental (target/ persistence)
|
||||||
- **Cargo cache:** /home/gbuser/.cargo/
|
- **Cargo cache:** /home/gbuser/.cargo/
|
||||||
- **Rustup:** /home/gbuser/.rustup/
|
- **Rustup:** /home/gbuser/.rustup/
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1054,8 +1054,7 @@ let system_prompt = if !message.active_switchers.is_empty() {
|
||||||
let mut in_analysis = false;
|
let mut in_analysis = false;
|
||||||
let mut tool_call_buffer = String::new(); // Accumulate potential tool call JSON chunks
|
let mut tool_call_buffer = String::new(); // Accumulate potential tool call JSON chunks
|
||||||
let mut accumulating_tool_call = false; // Track if we're currently accumulating a tool call
|
let mut accumulating_tool_call = false; // Track if we're currently accumulating a tool call
|
||||||
let mut html_buffer = String::new(); // Buffer for HTML content
|
let handler = llm_models::get_handler(&model);
|
||||||
let handler = llm_models::get_handler(&model);
|
|
||||||
|
|
||||||
trace!("Using model handler for {}", model);
|
trace!("Using model handler for {}", model);
|
||||||
info!("llm_start: Starting LLM streaming for session {}", session.id);
|
info!("llm_start: Starting LLM streaming for session {}", session.id);
|
||||||
|
|
@ -1438,29 +1437,7 @@ if !in_analysis {
|
||||||
#[cfg(not(feature = "chat"))]
|
#[cfg(not(feature = "chat"))]
|
||||||
let switchers: Vec<Switcher> = Vec::new();
|
let switchers: Vec<Switcher> = Vec::new();
|
||||||
|
|
||||||
// Flush any remaining HTML buffer before sending final response
|
// Content was already sent as streaming chunks.
|
||||||
if !html_buffer.is_empty() {
|
|
||||||
trace!("Flushing remaining {} chars in HTML buffer", html_buffer.len());
|
|
||||||
let final_chunk = BotResponse {
|
|
||||||
bot_id: message.bot_id.clone(),
|
|
||||||
user_id: message.user_id.clone(),
|
|
||||||
session_id: message.session_id.clone(),
|
|
||||||
channel: message.channel.clone(),
|
|
||||||
content: html_buffer.clone(),
|
|
||||||
message_type: MessageType::BOT_RESPONSE,
|
|
||||||
stream_token: None,
|
|
||||||
is_complete: false,
|
|
||||||
suggestions: Vec::new(),
|
|
||||||
switchers: Vec::new(),
|
|
||||||
context_name: None,
|
|
||||||
context_length: 0,
|
|
||||||
context_max_length: 0,
|
|
||||||
};
|
|
||||||
let _ = response_tx.send(final_chunk).await;
|
|
||||||
html_buffer.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content was already sent as streaming chunks.
|
|
||||||
// Sending full_response again would duplicate it (especially for WhatsApp which accumulates buffer).
|
// Sending full_response again would duplicate it (especially for WhatsApp which accumulates buffer).
|
||||||
// The final response is just a signal that streaming is complete - it should not contain content.
|
// The final response is just a signal that streaming is complete - it should not contain content.
|
||||||
let final_content = String::new();
|
let final_content = String::new();
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use std::time::Duration;
|
||||||
use super::types::DriveMonitor;
|
use super::types::DriveMonitor;
|
||||||
|
|
||||||
/// Intervalo de verificação do DriveMonitor e DriveCompiler (em segundos)
|
/// Intervalo de verificação do DriveMonitor e DriveCompiler (em segundos)
|
||||||
pub const CHECK_INTERVAL_SECS: u64 = 1;
|
pub const CHECK_INTERVAL_SECS: u64 = 2;
|
||||||
|
|
||||||
impl DriveMonitor {
|
impl DriveMonitor {
|
||||||
pub fn calculate_backoff(&self) -> Duration {
|
pub fn calculate_backoff(&self) -> Duration {
|
||||||
|
|
|
||||||
|
|
@ -256,7 +256,7 @@ rustls=off,rustls_pemfile=off,tokio_rustls=off,\
|
||||||
Ok(existing) if !existing.is_empty() => format!("{},{}", existing, noise_filters),
|
Ok(existing) if !existing.is_empty() => format!("{},{}", existing, noise_filters),
|
||||||
_ => format!("info,{}", noise_filters),
|
_ => format!("info,{}", noise_filters),
|
||||||
};
|
};
|
||||||
// Test mold+sccache build
|
// Test mold+incremental build
|
||||||
|
|
||||||
std::env::set_var("RUST_LOG", &rust_log);
|
std::env::set_var("RUST_LOG", &rust_log);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,49 +92,52 @@ function isTagBalanced(html) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStreaming(content) {
|
function updateStreaming(content) {
|
||||||
var el = document.getElementById(ChatState.streamingMessageId);
|
var el = document.getElementById(ChatState.streamingMessageId);
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
var msgContent = el.querySelector(".message-content");
|
var msgContent = el.querySelector(".message-content");
|
||||||
var cleanContent = stripMarkdownBlocks(content);
|
var cleanContent = stripMarkdownBlocks(content);
|
||||||
var isHtml = /<\/?[a-zA-Z][^>]*>|<!--|-->/i.test(cleanContent);
|
var isHtml = /<\/?[a-zA-Z][^>]*>|<!--|-->/i.test(cleanContent);
|
||||||
|
|
||||||
if (isHtml) {
|
if (isHtml) {
|
||||||
if (isTagBalanced(cleanContent) || (Date.now() - ChatState.lastRenderTime > 2000)) {
|
if (!el.querySelector(".streaming-loading")) {
|
||||||
msgContent.innerHTML = renderMentionInMessage(cleanContent); // Don't escape HTML
|
var loader = document.createElement("div");
|
||||||
ChatState.lastRenderTime = Date.now();
|
loader.className = "streaming-loading";
|
||||||
if (!ChatState.isUserScrolling) scrollToBottom(true);
|
loader.innerHTML = '<span class="loading-dots">...</span>';
|
||||||
|
msgContent.appendChild(loader);
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
var parsed = typeof marked !== "undefined" && marked.parse
|
||||||
var parsed = typeof marked !== "undefined" && marked.parse
|
? marked.parse(cleanContent)
|
||||||
? marked.parse(cleanContent)
|
: escapeHtml(cleanContent);
|
||||||
: escapeHtml(cleanContent);
|
parsed = renderMentionInMessage(parsed);
|
||||||
parsed = renderMentionInMessage(parsed);
|
msgContent.innerHTML = parsed;
|
||||||
msgContent.innerHTML = parsed;
|
if (!ChatState.isUserScrolling) scrollToBottom(true);
|
||||||
if (!ChatState.isUserScrolling) scrollToBottom(true);
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function finalizeStreaming() {
|
function finalizeStreaming() {
|
||||||
var el = document.getElementById(ChatState.streamingMessageId);
|
var el = document.getElementById(ChatState.streamingMessageId);
|
||||||
if (el) {
|
if (el) {
|
||||||
var cleanContent = stripMarkdownBlocks(ChatState.currentStreamingContent);
|
var cleanContent = stripMarkdownBlocks(ChatState.currentStreamingContent);
|
||||||
var hasHtmlTags = /<\/?[a-zA-Z][^>]*>|<!--|-->/i.test(cleanContent);
|
var hasHtmlTags = /<\/?[a-zA-Z][^>]*>|<!--|-->/i.test(cleanContent);
|
||||||
var parsed = hasHtmlTags
|
var parsed;
|
||||||
? cleanContent // Don't escape HTML
|
if (hasHtmlTags) {
|
||||||
: (typeof marked !== "undefined" && marked.parse
|
parsed = cleanContent;
|
||||||
|
} else {
|
||||||
|
parsed = typeof marked !== "undefined" && marked.parse
|
||||||
? marked.parse(cleanContent)
|
? marked.parse(cleanContent)
|
||||||
: escapeHtml(cleanContent));
|
: escapeHtml(cleanContent);
|
||||||
parsed = renderMentionInMessage(parsed);
|
}
|
||||||
el.querySelector(".message-content").innerHTML = parsed;
|
parsed = renderMentionInMessage(parsed);
|
||||||
el.removeAttribute("id");
|
el.querySelector(".message-content").innerHTML = parsed;
|
||||||
setupMentionClickHandlers(el);
|
el.removeAttribute("id");
|
||||||
if (!ChatState.isUserScrolling) scrollToBottom(true);
|
setupMentionClickHandlers(el);
|
||||||
}
|
if (!ChatState.isUserScrolling) scrollToBottom(true);
|
||||||
ChatState.streamingMessageId = null;
|
}
|
||||||
ChatState.currentStreamingContent = "";
|
ChatState.streamingMessageId = null;
|
||||||
ChatState.streamingBuffer = "";
|
ChatState.currentStreamingContent = "";
|
||||||
|
ChatState.streamingBuffer = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function processMessage(data) {
|
function processMessage(data) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue