add context.md, strip LLM timestamps, clippy fixes, simplify deploy target

This commit is contained in:
Fam Zheng
2026-04-10 22:43:52 +01:00
parent c0e12798ee
commit 9d2d2af33f
8 changed files with 91 additions and 23 deletions

View File

@@ -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

43
context.md Normal file
View File

@@ -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 (子代<E5AD90><E4BBA3>执行引擎)
- **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

View File

@@ -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

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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<teloxide::types::MessageId>,
@@ -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 {

View File

@@ -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);

View File

@@ -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;