docs/en/s19-cli-and-web-interface.mdXem bản gốc Bản dịch đầy đủ theo đúng cấu trúc tài liệu; code, liên kết và định danh kỹ thuật được giữ nguyên.
s19: CLI & Giao diện web
s00 > s01 > s02 > s03 > s04 > s05 > s06 > s07 > s08 > s09 > s10 > s11 > s12 > s13 > s14 > s15 > s16 > s17 > s18 > [ s19 ] > s20 > s21 > s22 > s23 > s24 > s25 > s26 > s27
agent từ s01-s17 đã chạy trốn
input()/print()terminal. Để có trải nghiệm thực sự hướng tới người dùng, bạn cần có đầu ra phát trực tuyến, phản hồi tiến trình, lệnh gạch chéo -- và bảng điều khiển web để bạn có thể quản lý mà không cần mở terminal.

Chương này giải quyết vấn đề gì
Ba kịch bản.
Kịch bản 1: Im lặng chờ đợi. Bạn yêu cầu nhân viên cấu trúc lại một tệp. Nó gọi 5 công cụ trong 40 giây. Trong suốt thời gian đó, terminal không hiển thị gì - bạn cho rằng nó bị treo, hãy nhấn Ctrl+C và nó sắp trả về kết quả cuối cùng.
You: Refactor parser.py for me
<- 40 seconds of silence
<- User hits Ctrl+C
(all 5 tool calls from the agent are lost)
Tình huống 2: Bức tường văn bản. Bạn đặt một câu hỏi phức tạp. Mô hình tạo ra 2000 từ. CLI trần chờ 30 giây, sau đó hiển thị tất cả 2000 từ trên màn hình cùng một lúc -- mắt bạn đuổi theo 3 màn hình cuộn.
You: Explain this system's architecture
<- 30 seconds of silence
Assistant: [2000 words appear all at once, scrolling 3 screens]
Trường hợp 3: Muốn thay đổi cấu hình mà không cần mở terminal. Hôm nay là cuối tuần và bạn đang sử dụng điện thoại. Bạn muốn xem những tác vụ theo lịch trình mà agent đã chạy ngày hôm qua hoặc thay đổi khóa API. Nhưng agent chỉ có điểm vào CLI -- bạn cần SSH vào máy chủ.
Chương này giải quyết cả ba vấn đề:
- terminal tương tác -- bộ công cụ nhắc nhở ghim khu vực đầu vào ở dưới cùng, đầu ra cuộn lên trên, các lệnh gọi công cụ hiển thị một vòng quay
- Đầu ra phát trực tuyến -- mỗi token mà mô hình tạo ra sẽ được hiển thị ngay lập tức, không cần chờ phản hồi đầy đủ
- Bảng quản lý web -- FastAPI hiển thị API REST; giao diện React xử lý cấu hình và quản lý phiên
Đề xuất đọc
s02-tool-system.md-- lệnh gọi lại tiến trình công cụ mở rộng cơ chế gọi công cụ của s02s12-gateway-architecture.md-- bảng điều khiển web về cơ bản là một bộ chuyển đổi khács15-scheduled-tasks.md-- bảng điều khiển web có thể quản lý các tác vụ đã lên lịch
Thuật ngữ chính
patch_stdout là gì
Thủ thuật cốt lõi trong nhắc_toolkit. Thông thường, print() viết văn bản ở vị trí hiện tại của con trỏ -- nếu vùng nhập của bạn ở dưới cùng, print() sẽ làm gián đoạn nó.
patch_stdout() chặn tất cả print() gọi và chuyển hướng đầu ra đến vùng cuộn phía trên đầu vào. Khu vực nhập liệu không bao giờ di chuyển; đầu ra cuộn lên trên.
+----------------------------------+
| [tool] terminal: ls -la 0.3s | <- Output area (scrolls up)
| [tool] read_file: src/... 0.1s |
| Assistant: This directory has...|
| |
+----------------------------------+
| You: _ | <- Input area (pinned at bottom)
+----------------------------------+
Stream_delta là gì
Ở chế độ phát trực tuyến, API mô hình không đợi cho đến khi quá trình tạo hoàn tất trước khi quay lại. Thay vào đó, nó sẽ gửi một delta (đoạn văn bản gia tăng) thông qua lệnh gọi lại cho mỗi token được tạo.
stream_delta_callback là một chức năng mà CLI đăng ký trên agent -- mỗi khi có một delta xuất hiện, CLI sẽ hiển thị nó trên màn hình ngay lập tức.
Lệnh gạch chéo là gì
Văn bản được người dùng nhập bắt đầu bằng / không được gửi đến mô hình; thay vào đó, CLI xử lý nó cục bộ. Ví dụ:
/help-- hiển thị trợ giúp/new-- bắt đầu một phiên mới/model-- chuyển đổi mô hình/tools-- quản lý bộ công cụ
Các lệnh gạch chéo được xác định thông qua sổ đăng ký trung tâm COMMAND_REGISTRY, với sự hỗ trợ cho bí danh và hoàn thành Tab.
Mô hình tinh thần tối thiểu
User
|
+-- Terminal input
| |
| v
| HermesCLI
| +-- Slash command? -> process_command() handles locally
| +-- Regular message? -> run_conversation()
| |
| | stream_delta_callback(token)
| v
| _stream_delta()
| | line buffer -> _cprint() -> patch_stdout -> screen
| |
| _on_tool_progress(event)
| | spinner update / persistent line
|
+-- Browser
|
v
FastAPI (/api/sessions, /api/config, /api/cron, ...)
|
+-- Reads and writes the same SQLite / config.yaml / jobs.json
CLI và bảng điều khiển web không phải là hai hệ thống độc lập. Chúng hoạt động trên cùng một dữ liệu -- các phiên trong SQLite, cấu hình trong config.yaml và các tác vụ được lên lịch trong jobs.json.
Phần 1: terminal tương tác
Từ CLI trần đến TUI
các run_cli() từ s01-s17 trông như thế này:
while True:
user_input = input("You: ") # Blocks, cannot show progress simultaneously
result = run_conversation(...) # Waits for full completion before returning
print(f"Assistant: {result}") # Dumps everything at once
Tất cả ba vấn đề đều có thể nhìn thấy được trong ba dòng này.
Cách tiếp cận của HermesCLI:
class HermesCLI:
def __init__(self, model=None, toolsets=None, ...):
self.streaming_enabled = config["display"].get("streaming", False)
self._pending_input = queue.Queue() # User input queue
self._interrupt_queue = queue.Queue() # Interrupt queue during agent runs
def run(self):
with patch_stdout(): # <- all print output redirected above input area
app = Application(
layout=Layout(HSplit([
# Output area (auto-scrolling)
# spinner status line
# separator
# Input area (pinned at bottom)
])),
key_bindings=self._build_key_bindings(),
)
app.run()
patch_stdout() là chìa khóa. Với nó, thông tin tiến độ print()-ed bởi agent trong khi nó chạy, các công cụ trong nền sẽ xuất hiện phía trên khu vực nhập mà không làm gián đoạn văn bản bạn đang nhập.
Máy trạng thái định tuyến đầu vào
Khi người dùng nhấn Enter, đầu vào không nhất thiết phải chuyển đến mô hình -- nó có thể trả lời lời nhắc mật khẩu sudo, phê duyệt một lệnh nguy hiểm hoặc chọn một tùy chọn.
User presses Enter
|
+-- sudo_state active? -> Password goes to sudo queue
+-- approval_state active? -> Choice goes to approval queue
+-- clarify_state active? -> Answer goes to selection queue
+-- Agent currently running?
| +-- Input is a slash command? -> Execute immediately
| +-- Regular text? -> Put in interrupt_queue (inserted in next turn)
+-- Idle state -> Put in pending_input queue
Mỗi trạng thái phương thức có hàng đợi riêng. HermesCLI của run() vòng lặp kéo tin nhắn từ _pending_input để xử lý; các công cụ đặt trạng thái tương ứng và chờ trên hàng đợi tương ứng khi chúng cần tương tác.
Kịch bản: agent sắp chạy rm -rf /tmp/build
1. The terminal tool detects a dangerous command (s09)
2. Sets _approval_state, displays the approval panel
3. The user sees the panel: "Allow rm -rf /tmp/build? [y/n]"
4. The user types y + Enter -> routed to the approval queue
5. The terminal tool receives the approval result and continues execution
6. _approval_state is cleared, input routing returns to normal
Lệnh gọi lại tiến trình công cụ
Mỗi khi agent gọi một công cụ, CLI sẽ nhận được hai sự kiện:
def _on_tool_progress(self, event_type, function_name, preview,
function_args, duration, is_error):
if event_type == "tool.started":
# Update spinner: show tool name and argument summary
# "... terminal: pip install numpy"
pass
elif event_type == "tool.completed":
# Print a persistent line to the scrolling area
# " [tool] terminal: pip install numpy 2.3s"
pass
Công cụ quay vòng cập nhật theo thời gian thực -- nếu nhân viên gọi 3 công cụ, bạn sẽ thấy công cụ quay vòng thay đổi từ terminal để read_file để write_file. Mỗi công cụ hoàn thiện sẽ trở thành một đường cố định trong vùng cuộn.
Bốn chế độ hiển thị (được định cấu hình qua display.tool_progress):
| Chế độ | Hành vi |
|---|---|
off | Không hiển thị tiến trình công cụ |
new | Chỉ hiển thị mỗi công cụ lần đầu tiên (bỏ qua các bản sao liên tiếp) |
all | Hiển thị mọi cuộc gọi |
verbose | Hiển thị + thông tin gỡ lỗi bổ sung |
Phần 2: Đầu ra phát trực tuyến
Đăng ký gọi lại
Cốt lõi của đầu ra phát trực tuyến là chuỗi gọi lại:
Model API (streaming mode)
| token
v
AIAgent._fire_stream_delta(text)
|
+-- stream_delta_callback -> HermesCLI._stream_delta(text)
+-- _stream_callback -> (TTS and other consumers)
Việc đăng ký diễn ra khi agent được tạo:
# At CLI startup
agent = AIAgent(
stream_delta_callback=self._stream_delta if self.streaming_enabled else None,
)
Nếu streaming_enabled=False, không có cuộc gọi lại nào được thông qua và agent chờ quá trình tạo hoàn chỉnh trước khi trả về toàn bộ văn bản.
Bộ đệm dòng
Delta được mô hình trả về là các đoạn văn bản có độ dài tùy ý -- có thể là nửa ký tự, một từ hoặc vài dòng. Hiển thị trực tiếp một nửa ký tự sẽ khiến terminal bị nhấp nháy.
_stream_delta thực hiện đệm dòng: nó tích lũy văn bản cho đến khi một dòng hoàn chỉnh (gặp \n) trước khi kết xuất.
def _stream_delta(self, text):
# None = turn boundary signal (tool call ended)
if text is None:
self._flush_stream() # Flush the buffer
self._reset_stream_state()
return
self._buffer += text
while "\n" in self._buffer:
line, self._buffer = self._buffer.split("\n", 1)
self._cprint(line) # Render above the input area via patch_stdout
Rẽ ranh giới
agent có thể gọi nhiều công cụ trong một cuộc trò chuyện. Giữa các cuộc gọi công cụ, agent sẽ gửi None như một tín hiệu biên:
Model generates text -> delta("Let me check the file first")
Model calls a tool -> delta(None) <- turn boundary
Tool execution completes
Model continues -> delta("The file contains...")
Model calls a tool -> delta(None) <- turn boundary
Tool execution completes
Model final reply -> delta("Done with the changes,...")
Khi CLI nhận được None: nó đóng hộp đầu ra hiện tại, xóa bộ đệm và chuẩn bị hiển thị tiến trình chạy dao của lượt tiếp theo.
Phần 3: Hệ thống lệnh gạch chéo
Cơ quan đăng ký trung tâm
Tất cả các lệnh được xác định trong một danh sách duy nhất:
@dataclass
class CommandDef:
name: str # Canonical name: "background"
description: str # Help text
category: str # "Session" / "Configuration" / "Tools & Skills"
aliases: tuple = () # Short names: ("bg",)
args_hint: str = "" # Argument placeholder: "<prompt>"
cli_only: bool = False # Only available in CLI
gateway_only: bool = False
COMMAND_REGISTRY = [
CommandDef("new", "Start a new session", "Session", aliases=("reset",)),
CommandDef("clear", "Clear screen and start new session", "Session", cli_only=True),
CommandDef("history", "Show conversation history", "Session", cli_only=True),
CommandDef("model", "Switch model or provider", "Configuration", args_hint="[name]"),
CommandDef("tools", "Manage toolsets", "Tools & Skills", args_hint="[list|enable|disable]"),
CommandDef("background", "Run prompt in background", "Session", aliases=("bg",), args_hint="<prompt>"),
CommandDef("quit", "Exit", "Exit", cli_only=True, aliases=("exit",)),
# ... 30+ commands
]
công văn
def process_command(self, command: str) -> bool:
cmd_word = command.split()[0].lstrip("/").lower()
resolved = resolve_command(cmd_word) # Look up registry, resolve aliases
if resolved.name == "quit":
return False
elif resolved.name == "help":
self._show_help()
elif resolved.name == "model":
self._handle_model_command(command)
elif resolved.name == "tools":
self._handle_tools_command(command)
# ...
return True
Tại sao không sử dụng kết hợp if-elif trên chuỗi thô? Vì các lệnh có bí danh. /bg và /background là cùng một lệnh. Cơ quan đăng ký xử lý việc phân giải bí danh một cách tập trung và logic điều phối chỉ nhận dạng các tên chuẩn.
Tại sao cli_only và gateway_only? Bởi vì /clear (xóa màn hình) là vô nghĩa trên Telegram và /approve (phê duyệt) không cần thiết trong CLI (CLI sẽ bật lên trực tiếp bảng tương tác).
Phần 4: Bảng quản lý Web
Không phải giao diện trò chuyện
Bảng điều khiển web không dành cho trò chuyện với nhân viên hỗ trợ trong trình duyệt. Đó là bảng điều khiển của tổng đài viên -- để xem lịch sử phiên, sửa đổi cấu hình và quản lý các tác vụ đã lên lịch.
Tại sao không có trò chuyện? Bởi vì trải nghiệm tương tác của CLI (hoàn thành tab, hoàn thành đường dẫn tệp, bảng sudo, khác biệt nội tuyến) không thể được sao chép một cách trung thực trong trình duyệt. Web xử lý việc quản lý; CLI xử lý sự tương tác.
Phần phụ trợ FastAPI
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI(title="Hermes Agent")
# CORS allows only localhost -- the web panel is for local use only
app.add_middleware(
CORSMiddleware,
allow_origin_regex=r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$",
allow_methods=["*"],
allow_headers=["*"],
)
Bảo mật token tạm thời
Trình duyệt và chương trình phụ trợ nằm trên cùng một máy, nhưng bạn vẫn cần ngăn JavaScript từ các trang web khác bí mật gọi API (CSRF) của bạn.
import secrets, hmac
_SESSION_TOKEN = secrets.token_urlsafe(32) # Generated at startup
@app.middleware("http")
async def auth_middleware(request, call_next):
if request.url.path.startswith("/api/") and path not in _PUBLIC_PATHS:
auth = request.headers.get("authorization", "")
expected = f"Bearer {_SESSION_TOKEN}"
if not hmac.compare_digest(auth.encode(), expected.encode()):
return JSONResponse(status_code=401, detail="Unauthorized")
return await call_next(request)
token được tạo khi khởi động máy chủ và được đưa vào HTML của SPA thông qua biến mẫu. Nó thay đổi mỗi lần khởi động lại -- không tồn tại lâu dài, không có nguy cơ rò rỉ.
hmac.compare_digest ngăn chặn các cuộc tấn công về thời gian: ngay cả khi kẻ tấn công có thể đo chính xác thời gian phản hồi, chúng không thể đoán từng byte token.
API cốt lõi
GET /api/status <- Public: version, running state
GET /api/sessions?limit=20 <- Session list (paginated)
GET /api/sessions/search?q=... <- Full-text search (FTS5)
GET /api/config <- Read configuration
PUT /api/config <- Write configuration
GET /api/cron/jobs <- Scheduled task list
POST /api/cron/jobs <- Create a scheduled task
GET /api/env <- Environment variable list
PUT /api/env <- Set an environment variable
GET /api/tools/toolsets <- Toolset list
GET /api/skills <- Skill list
Kịch bản: Thay đổi khóa API từ điện thoại của bạn vào cuối tuần
1. Open http://your-server:8080 in the phone browser
2. The SPA loads, automatically carrying the injected session token
3. Tap the "Env" tab
4. Find OPENAI_API_KEY, tap "Edit"
5. PUT /api/env {"key": "OPENAI_API_KEY", "value": "sk-new-xxx"}
6. The backend writes to the .env file
7. The next time the agent starts, it automatically reads the new key
Bảng điều khiển web liên quan như thế nào đến các điểm vào khác
+-- CLI (prompt_toolkit)
| Interactive chat, streaming output, slash commands
|
User ----------+-- Web panel (FastAPI + React)
| Config management, session browsing, scheduled tasks, logs
|
+-- Gateway adapters (Telegram / Discord / ...)
Multi-platform messaging
All entry points share the same data:
SQLite / config.yaml / jobs.json / skills/
Bảng điều khiển web về cơ bản là một "bộ chuyển đổi" khác -- ngoại trừ việc nó không đi qua vòng hội thoại của tổng đài viên; thay vào đó, nó hoạt động trực tiếp trên dữ liệu cơ bản.
Điều gì đã thay đổi (s17 -> s19)
| Thành phần | s17 | s19 |
|----------||------|------|
| Đầu vào CLI | input() chặn | khu vực đầu vào được ghim của bộ công cụ nhắc nhở |
| Đầu ra CLI | print() tất cả cùng một lúc | patch_stdout() + kết xuất trực tuyến |
| Tiến trình công cụ | Không có phản hồi | Máy quay + _on_tool_progress gọi lại |
| Đầu ra mô hình | Chờ trả lời đầy đủ | stream_delta_callback hiển thị token theo token |
| Hệ thống chỉ huy | Không có | COMMAND_REGISTRY + bí danh + Hoàn thành tab |
| Tương tác phương thức | Không có | sudo / bảng phê duyệt / lựa chọn (theo hàng đợi) |
| Giao diện web | Không có | API FastAPI REST + React SPA |
| An ninh | Không có | token phù du + CORS chỉ dành cho localhost |
Những lỗi thường gặp của người mới bắt đầu
1. Sử dụng in trực tiếp không cần patch_stdout
# Wrong: print disrupts the input area
print("Tool execution complete")
Đang gọi print() trực tiếp bên trong ứng dụng nhắc_toolkit ghi đầu ra vào giữa vùng đầu vào, phá vỡ bố cục.
Khắc phục: Tất cả đầu ra phải được thông qua print() bên trong một patch_stdout() ngữ cảnh hoặc sử dụng của nhắc_toolkit print_formatted_text().
2. Thực hiện công việc tốn kém trong việc gọi lại luồng
def _stream_delta(self, text):
# Wrong: file I/O in the callback
with open("log.txt", "a") as f:
f.write(text)
self._render(text)
_stream_delta được gọi trên luồng trả về I/O của mô hình. Các hoạt động tốn kém chặn việc tiếp nhận token tiếp theo, khiến đầu ra phát trực tuyến bị gián đoạn.
Khắc phục: Cuộc gọi lại chỉ hiển thị. Việc ghi nhật ký sẽ được đưa vào hàng đợi để xử lý không đồng bộ.
3. Quên các hạn chế CORS trên API web
# Wrong: allow all origins
app.add_middleware(CORSMiddleware, allow_origins=["*"])
API của bạn có thể đọc và ghi cấu hình và khóa API. allow_origins=["*"] có nghĩa là JavaScript của bất kỳ trang web nào cũng có thể gọi API của bạn -- việc truy cập trang độc hại có thể lấy cắp khóa của bạn.
Khắc phục: Chỉ cho phép localhost. allow_origin_regex=r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$"
4. So sánh chuỗi token, cho phép tấn công thời gian
# Wrong: ordinary string comparison
if auth_header == f"Bearer {token}":
của Python == trả về Sai ngay khi tìm thấy ký tự không khớp đầu tiên. Kẻ tấn công có thể đoán từng byte token bằng cách đo chính xác thời gian phản hồi.
Sửa chữa: hmac.compare_digest() để so sánh theo thời gian không đổi.
Phạm vi của chương này
Chương này bao gồm bốn điều:
- Bố cục TUI của bộ công cụ nhắc nhở -- khu vực đầu vào được ghim, đầu ra cuộn, bảng điều khiển phương thức
- Đầu ra phát trực tuyến -- gọi lại luồng_delta, lưu vào bộ đệm dòng, chuyển ranh giới
- Hệ thống lệnh gạch chéo -- sổ đăng ký trung tâm, phân giải bí danh, gửi đi
- Bảng quản lý web -- FastAPI + token tạm thời + API REST
Không được bảo hiểm:
- Công cụ giao diện / chuyển đổi chủ đề -> mang tính thẩm mỹ, không phải cơ chế cốt lõi
- Triển khai thành phần React -> hướng dẫn phát triển frontend
- Tích hợp OAuth -> nối dây sản xuất
- Giao diện người dùng chế độ giọng nói -> được đề cập trong s18
- Chi tiết định dạng thư viện phong phú -> tham khảo tài liệu
Chương này liên quan đến những chương khác như thế nào
- s01
run_conversation()-> HermesCLI kết thúc nó, thêm các cuộc gọi lại phát trực tuyến và tiến trình - Hệ thống công cụ của s02 -> lệnh gọi lại tiến trình công cụ mở rộng công văn
- Hệ thống cấp phép của s09 -> bảng phê duyệt kết nối với CLI thông qua máy trạng thái phương thức
- Hệ thống cấu hình của s11 -> bảng điều khiển web đọc và ghi config.yaml tương tự
- s12's Gateway -> bảng điều khiển web và bộ điều hợp Cổng có chung một triết lý: các điểm vào khác nhau, cùng một dữ liệu
- Nhiệm vụ theo lịch trình của s15 -> bảng điều khiển web cung cấp API cron để quản lý jobs.json
Sau khi kết thúc chương này, bạn sẽ có thể trả lời
- Tại sao vậy
patch_stdout()nền tảng cho TUI của bộ công cụ nhắc nhở? Điều gì xảy ra nếu không có nó? - Khi đó nó có ý nghĩa gì
_stream_deltanhận đượcNone? Tại sao tín hiệu này lại cần thiết? - Tại sao gửi lệnh gạch chéo thông qua sổ đăng ký thay vì khớp chuỗi thô với if-elif?
- Tại sao bảng điều khiển web không có chức năng trò chuyện? Mối quan hệ của nó với CLI là gì?
- Tại sao phải so sánh token tạm thời với
hmac.compare_digest? Có gì sai với đơn giản==?
Một câu cần nhớ: CLI sử dụng quick_toolkit để ghim vùng đầu vào và hiển thị đầu ra dưới dạng luồng; bảng điều khiển web sử dụng FastAPI để hiển thị các API quản lý. Cả hai đều hoạt động trên cùng một dữ liệu, với các vai trò khác nhau -- CLI để tương tác, web để quản lý.