From 9d2d2af33f0812bec2253a3ec27c2875e6478cf4 Mon Sep 17 00:00:00 2001 From: Fam Zheng Date: Fri, 10 Apr 2026 22:43:52 +0100 Subject: [PATCH] add context.md, strip LLM timestamps, clippy fixes, simplify deploy target --- Makefile | 19 ++----------------- context.md | 43 +++++++++++++++++++++++++++++++++++++++++++ src/config.rs | 1 + src/display.rs | 19 +++++++++++++++++++ src/gitea.rs | 2 +- src/output.rs | 4 ++++ src/stream.rs | 24 ++++++++++++++++++++---- src/tools.rs | 2 +- 8 files changed, 91 insertions(+), 23 deletions(-) create mode 100644 context.md diff --git a/Makefile b/Makefile index 0bb1727..337a9ed 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ REPO := $(shell pwd) +SUITE := noc HERA := heradev HERA_DIR := noc IMAGE := noc-suite @@ -28,23 +29,7 @@ docker: build-musl # ── systemd deploy ────────────────────────────────────────────────── -noc.service: noc.service.in - sed -e 's|@REPO@|$(REPO)|g' -e 's|@PATH@|$(PATH)|g' $< > $@ - -deploy: test build noc.service - mkdir -p ~/bin ~/.config/systemd/user - systemctl --user stop noc 2>/dev/null || true - install target/release/noc ~/bin/noc - cp noc.service ~/.config/systemd/user/ - systemctl --user daemon-reload - systemctl --user enable --now noc - systemctl --user restart noc - -SUITE := noc -SUITE_DIR := noc -GITEA_VERSION := 1.23 - -deploy-suite: build +deploy: test build ssh $(SUITE) 'mkdir -p ~/bin /data/noc/tools ~/.config/systemd/user && systemctl --user stop noc 2>/dev/null || true' scp target/release/noc $(SUITE):~/bin/ scp config.suite.yaml $(SUITE):/data/noc/config.yaml diff --git a/context.md b/context.md new file mode 100644 index 0000000..50c7d5e --- /dev/null +++ b/context.md @@ -0,0 +1,43 @@ +你运行在 suite VPS (Ubuntu 24.04, 4C8G) 上,域名 famzheng.me。 + +### 服务架构 +- **noc**: systemd user service, binary ~/bin/noc, 数据 /data/noc/ +- **Gitea**: Docker container (gitea/gitea:1.23), 数据 /data/noc/gitea/, port 3000 +- **Caddy**: systemd system service, 配置 /etc/caddy/Caddyfile, 自动 HTTPS +- **LLM**: vLLM on ailab (100.84.7.49:8000), gemma-4-31B-it-AWQ +- **Claude Code**: ~/.local/bin/claude (子代��执行引擎) +- **uv**: ~/.local/bin/uv (Python 包管理) + +### 域名路由 (Caddy) +- famzheng.me — 主站(占位) +- git.famzheng.me → Gitea (localhost:3000) +- 新增子域名:编辑 /etc/caddy/Caddyfile,然后 `sudo systemctl reload caddy` + +### Caddy 管理 +Caddyfile 路径: /etc/caddy/Caddyfile +添加新站点示例: +``` +app.famzheng.me { + root * /data/www/app + file_server +} +``` +或反向代理: +``` +api.famzheng.me { + reverse_proxy localhost:8080 +} +``` +修改后执行 `sudo systemctl reload caddy` 生效。 +Caddy 自动申请和续期 Let's Encrypt 证书,无需手动管理。 + +### Gitea +- URL: https://git.famzheng.me +- Admin: noc (token 在 /data/noc/gitea-token) +- 可通过 call_gitea_api 工具或 spawn_agent 管理 + +### 可用工具 +- run_shell: 直接执行 shell 命令 +- run_python: uv run 执行 Python(支持 deps 自动安装) +- spawn_agent: 复杂任务交给 Claude Code 子代理 +- 管理 Caddy、部署 web app 等基础设施操作,优先用 spawn_agent diff --git a/src/config.rs b/src/config.rs index e9ac511..1a85c23 100644 --- a/src/config.rs +++ b/src/config.rs @@ -16,6 +16,7 @@ pub struct Config { } #[derive(Deserialize, Clone)] +#[allow(dead_code)] pub struct GiteaConfig { pub url: String, /// Direct token or read from token_file at startup diff --git a/src/display.rs b/src/display.rs index 00ef56d..fa55679 100644 --- a/src/display.rs +++ b/src/display.rs @@ -6,6 +6,25 @@ use teloxide::types::ParseMode; use crate::stream::{CURSOR, TG_MSG_LIMIT}; +/// Strip leading timestamps that LLM copies from our injected message timestamps. +/// Matches patterns like `[2026-04-10 21:13:15]` or `[2026-04-10 21:13]` at the start. +pub fn strip_leading_timestamp(s: &str) -> &str { + let trimmed = s.trim_start(); + if trimmed.starts_with('[') { + if let Some(end) = trimmed.find(']') { + let inside = &trimmed[1..end]; + // check if it looks like a timestamp: starts with 20xx- + if inside.len() >= 16 && inside.starts_with("20") && inside.contains('-') { + let after = trimmed[end + 1..].trim_start(); + if !after.is_empty() { + return after; + } + } + } + } + s +} + pub fn truncate_for_display(s: &str) -> String { let budget = TG_MSG_LIMIT - CURSOR.len() - 1; if s.len() <= budget { diff --git a/src/gitea.rs b/src/gitea.rs index df62e4d..d5e3ed8 100644 --- a/src/gitea.rs +++ b/src/gitea.rs @@ -6,7 +6,7 @@ use axum::http::StatusCode; use axum::response::IntoResponse; use axum::routing::post; use axum::Json; -use tracing::{error, info, warn}; +use tracing::{error, info}; use crate::config::GiteaConfig; diff --git a/src/output.rs b/src/output.rs index 916e935..17dc583 100644 --- a/src/output.rs +++ b/src/output.rs @@ -36,6 +36,7 @@ use crate::stream::{send_message_draft, DRAFT_INTERVAL_MS, EDIT_INTERVAL_MS, TG_ pub struct TelegramOutput { pub bot: Bot, pub chat_id: ChatId, + #[allow(dead_code)] pub is_private: bool, // internal state msg_id: Option, @@ -140,6 +141,7 @@ impl Output for TelegramOutput { use crate::gitea::GiteaClient; +#[allow(dead_code)] pub struct GiteaOutput { pub client: GiteaClient, pub owner: String, @@ -173,10 +175,12 @@ impl Output for GiteaOutput { // ── Buffer (for Worker, tests) ───────────────────────────────────── +#[allow(dead_code)] pub struct BufferOutput { pub text: String, } +#[allow(dead_code)] impl BufferOutput { pub fn new() -> Self { Self { diff --git a/src/stream.rs b/src/stream.rs index 46e5327..826c1d0 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -4,7 +4,7 @@ use anyhow::Result; use tracing::{error, info, warn}; use crate::config::Config; -use crate::display::truncate_at_char_boundary; +use crate::display::{strip_leading_timestamp, truncate_at_char_boundary}; use crate::output::Output; use crate::state::AppState; use crate::tools::{discover_tools, execute_tool, ToolCall}; @@ -209,11 +209,14 @@ pub async fn run_openai_with_tools( continue; } - if !accumulated.is_empty() { - let _ = output.finalize(&accumulated).await; + // strip timestamps that LLM copies from our message format + let cleaned = strip_leading_timestamp(&accumulated).to_string(); + + if !cleaned.is_empty() { + let _ = output.finalize(&cleaned).await; } - return Ok(accumulated); + return Ok(cleaned); } } @@ -257,6 +260,19 @@ pub fn build_system_prompt(summary: &str, persona: &str, memory_slots: &[(i32, S text.push_str(inner_state); } + // inject context file if present (e.g. /data/noc/context.md) + let config_path = std::env::var("NOC_CONFIG").unwrap_or_else(|_| "config.yaml".into()); + let context_path = std::path::Path::new(&config_path) + .parent() + .unwrap_or(std::path::Path::new(".")) + .join("context.md"); + if let Ok(ctx) = std::fs::read_to_string(&context_path) { + if !ctx.trim().is_empty() { + text.push_str("\n\n## 运行环境\n"); + text.push_str(ctx.trim()); + } + } + if !summary.is_empty() { text.push_str("\n\n## 之前的对话总结\n"); text.push_str(summary); diff --git a/src/tools.rs b/src/tools.rs index 368d8ea..2adf1ec 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -7,7 +7,7 @@ use anyhow::Result; use tokio::io::AsyncBufReadExt; use tokio::process::Command; use tokio::sync::RwLock; -use tracing::{error, info, warn}; +use tracing::{info, warn}; use crate::config::Config; use crate::display::truncate_at_char_boundary;