Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79957eba0f |
No files matched your search
@@ -47,3 +47,6 @@ screenshots/gen_*.png
|
|||||||
# 临时文件
|
# 临时文件
|
||||||
*.tmp
|
*.tmp
|
||||||
*.bak
|
*.bak
|
||||||
|
|
||||||
|
# v8.0 TUI 快照产物(运行时生成,供 AI 自验/预览)
|
||||||
|
.tui_snapshots/
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://img.shields.io/badge/version-5.4.1-blue" alt="version">
|
<img src="https://img.shields.io/badge/version-8.0.0-blue" alt="version">
|
||||||
<img src="https://img.shields.io/badge/python-3.10+-green" alt="python">
|
<img src="https://img.shields.io/badge/python-3.10+-green" alt="python">
|
||||||
<img src="https://img.shields.io/badge/license-MIT-orange" alt="license">
|
<img src="https://img.shields.io/badge/license-MIT-orange" alt="license">
|
||||||
<img src="https://img.shields.io/badge/tests-150+%20passed-brightgreen" alt="tests">
|
<img src="https://img.shields.io/badge/tests-150+%20passed-brightgreen" alt="tests">
|
||||||
@@ -22,6 +22,12 @@
|
|||||||
|
|
||||||
小狸 Pro-CLI 是一款基于 Python 的智能编程助手,集成了代码编辑、版本控制、浏览器自动化、图像生成、记忆系统等功能,支持多种 AI 引擎,帮助开发者提升工作效率。
|
小狸 Pro-CLI 是一款基于 Python 的智能编程助手,集成了代码编辑、版本控制、浏览器自动化、图像生成、记忆系统等功能,支持多种 AI 引擎,帮助开发者提升工作效率。
|
||||||
|
|
||||||
|
## v8.0 新特性
|
||||||
|
|
||||||
|
- **Rich 面板式 TUI(默认)** — `rich` 驱动的可视化对话面板;自带**快照出口**(导出 text/svg/html),AI 开发界面时可把渲染结果读回自验,补上"看不见渲染"的反馈闭环。输入 `/tui` 进入,`/snapshot` 随时导出当前界面。
|
||||||
|
- **Token 感知压缩** — 自动读取 OpenAI 格式返回的 `usage.prompt_tokens`,当 `已用 token / 模型上下文窗口 ≥ 0.7`(可在 `config.json` 设 `system.compress_ratio` / `system.context_window` 覆盖)即触发上下文压缩,消息数阈值作为兜底,避免超窗截断。
|
||||||
|
- **记忆模式** — 持久化 `MEMORY.md` + 每日日记 + 自动保存聊天记录 + 关键词检索;新增 `/memory mode <companion|work>` 切换与 `/compress` 手动压缩。
|
||||||
|
|
||||||
## 特性
|
## 特性
|
||||||
|
|
||||||
- **双 AI 引擎** — Ollama 本地模型 + OpenAI 兼容格式(DeepSeek、Grok、硅基流动等)
|
- **双 AI 引擎** — Ollama 本地模型 + OpenAI 兼容格式(DeepSeek、Grok、硅基流动等)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
**产品标识**
|
**产品标识**
|
||||||
- **产品名称**:小狸智能体 增强-终端(Xiaoli Pro-CLI)
|
- **产品名称**:小狸智能体 增强-终端(Xiaoli Pro-CLI)
|
||||||
- **内部版本号**:20260428
|
- **内部版本号**:20260801
|
||||||
- **发布版本**:发行版5.1.3
|
- **发布版本**:发行版8.0.0
|
||||||
|
|
||||||
**技术规格**
|
**技术规格**
|
||||||
- **主要语言**:简体中文(UTF-8)
|
- **主要语言**:简体中文(UTF-8)
|
||||||
|
|||||||
@@ -13,6 +13,31 @@ import logging
|
|||||||
# 设置日志
|
# 设置日志
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from xcli_core.config import get_system_config
|
||||||
|
except Exception:
|
||||||
|
def get_system_config(key, default=None):
|
||||||
|
return default
|
||||||
|
|
||||||
|
# ── 常见模型的上下文窗口(token 数)──
|
||||||
|
# OpenAI /chat/completions 协议本身不返回模型窗口,需本地维护。
|
||||||
|
MODEL_CONTEXT_WINDOWS = {
|
||||||
|
"gpt-4o": 128000, "gpt-4o-mini": 128000, "gpt-4": 8192, "gpt-4-turbo": 128000,
|
||||||
|
"gpt-3.5-turbo": 16385,
|
||||||
|
"deepseek-chat": 64000, "deepseek-reasoner": 64000,
|
||||||
|
"qwen2.5": 32768, "qwen2": 32768, "qwen-max": 32768, "qwen-plus": 32768,
|
||||||
|
"llama3": 8192, "llama3.1": 128000, "llama3.2": 128000, "llama2": 4096,
|
||||||
|
"claude-3": 200000, "claude-3.5": 200000, "claude-3.7": 200000, "claude": 200000,
|
||||||
|
"grok": 131072, "grok-2": 131072, "grok-3": 131072,
|
||||||
|
"glm-4": 128000, "glm-4v": 128000,
|
||||||
|
"mistral": 32768, "mixtral": 32768, "mistral-large": 128000,
|
||||||
|
"gemini": 1000000, "gemini-1.5": 1000000, "gemini-2.0": 1000000, "gemini-2.5": 1000000,
|
||||||
|
"yi": 200000, "yi-large": 200000,
|
||||||
|
"kimi": 200000, "moonshot": 200000,
|
||||||
|
"abab": 200000, "minimax": 200000,
|
||||||
|
"baichuan": 192000, "chatglm": 32768,
|
||||||
|
}
|
||||||
|
|
||||||
# 添加项目根目录到sys.path
|
# 添加项目根目录到sys.path
|
||||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
project_root = os.path.dirname(current_dir)
|
project_root = os.path.dirname(current_dir)
|
||||||
@@ -95,6 +120,9 @@ OpenAI 兼容格式引擎帮助信息
|
|||||||
self.conversation_history = []
|
self.conversation_history = []
|
||||||
self.shared_conversation_history = None
|
self.shared_conversation_history = None
|
||||||
|
|
||||||
|
# 最近一次请求的 token 用量(供 token 感知压缩使用)
|
||||||
|
self.last_prompt_tokens = None
|
||||||
|
|
||||||
# 思考标记(兼容深度思考模型)
|
# 思考标记(兼容深度思考模型)
|
||||||
self.thinking_start_marker = "<think>"
|
self.thinking_start_marker = "<think>"
|
||||||
self.thinking_end_marker = "</think>"
|
self.thinking_end_marker = "</think>"
|
||||||
@@ -169,6 +197,13 @@ OpenAI 兼容格式引擎帮助信息
|
|||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
response_data = response.json()
|
response_data = response.json()
|
||||||
|
# ── 记录 token 用量(供 token 感知压缩)──
|
||||||
|
usage = response_data.get("usage", {})
|
||||||
|
if isinstance(usage, dict) and usage.get("prompt_tokens"):
|
||||||
|
try:
|
||||||
|
self.last_prompt_tokens = int(usage["prompt_tokens"])
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
self.last_prompt_tokens = None
|
||||||
if "choices" in response_data and len(response_data["choices"]) > 0:
|
if "choices" in response_data and len(response_data["choices"]) > 0:
|
||||||
message = response_data["choices"][0].get("message", {})
|
message = response_data["choices"][0].get("message", {})
|
||||||
|
|
||||||
@@ -342,6 +377,24 @@ OpenAI 兼容格式引擎帮助信息
|
|||||||
self.model = model_name
|
self.model = model_name
|
||||||
print(f"{Fore.GREEN}已切换模型为: {model_name}{Style.RESET_ALL}")
|
print(f"{Fore.GREEN}已切换模型为: {model_name}{Style.RESET_ALL}")
|
||||||
|
|
||||||
|
def context_window(self):
|
||||||
|
"""
|
||||||
|
返回当前模型的上下文窗口(token 数)。
|
||||||
|
优先级: config.system.context_window > 模型名查表 > 默认 128000。
|
||||||
|
注: OpenAI /chat/completions 协议本身不返回模型窗口,需本地维护。
|
||||||
|
"""
|
||||||
|
cfg = get_system_config("context_window", None)
|
||||||
|
if cfg:
|
||||||
|
try:
|
||||||
|
return int(cfg)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
model = (self.model or "").lower()
|
||||||
|
for key, win in MODEL_CONTEXT_WINDOWS.items():
|
||||||
|
if key and key in model:
|
||||||
|
return win
|
||||||
|
return 128000
|
||||||
|
|
||||||
def list_models(self):
|
def list_models(self):
|
||||||
"""列出可用模型(调用 API 的 /models 端点)"""
|
"""列出可用模型(调用 API 的 /models 端点)"""
|
||||||
if not self.base_url:
|
if not self.base_url:
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ pygame>=2.0.0
|
|||||||
|
|
||||||
# TUI 模式
|
# TUI 模式
|
||||||
textual>=0.40.0
|
textual>=0.40.0
|
||||||
|
# v8.0 Rich 面板式 TUI(支持快照自验,默认 TUI 渲染器)
|
||||||
|
rich>=13.0.0
|
||||||
|
|
||||||
# WebSocket 远程
|
# WebSocket 远程
|
||||||
websockets>=11.0
|
websockets>=11.0
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"""
|
||||||
|
v8.0 TUI 快照生成器(无头)
|
||||||
|
==========================
|
||||||
|
直接用 RichTUI.push() 喂一段脚本化对话,导出 .tui_snapshots/demo.{txt,svg,html}。
|
||||||
|
AI 可把 .txt 读回核验布局;把 .html/.svg 交给用户预览。
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from rich.text import Text
|
||||||
|
from xcli_core.rich_tui import RichTUI, SNAP_DIR
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
tui = RichTUI(cli=None)
|
||||||
|
tui.set_status(
|
||||||
|
engine="openai", model="deepseek-chat", mode="TUI",
|
||||||
|
used_tokens=47000, context_window=64000, msg_count=4,
|
||||||
|
)
|
||||||
|
tui.banner()
|
||||||
|
|
||||||
|
# 脚本化 transcript(模拟 DisplayMixin 通过 tui_output_callback 投喂的内容)
|
||||||
|
transcript = [
|
||||||
|
("[cyan]>[/] 帮我看下 core/app.py 有没有内存泄漏", "user"),
|
||||||
|
("[yellow]思考: 我先搜索一下文件结构与资源释放逻辑...[/]", "think"),
|
||||||
|
("[green] OK 工具: read_file[/]", "tool_ok"),
|
||||||
|
("[green] 文件路径: core/app.py (248 行)[/]", "tool_ok"),
|
||||||
|
("[red] X 工具: shell_exec[/]", "tool_err"),
|
||||||
|
("[red] 错误: 命令超时 (30s)[/]", "tool_err"),
|
||||||
|
("[white]根据分析,core/app.py 在 __del__ 中调用 cleanup_resources,"
|
||||||
|
"但多线程下可能重复释放...[/]", "assistant"),
|
||||||
|
("[cyan]>[/] 那顺手修一下吧", "user"),
|
||||||
|
("[green] OK 工具: edit_file[/]", "tool_ok"),
|
||||||
|
("[green] 已更新 cleanup_resources,加锁防止重复释放[/]", "tool_ok"),
|
||||||
|
("[white]已修复:为 cleanup_resources 增加了 _cleaned 标志位,确保只释放一次。[/]", "assistant"),
|
||||||
|
]
|
||||||
|
for msg, _kind in transcript:
|
||||||
|
tui.push(msg)
|
||||||
|
|
||||||
|
tui.console.print(Text.from_markup(tui._bar()))
|
||||||
|
|
||||||
|
os.makedirs(SNAP_DIR, exist_ok=True)
|
||||||
|
stem = os.path.join(SNAP_DIR, "demo")
|
||||||
|
txt, svg, html = tui.save_snapshot(stem)
|
||||||
|
print("快照已生成:")
|
||||||
|
print(" 文本:", txt)
|
||||||
|
print(" SVG :", svg)
|
||||||
|
print(" HTML:", html)
|
||||||
|
print("\n===== 文本快照预览 =====\n")
|
||||||
|
print(tui.snapshot_text())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+74
-19
@@ -9,7 +9,7 @@ import random
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from colorama import Fore, Style
|
from colorama import Fore, Style
|
||||||
|
|
||||||
from .constants import TEXTUAL_AVAILABLE, LOVE_FILE_PATH, DEFAULT_MAX_HISTORY
|
from .constants import TEXTUAL_AVAILABLE, LOVE_FILE_PATH, DEFAULT_MAX_HISTORY, VERSION
|
||||||
from .config import get_system_config, logger
|
from .config import get_system_config, logger
|
||||||
from .cli_base import BaseAICLI
|
from .cli_base import BaseAICLI
|
||||||
from .safety import get_safety, MODE_UNRESTRICTED, MODE_NORMAL, MODE_MANUAL
|
from .safety import get_safety, MODE_UNRESTRICTED, MODE_NORMAL, MODE_MANUAL
|
||||||
@@ -714,17 +714,33 @@ multi 操作支持一次修改多处:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# ── 自动压缩上下文 ──
|
# ── 自动压缩上下文(token 感知优先,消息数兜底)──
|
||||||
try:
|
try:
|
||||||
if self.memory_manager.should_compress(self.shared_conversation_history):
|
if hasattr(self, 'memory_manager') and self.memory_manager:
|
||||||
self.shared_conversation_history = self.memory_manager.compress_context(
|
engine = self.current_engine
|
||||||
self.shared_conversation_history,
|
prompt_tokens = getattr(engine, 'last_prompt_tokens', None) if engine else None
|
||||||
engine=self.current_engine
|
ctx = None
|
||||||
)
|
if engine and hasattr(engine, 'context_window'):
|
||||||
self._set_shared_conversation_history()
|
try:
|
||||||
print(f"{Fore.DIM}[上下文已压缩,保留最近 10 条]{Style.RESET_ALL}")
|
ctx = engine.context_window()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
ctx = None
|
||||||
|
ratio = get_system_config('compress_ratio', 0.7)
|
||||||
|
self.memory_manager.set_compress_ratio(ratio)
|
||||||
|
if self.memory_manager.should_compress(
|
||||||
|
self.shared_conversation_history, prompt_tokens, ctx, ratio
|
||||||
|
):
|
||||||
|
before = len(self.shared_conversation_history)
|
||||||
|
self.shared_conversation_history = self.memory_manager.compress_context(
|
||||||
|
self.shared_conversation_history,
|
||||||
|
engine=engine
|
||||||
|
)
|
||||||
|
token_info = f",token {prompt_tokens}/{ctx}" if (prompt_tokens and ctx) else ""
|
||||||
|
self._output(
|
||||||
|
f"{Fore.CYAN}🗜 上下文已自动压缩 (原 {before} 条 → {len(self.shared_conversation_history)} 条{token_info},阈值 {ratio}){Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"自动压缩上下文失败: {e}")
|
||||||
|
|
||||||
def _extract_task_summary(self, user_input: str) -> str:
|
def _extract_task_summary(self, user_input: str) -> str:
|
||||||
"""提取任务摘要用于通知显示"""
|
"""提取任务摘要用于通知显示"""
|
||||||
@@ -983,20 +999,35 @@ multi 操作支持一次修改多处:
|
|||||||
# ── CLI 主循环 ──
|
# ── CLI 主循环 ──
|
||||||
|
|
||||||
def run_tui(self):
|
def run_tui(self):
|
||||||
"""运行 TUI 模式"""
|
"""运行 TUI 模式(v8.0 默认 Rich 面板 TUI,支持快照自验;textual 作为可选回退)"""
|
||||||
if not TEXTUAL_AVAILABLE:
|
try:
|
||||||
print(f"{Fore.RED}TUI 模式不可用:Textual 库未安装{Style.RESET_ALL}")
|
from .rich_tui import RichTUI
|
||||||
print(f"{Fore.YELLOW}请运行: pip install textual rich{Style.RESET_ALL}")
|
from xcli_core.constants import VERSION as _V
|
||||||
|
tui = RichTUI(self)
|
||||||
|
tui.version = _V
|
||||||
|
tui.set_status(
|
||||||
|
engine=getattr(self.current_engine, 'name', '') if self.current_engine else '',
|
||||||
|
model=getattr(self.current_engine, 'model', '') if self.current_engine else '',
|
||||||
|
mode='TUI',
|
||||||
|
)
|
||||||
|
self.tui_output_callback = tui.push
|
||||||
|
tui.run()
|
||||||
|
self.tui_output_callback = None
|
||||||
return
|
return
|
||||||
from .tui import XiaoliTUI
|
except ImportError:
|
||||||
app = XiaoliTUI(self)
|
# 回退到旧版 textual TUI
|
||||||
app.run()
|
if not TEXTUAL_AVAILABLE:
|
||||||
|
print(f"{Fore.RED}TUI 不可用: 请安装 rich (pip install rich){Style.RESET_ALL}")
|
||||||
|
return
|
||||||
|
from .tui import XiaoliTUI
|
||||||
|
app = XiaoliTUI(self)
|
||||||
|
app.run()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""运行 CLI"""
|
"""运行 CLI"""
|
||||||
import uuid
|
import uuid
|
||||||
self.user_id = str(uuid.getnode())
|
self.user_id = str(uuid.getnode())
|
||||||
print(f"{Fore.GREEN}小狸 Pro-CLI 已启动!{Style.RESET_ALL}")
|
print(f"{Fore.GREEN}小狸 Pro-CLI v{VERSION} 已启动!{Style.RESET_ALL}")
|
||||||
print(f"{Fore.GREEN}用户ID: {self.user_id}{Style.RESET_ALL}")
|
print(f"{Fore.GREEN}用户ID: {self.user_id}{Style.RESET_ALL}")
|
||||||
print(f"{Fore.GREEN}输入 '/help' 查看帮助信息{Style.RESET_ALL}")
|
print(f"{Fore.GREEN}输入 '/help' 查看帮助信息{Style.RESET_ALL}")
|
||||||
print(f"{Fore.GREEN}输入 '/quit' 退出程序{Style.RESET_ALL}")
|
print(f"{Fore.GREEN}输入 '/quit' 退出程序{Style.RESET_ALL}")
|
||||||
@@ -1215,6 +1246,30 @@ multi 操作支持一次修改多处:
|
|||||||
print(f"{Fore.CYAN}已从 TUI 模式返回 CLI 模式{Style.RESET_ALL}")
|
print(f"{Fore.CYAN}已从 TUI 模式返回 CLI 模式{Style.RESET_ALL}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if user_input == '/compress':
|
||||||
|
if hasattr(self, 'memory_manager') and self.memory_manager:
|
||||||
|
engine = self.current_engine
|
||||||
|
before = len(self.shared_conversation_history)
|
||||||
|
self.shared_conversation_history = self.memory_manager.compress_context(
|
||||||
|
self.shared_conversation_history, engine=engine)
|
||||||
|
self._set_shared_conversation_history()
|
||||||
|
print(f"{Fore.CYAN}已手动压缩上下文: {before} → {len(self.shared_conversation_history)} 条{Style.RESET_ALL}")
|
||||||
|
else:
|
||||||
|
print(f"{Fore.YELLOW}记忆系统未加载,无法压缩{Style.RESET_ALL}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if user_input.startswith('/memory mode '):
|
||||||
|
mode = user_input[len('/memory mode '):].strip().lower()
|
||||||
|
if mode in ('companion', 'work'):
|
||||||
|
try:
|
||||||
|
set_system_config('memory_mode', mode)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
print(f"{Fore.GREEN}记忆模式已设为: {mode}{Style.RESET_ALL}")
|
||||||
|
else:
|
||||||
|
print(f"{Fore.RED}用法: /memory mode <companion|work>{Style.RESET_ALL}")
|
||||||
|
continue
|
||||||
|
|
||||||
if user_input.startswith('/safe'):
|
if user_input.startswith('/safe'):
|
||||||
parts = user_input.split()
|
parts = user_input.split()
|
||||||
safety = get_safety()
|
safety = get_safety()
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ VIDEO_FRAME_DELAY = 0.04 # 40毫秒
|
|||||||
LOVE_FILE_PATH = "love.txt"
|
LOVE_FILE_PATH = "love.txt"
|
||||||
DEFAULT_MAX_HISTORY = 999999 # 无限制对话历史
|
DEFAULT_MAX_HISTORY = 999999 # 无限制对话历史
|
||||||
|
|
||||||
|
# 版本号(v8.0 基于 v5.4.1 重新立项)
|
||||||
|
VERSION = "8.0.0"
|
||||||
|
VERSION_NAME = "小狸 Pro-CLI"
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import importlib.util
|
import importlib.util
|
||||||
|
|||||||
+24
-3
@@ -22,7 +22,8 @@ class MemoryManager:
|
|||||||
self.memory_dir = os.path.join(project_dir, "memory")
|
self.memory_dir = os.path.join(project_dir, "memory")
|
||||||
self.memory_file = os.path.join(project_dir, "MEMORY.md")
|
self.memory_file = os.path.join(project_dir, "MEMORY.md")
|
||||||
self.chat_dir = os.path.join(project_dir, "chat_history")
|
self.chat_dir = os.path.join(project_dir, "chat_history")
|
||||||
self.compress_threshold = 50 # 超过 50 条消息触发压缩
|
self.compress_threshold = 50 # 超过 50 条消息触发压缩(兜底)
|
||||||
|
self.compress_ratio = 0.7 # token 感知压缩阈值:used/context_window >= 0.7 触发
|
||||||
self._ensure_dirs()
|
self._ensure_dirs()
|
||||||
|
|
||||||
def _ensure_dirs(self):
|
def _ensure_dirs(self):
|
||||||
@@ -299,8 +300,28 @@ class MemoryManager:
|
|||||||
# 上下文压缩
|
# 上下文压缩
|
||||||
# ══════════════════════════════════════════════
|
# ══════════════════════════════════════════════
|
||||||
|
|
||||||
def should_compress(self, conversation: list) -> bool:
|
def set_compress_ratio(self, ratio: float):
|
||||||
"""判断是否需要压缩上下文"""
|
"""设置 token 感知压缩阈值(0~1)"""
|
||||||
|
try:
|
||||||
|
self.compress_ratio = float(ratio)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def should_compress(self, conversation: list, prompt_tokens=None, context_window=None, ratio=None) -> bool:
|
||||||
|
"""
|
||||||
|
判断是否需要压缩上下文。
|
||||||
|
优先级:
|
||||||
|
1. token 感知: prompt_tokens/context_window >= ratio 触发(OpenAI 格式可拿到真实 token 数)
|
||||||
|
2. 兜底: 消息数超过 compress_threshold
|
||||||
|
"""
|
||||||
|
if ratio is None:
|
||||||
|
ratio = self.compress_ratio
|
||||||
|
if prompt_tokens and context_window:
|
||||||
|
try:
|
||||||
|
if int(context_window) > 0 and (int(prompt_tokens) / int(context_window)) >= ratio:
|
||||||
|
return True
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
return len(conversation) > self.compress_threshold
|
return len(conversation) > self.compress_threshold
|
||||||
|
|
||||||
def compress_context(self, conversation: list, engine=None) -> list:
|
def compress_context(self, conversation: list, engine=None) -> list:
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"""
|
||||||
|
v8.0 Rich 面板式 TUI + 可截取快照出口
|
||||||
|
======================================
|
||||||
|
- 通过 cli.tui_output_callback = RichTUI.push 接收逐行输出,渲染为 rich 面板
|
||||||
|
- Console(record=True) 快照出口: export_text / export_svg / export_html
|
||||||
|
→ AI 开发 TUI 时能把渲染结果读回自验,补上“看不见渲染”的反馈闭环
|
||||||
|
|
||||||
|
设计取舍(与 7.0 重构前 textual TUI 的区别):
|
||||||
|
- 不用 textual 的异步全屏 App,避免与同步 input() 主循环冲突;
|
||||||
|
- 采用 rich 顺序面板流(一问一答往下渲染),天然支持 record=True 快照;
|
||||||
|
- 这正是“让 AI 能看见自己写的 TUI”的关键能力。
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.text import Text
|
||||||
|
from rich.box import ROUNDED
|
||||||
|
|
||||||
|
SNAP_DIR = os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".tui_snapshots"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RichTUI:
|
||||||
|
"""rich 面板式 TUI:聊天面板 + 状态栏 + 快照出口。"""
|
||||||
|
|
||||||
|
def __init__(self, cli):
|
||||||
|
self.cli = cli
|
||||||
|
# record=True 是关键:渲染过程被记录,可随时导出文本/SVG/HTML 自验
|
||||||
|
self.console = Console(record=True, width=100)
|
||||||
|
self.version = "8.0.0"
|
||||||
|
self.engine = ""
|
||||||
|
self.model = ""
|
||||||
|
self.mode = "普通"
|
||||||
|
self.used_tokens = None
|
||||||
|
self.context_window = None
|
||||||
|
self.msg_count = 0
|
||||||
|
self._log = []
|
||||||
|
|
||||||
|
# ── 状态设置(由 cli 在切换引擎/压缩后刷新)──
|
||||||
|
def set_status(self, engine=None, model=None, mode=None,
|
||||||
|
used_tokens=None, context_window=None, msg_count=None):
|
||||||
|
if engine is not None:
|
||||||
|
self.engine = engine
|
||||||
|
if model is not None:
|
||||||
|
self.model = model
|
||||||
|
if mode is not None:
|
||||||
|
self.mode = mode
|
||||||
|
if used_tokens is not None:
|
||||||
|
self.used_tokens = used_tokens
|
||||||
|
if context_window is not None:
|
||||||
|
self.context_window = context_window
|
||||||
|
if msg_count is not None:
|
||||||
|
self.msg_count = msg_count
|
||||||
|
|
||||||
|
def banner(self):
|
||||||
|
self.console.print(Panel(
|
||||||
|
Text.from_markup(f"[bold cyan]小狸 Pro-CLI[/] [yellow]v{self.version}[/] · Rich TUI"),
|
||||||
|
subtitle="[dim]自然语言对话 · /exit 返回 CLI · /snapshot 导出快照[/]",
|
||||||
|
box=ROUNDED, border_style="cyan"))
|
||||||
|
self.console.print()
|
||||||
|
|
||||||
|
def _bar(self):
|
||||||
|
if self.used_tokens and self.context_window:
|
||||||
|
ratio = min(1.0, self.used_tokens / self.context_window)
|
||||||
|
filled = int(ratio * 10)
|
||||||
|
bar = "▓" * filled + "░" * (10 - filled)
|
||||||
|
tok = f"{self.used_tokens}/{self.context_window} {bar}"
|
||||||
|
elif self.used_tokens:
|
||||||
|
tok = f"{self.used_tokens} tokens"
|
||||||
|
else:
|
||||||
|
tok = "-"
|
||||||
|
return (f"[dim]引擎 {self.engine or '-'} · 模型 {self.model or '-'} · "
|
||||||
|
f"模式 {self.mode} · {tok} · 消息 {self.msg_count}[/]")
|
||||||
|
|
||||||
|
# ── 回调入口:DisplayMixin._output 把每行输出投到这里 ──
|
||||||
|
def push(self, message):
|
||||||
|
m = (message or "").strip("\n")
|
||||||
|
if not m:
|
||||||
|
return
|
||||||
|
self._log.append(m)
|
||||||
|
if len(self._log) > 300:
|
||||||
|
self._log = self._log[-300:]
|
||||||
|
# 原始 message 已由 DisplayMixin 产出合法 rich markup,直接打印;
|
||||||
|
# 个别工具结果含裸方括号可能破坏 markup,失败则降级为纯文本。
|
||||||
|
try:
|
||||||
|
self.console.print(m)
|
||||||
|
except Exception:
|
||||||
|
self.console.print(m, markup=False)
|
||||||
|
|
||||||
|
# ── 快照出口(AI 自验渲染的关键能力)──
|
||||||
|
def snapshot_text(self):
|
||||||
|
return self.console.export_text(clear=False)
|
||||||
|
|
||||||
|
def snapshot_svg(self, title="小狸 Pro-CLI v8.0 Rich TUI"):
|
||||||
|
return self.console.export_svg(title=title, clear=False)
|
||||||
|
|
||||||
|
def snapshot_html(self, title="小狸 Pro-CLI v8.0 Rich TUI"):
|
||||||
|
# rich 新版 export_html 不接受 title 参数,标题作为注释写入
|
||||||
|
html = self.console.export_html(clear=False)
|
||||||
|
return f"<!-- {title} -->\n{html}"
|
||||||
|
|
||||||
|
def save_snapshot(self, stem):
|
||||||
|
os.makedirs(os.path.dirname(stem), exist_ok=True)
|
||||||
|
with open(stem + ".txt", "w", encoding="utf-8") as f:
|
||||||
|
f.write(self.snapshot_text())
|
||||||
|
with open(stem + ".svg", "w", encoding="utf-8") as f:
|
||||||
|
f.write(self.snapshot_svg())
|
||||||
|
with open(stem + ".html", "w", encoding="utf-8") as f:
|
||||||
|
f.write(self.snapshot_html())
|
||||||
|
return stem + ".txt", stem + ".svg", stem + ".html"
|
||||||
|
|
||||||
|
# ── 主循环 ──
|
||||||
|
def run(self):
|
||||||
|
self.banner()
|
||||||
|
try:
|
||||||
|
import readline # 可选:输入历史补全
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
ui = input("> ").strip()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
break
|
||||||
|
if ui in ("/exit", "/quit"):
|
||||||
|
break
|
||||||
|
if ui == "/snapshot":
|
||||||
|
paths = self.save_snapshot(os.path.join(SNAP_DIR, "live"))
|
||||||
|
print(f"[快照已保存] {paths[2]}")
|
||||||
|
continue
|
||||||
|
if ui:
|
||||||
|
self.cli.process_conversation(ui)
|
||||||
|
self.msg_count = len(self.cli.shared_conversation_history)
|
||||||
|
self.console.print(Text.from_markup(self._bar()))
|
||||||
Reference in new issue
Block a user