ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
@@ -0,0 +1,219 @@
"""实验 5-11:对话式界面定制系统 —— FastAPI 后端。
一个最小的 chatbot 后端:前端把用户消息 POST 到 /api/chat,后端返回回复。
开发模式下用 `uvicorn main:app --reload` 启动,改动后端代码会自动 reload
(对应书中所说的"FastAPI 的热加载")。
两种回复模式(默认保持"回声式"占位,聚焦 UI 定制这一主题):
- **echo(默认)**:后端把用户消息原样回显,无需任何模型 Key,开箱即用;
- **llm(可选)**:设置模型后走真实 LLM 对话,让运行起来的 chatbot 真会说话。
通过命令行 `--model` 或环境变量 `CHAT_MODEL` 打开,复用与 agent.py 相同的
OPENAI_API_KEY / OPENAI_BASE_URL 配置。
启动方式(二选一,行为一致):
uvicorn main:app --reload --port 8000 # 书中示例:模块级 app + uvicorn 热加载
python main.py --reload --port 8000 # 本文件自带的命令行入口(见 --help)
"""
import os
import argparse
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
try: # dotenv 可选:让 --model 模式也能读到 .env 里的 OPENAI_API_KEY
from dotenv import load_dotenv
load_dotenv()
except Exception:
pass
app = FastAPI(title="Conversational UI Backend")
# 允许前端(Vite dev server, 5173)直接跨域访问,方便本地开发。
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
class ChatRequest(BaseModel):
message: str
def _chat_model() -> str:
"""当前回复模式:返回模型名则走真实 LLM,返回空串则为默认 echo 模式。
从环境变量 CHAT_MODEL 读取(而非模块级常量),这样即使 `--reload` 触发的
子进程重新 import 本模块,也能透过环境变量拿到命令行传入的模型设置。
"""
return (os.getenv("CHAT_MODEL") or "").strip()
def _llm_reply(message: str, model: str) -> str:
"""走真实 LLM 生成回复,复用 agent.py 同款 OPENAI_* 配置。
任何异常都降级为清晰的提示(绝不编造回复),保证前端不至于白屏。
"""
try:
from openai import OpenAI
except Exception:
return "(未安装 openai 依赖,无法启用 LLM 模式;已回退占位回复)"
api_key = os.getenv("OPENAI_API_KEY")
base_url = os.getenv("OPENAI_BASE_URL")
orkey = os.getenv("OPENROUTER_API_KEY")
# 通用 OpenRouter 兜底:无直连 key,或 gpt-5.x(直连需组织实名认证)时改走 OpenRouter。
prefer_or = bool(orkey) and (model or "").lower().startswith("gpt-5")
if prefer_or or (not api_key and orkey):
api_key, base_url = orkey, "https://openrouter.ai/api/v1"
if "/" not in model:
model = ("openai/" + model) if model.lower().startswith(("gpt-", "o1", "o3", "o4")) else "openai/gpt-5.6-luna"
if not api_key:
return "(未配置 OPENAI_API_KEY 或 OPENROUTER_API_KEY,无法启用 LLM 模式;已回退占位回复)"
client_kwargs = {"api_key": api_key, "timeout": 60.0, "max_retries": 2}
if base_url:
client_kwargs["base_url"] = base_url
try:
client = OpenAI(**client_kwargs)
resp = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": "你是一个乐于助人的中文智能助手,回答简洁友好。"},
{"role": "user", "content": message},
],
temperature=(1 if any(k in (model or "").lower()
for k in ("gpt-5", "o1", "o3", "o4", "thinking", "reasoner", "kimi-k3"))
else 0.7),
)
return resp.choices[0].message.content or "(模型返回了空回复)"
except Exception as e: # 网络/鉴权/模型名等问题都在此兜底
return f"(调用 LLM 失败:{e};已回退占位回复)"
@app.get("/api/health")
def health():
model = _chat_model()
return {"status": "ok", "mode": "llm" if model else "echo", "model": model or None}
@app.post("/api/chat")
def chat(req: ChatRequest):
"""默认回声式回复;设置 CHAT_MODEL 后走真实 LLM 对话。
本实验聚焦"对话式 UI 定制",后端逻辑刻意保持最小;
如需真实客服体验,用 `python main.py --model <模型名>` 打开 LLM 模式即可。
"""
model = _chat_model()
if model:
return {"reply": _llm_reply(req.message, model)}
return {"reply": f"我收到了你的消息:{req.message}"}
# ---------------------------------------------------------------------------
# 命令行入口:让后端既能 `uvicorn main:app --reload` 启动,也能 `python main.py`
# 启动,并通过参数控制 host/port/热加载/回复模式/日志。
# ---------------------------------------------------------------------------
def parse_args(argv=None):
parser = argparse.ArgumentParser(
prog="main.py",
description="实验 5-11:对话式界面定制系统 —— FastAPI 后端(最小 chatbot 服务)。"
"为可对话定制的前端提供 /api/chat 载体,开发模式下配合 --reload 演示后端热加载。",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--host",
default="127.0.0.1",
help="监听地址;对外可用 0.0.0.0。",
)
parser.add_argument(
"--port",
type=int,
default=8000,
help="监听端口(前端 vite.config.js 默认把 /api 代理到 8000)。",
)
reload_group = parser.add_mutually_exclusive_group()
reload_group.add_argument(
"--reload",
dest="reload",
action="store_true",
default=True,
help="开启热加载:改动后端 .py 自动重启(开发默认开启)。",
)
reload_group.add_argument(
"--no-reload",
dest="reload",
action="store_false",
help="关闭热加载(更接近生产运行)。",
)
parser.add_argument(
"--model",
default=os.getenv("CHAT_MODEL") or None,
metavar="NAME",
help="打开真实 LLM 对话并指定模型名(如 gpt-5.6-luna);"
"缺省则为默认的 echo 回声模式。也可用环境变量 CHAT_MODEL 设置。",
)
parser.add_argument(
"--log-level",
default="info",
choices=["critical", "error", "warning", "info", "debug", "trace"],
help="uvicorn 日志/输出级别。",
)
parser.add_argument(
"--print-config",
action="store_true",
help="只打印生效配置(JSON)后退出,不真正监听端口(便于无网络/无端口环境下校验)。",
)
return parser.parse_args(argv)
def main(argv=None):
import json
args = parse_args(argv)
# 把 --model 写回环境变量:这样 --reload 派生的子进程重新 import 本模块时,
# 也能通过 CHAT_MODEL 感知到 LLM 模式(子进程不共享本函数的局部状态)。
if args.model:
os.environ["CHAT_MODEL"] = args.model
else:
os.environ.pop("CHAT_MODEL", None)
config = {
"host": args.host,
"port": args.port,
"reload": args.reload,
"mode": "llm" if args.model else "echo",
"model": args.model,
"log_level": args.log_level,
}
if args.print_config:
print(json.dumps(config, ensure_ascii=False, indent=2))
return 0
import uvicorn
print(
f"启动 FastAPI 后端:http://{args.host}:{args.port}"
f" 模式={config['mode']}"
f" 热加载={'' if args.reload else ''}"
)
# 用 import string 才能在 --reload 下工作;从 backend/ 目录运行 `python main.py`。
uvicorn.run(
"main:app",
host=args.host,
port=args.port,
reload=args.reload,
log_level=args.log_level,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>对话式界面定制系统</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
@@ -0,0 +1,20 @@
{
"name": "conversational-ui-frontend",
"private": true,
"version": "0.1.0",
"type": "module",
"description": "实验 5-11:对话式界面定制系统 —— React(Vite) 前端",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
"vite": "^6.0.7"
}
}
@@ -0,0 +1,78 @@
import { useState } from "react";
// ===========================================================================
// 基础 chatbot 界面。
// 这是一个"可被自然语言定制"的最小 React 应用:
// - 标题文案、按钮文字等 UI 文本都写在这里(Agent 可按需求改文案);
// - 颜色、字体、布局等样式集中在 theme.css(Agent 可按需求改样式)。
// 用户在对话中说"把发送按钮改成蓝色 / 换成等宽字体 / 标题改成 XXX"
// Agent 会定位并修改这些源码文件,Vite HMR 让改动即时生效。
// ===========================================================================
// UI 文案(Agent 定制"文案"需求时修改这里)
const HEADER_TITLE = "智能助手";
const HEADER_SUBTITLE = "有什么可以帮你的吗?";
const SEND_BUTTON_LABEL = "发送";
export default function App() {
const [messages, setMessages] = useState([
{ role: "assistant", text: "你好!我是你的智能助手,随时为你服务。" },
]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
async function handleSend() {
const text = input.trim();
if (!text || loading) return;
setMessages((m) => [...m, { role: "user", text }]);
setInput("");
setLoading(true);
try {
const resp = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: text }),
});
const data = await resp.json();
setMessages((m) => [...m, { role: "assistant", text: data.reply }]);
} catch (e) {
setMessages((m) => [
...m,
{ role: "assistant", text: "(后端未连接,这是本地占位回复)" },
]);
} finally {
setLoading(false);
}
}
return (
<div className="app">
<header className="header">
<h1 className="header-title">{HEADER_TITLE}</h1>
<p className="header-subtitle">{HEADER_SUBTITLE}</p>
</header>
<main className="chat-window">
{messages.map((m, i) => (
<div key={i} className={`bubble bubble-${m.role}`}>
{m.text}
</div>
))}
{loading && <div className="bubble bubble-assistant">思考中</div>}
</main>
<footer className="composer">
<input
className="composer-input"
value={input}
placeholder="输入消息…"
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSend()}
/>
<button className="send-button" onClick={handleSend}>
{SEND_BUTTON_LABEL}
</button>
</footer>
</div>
);
}
@@ -0,0 +1,10 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./theme.css";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
@@ -0,0 +1,108 @@
/* ==========================================================================
主题样式表(Agent 定制"颜色 / 字体 / 布局"需求时修改这里)
为方便自然语言定制,关键视觉变量集中在 :root 里。
========================================================================== */
:root {
--color-primary: #2563eb; /* 主色(发送按钮、用户气泡)——改为蓝色 */
--color-primary-text: #ffffff; /* 主色上的文字颜色 */
--color-bg: #f5f5f5; /* 页面背景 */
--color-panel: #ffffff; /* 面板/卡片背景 */
--color-text: #1f2937; /* 正文文字 */
--font-family: system-ui, "PingFang SC", "Microsoft YaHei", sans-serif;
--radius: 12px;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--color-bg);
color: var(--color-text);
font-family: var(--font-family);
}
.app {
max-width: 640px;
margin: 0 auto;
min-height: 100vh;
display: flex;
flex-direction: column;
background: var(--color-panel);
}
.header {
padding: 20px 24px;
border-bottom: 1px solid #e5e7eb;
}
.header-title {
margin: 0;
font-size: 20px;
}
.header-subtitle {
margin: 4px 0 0;
font-size: 13px;
color: #6b7280;
}
.chat-window {
flex: 1;
padding: 20px 24px;
display: flex;
flex-direction: column;
gap: 12px;
overflow-y: auto;
}
.bubble {
max-width: 75%;
padding: 10px 14px;
border-radius: var(--radius);
line-height: 1.5;
white-space: pre-wrap;
}
.bubble-assistant {
align-self: flex-start;
background: #eef2f5;
color: var(--color-text);
}
.bubble-user {
align-self: flex-end;
background: var(--color-primary);
color: var(--color-primary-text);
}
.composer {
display: flex;
gap: 8px;
padding: 16px 24px;
border-top: 1px solid #e5e7eb;
}
.composer-input {
flex: 1;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: var(--radius);
font-size: 14px;
font-family: inherit;
}
.send-button {
padding: 10px 20px;
border: none;
border-radius: var(--radius);
background: var(--color-primary);
color: var(--color-primary-text);
font-size: 14px;
cursor: pointer;
}
.send-button:hover {
opacity: 0.9;
}
@@ -0,0 +1,15 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// Vite 开发服务器默认开启 HMR(热模块替换)。
// 当 Agent 修改 src/ 下的源码时,浏览器无需整页刷新即可即时看到界面变化。
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
// 后端 FastAPI 跑在 8000,前端把 /api 请求代理过去,避免跨域。
proxy: {
"/api": "http://127.0.0.1:55409",
},
},
});
@@ -0,0 +1,219 @@
"""实验 5-11:对话式界面定制系统 —— FastAPI 后端。
一个最小的 chatbot 后端:前端把用户消息 POST 到 /api/chat,后端返回回复。
开发模式下用 `uvicorn main:app --reload` 启动,改动后端代码会自动 reload
(对应书中所说的"FastAPI 的热加载")。
两种回复模式(默认保持"回声式"占位,聚焦 UI 定制这一主题):
- **echo(默认)**:后端把用户消息原样回显,无需任何模型 Key,开箱即用;
- **llm(可选)**:设置模型后走真实 LLM 对话,让运行起来的 chatbot 真会说话。
通过命令行 `--model` 或环境变量 `CHAT_MODEL` 打开,复用与 agent.py 相同的
OPENAI_API_KEY / OPENAI_BASE_URL 配置。
启动方式(二选一,行为一致):
uvicorn main:app --reload --port 8000 # 书中示例:模块级 app + uvicorn 热加载
python main.py --reload --port 8000 # 本文件自带的命令行入口(见 --help)
"""
import os
import argparse
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
try: # dotenv 可选:让 --model 模式也能读到 .env 里的 OPENAI_API_KEY
from dotenv import load_dotenv
load_dotenv()
except Exception:
pass
app = FastAPI(title="Conversational UI Backend")
# 允许前端(Vite dev server, 5173)直接跨域访问,方便本地开发。
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
class ChatRequest(BaseModel):
message: str
def _chat_model() -> str:
"""当前回复模式:返回模型名则走真实 LLM,返回空串则为默认 echo 模式。
从环境变量 CHAT_MODEL 读取(而非模块级常量),这样即使 `--reload` 触发的
子进程重新 import 本模块,也能透过环境变量拿到命令行传入的模型设置。
"""
return (os.getenv("CHAT_MODEL") or "").strip()
def _llm_reply(message: str, model: str) -> str:
"""走真实 LLM 生成回复,复用 agent.py 同款 OPENAI_* 配置。
任何异常都降级为清晰的提示(绝不编造回复),保证前端不至于白屏。
"""
try:
from openai import OpenAI
except Exception:
return "(未安装 openai 依赖,无法启用 LLM 模式;已回退占位回复)"
api_key = os.getenv("OPENAI_API_KEY")
base_url = os.getenv("OPENAI_BASE_URL")
orkey = os.getenv("OPENROUTER_API_KEY")
# 通用 OpenRouter 兜底:无直连 key,或 gpt-5.x(直连需组织实名认证)时改走 OpenRouter。
prefer_or = bool(orkey) and (model or "").lower().startswith("gpt-5")
if prefer_or or (not api_key and orkey):
api_key, base_url = orkey, "https://openrouter.ai/api/v1"
if "/" not in model:
model = ("openai/" + model) if model.lower().startswith(("gpt-", "o1", "o3", "o4")) else "openai/gpt-5.6-luna"
if not api_key:
return "(未配置 OPENAI_API_KEY 或 OPENROUTER_API_KEY,无法启用 LLM 模式;已回退占位回复)"
client_kwargs = {"api_key": api_key, "timeout": 60.0, "max_retries": 2}
if base_url:
client_kwargs["base_url"] = base_url
try:
client = OpenAI(**client_kwargs)
resp = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": "你是一个乐于助人的中文智能助手,回答简洁友好。"},
{"role": "user", "content": message},
],
temperature=(1 if any(k in (model or "").lower()
for k in ("gpt-5", "o1", "o3", "o4", "thinking", "reasoner", "kimi-k3"))
else 0.7),
)
return resp.choices[0].message.content or "(模型返回了空回复)"
except Exception as e: # 网络/鉴权/模型名等问题都在此兜底
return f"(调用 LLM 失败:{e};已回退占位回复)"
@app.get("/api/health")
def health():
model = _chat_model()
return {"status": "ok", "mode": "llm" if model else "echo", "model": model or None}
@app.post("/api/chat")
def chat(req: ChatRequest):
"""默认回声式回复;设置 CHAT_MODEL 后走真实 LLM 对话。
本实验聚焦"对话式 UI 定制",后端逻辑刻意保持最小;
如需真实客服体验,用 `python main.py --model <模型名>` 打开 LLM 模式即可。
"""
model = _chat_model()
if model:
return {"reply": _llm_reply(req.message, model)}
return {"reply": f"我收到了你的消息:{req.message}"}
# ---------------------------------------------------------------------------
# 命令行入口:让后端既能 `uvicorn main:app --reload` 启动,也能 `python main.py`
# 启动,并通过参数控制 host/port/热加载/回复模式/日志。
# ---------------------------------------------------------------------------
def parse_args(argv=None):
parser = argparse.ArgumentParser(
prog="main.py",
description="实验 5-11:对话式界面定制系统 —— FastAPI 后端(最小 chatbot 服务)。"
"为可对话定制的前端提供 /api/chat 载体,开发模式下配合 --reload 演示后端热加载。",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--host",
default="127.0.0.1",
help="监听地址;对外可用 0.0.0.0。",
)
parser.add_argument(
"--port",
type=int,
default=8000,
help="监听端口(前端 vite.config.js 默认把 /api 代理到 8000)。",
)
reload_group = parser.add_mutually_exclusive_group()
reload_group.add_argument(
"--reload",
dest="reload",
action="store_true",
default=True,
help="开启热加载:改动后端 .py 自动重启(开发默认开启)。",
)
reload_group.add_argument(
"--no-reload",
dest="reload",
action="store_false",
help="关闭热加载(更接近生产运行)。",
)
parser.add_argument(
"--model",
default=os.getenv("CHAT_MODEL") or None,
metavar="NAME",
help="打开真实 LLM 对话并指定模型名(如 gpt-5.6-luna);"
"缺省则为默认的 echo 回声模式。也可用环境变量 CHAT_MODEL 设置。",
)
parser.add_argument(
"--log-level",
default="info",
choices=["critical", "error", "warning", "info", "debug", "trace"],
help="uvicorn 日志/输出级别。",
)
parser.add_argument(
"--print-config",
action="store_true",
help="只打印生效配置(JSON)后退出,不真正监听端口(便于无网络/无端口环境下校验)。",
)
return parser.parse_args(argv)
def main(argv=None):
import json
args = parse_args(argv)
# 把 --model 写回环境变量:这样 --reload 派生的子进程重新 import 本模块时,
# 也能通过 CHAT_MODEL 感知到 LLM 模式(子进程不共享本函数的局部状态)。
if args.model:
os.environ["CHAT_MODEL"] = args.model
else:
os.environ.pop("CHAT_MODEL", None)
config = {
"host": args.host,
"port": args.port,
"reload": args.reload,
"mode": "llm" if args.model else "echo",
"model": args.model,
"log_level": args.log_level,
}
if args.print_config:
print(json.dumps(config, ensure_ascii=False, indent=2))
return 0
import uvicorn
print(
f"启动 FastAPI 后端:http://{args.host}:{args.port}"
f" 模式={config['mode']}"
f" 热加载={'' if args.reload else ''}"
)
# 用 import string 才能在 --reload 下工作;从 backend/ 目录运行 `python main.py`。
uvicorn.run(
"main:app",
host=args.host,
port=args.port,
reload=args.reload,
log_level=args.log_level,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>对话式界面定制系统</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
@@ -0,0 +1,20 @@
{
"name": "conversational-ui-frontend",
"private": true,
"version": "0.1.0",
"type": "module",
"description": "实验 5-11:对话式界面定制系统 —— React(Vite) 前端",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
"vite": "^6.0.7"
}
}
@@ -0,0 +1,78 @@
import { useState } from "react";
// ===========================================================================
// 基础 chatbot 界面。
// 这是一个"可被自然语言定制"的最小 React 应用:
// - 标题文案、按钮文字等 UI 文本都写在这里(Agent 可按需求改文案);
// - 颜色、字体、布局等样式集中在 theme.css(Agent 可按需求改样式)。
// 用户在对话中说"把发送按钮改成蓝色 / 换成等宽字体 / 标题改成 XXX"
// Agent 会定位并修改这些源码文件,Vite HMR 让改动即时生效。
// ===========================================================================
// UI 文案(Agent 定制"文案"需求时修改这里)
const HEADER_TITLE = "智能助手";
const HEADER_SUBTITLE = "有什么可以帮你的吗?";
const SEND_BUTTON_LABEL = "发送";
export default function App() {
const [messages, setMessages] = useState([
{ role: "assistant", text: "你好!我是你的智能助手,随时为你服务。" },
]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
async function handleSend() {
const text = input.trim();
if (!text || loading) return;
setMessages((m) => [...m, { role: "user", text }]);
setInput("");
setLoading(true);
try {
const resp = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: text }),
});
const data = await resp.json();
setMessages((m) => [...m, { role: "assistant", text: data.reply }]);
} catch (e) {
setMessages((m) => [
...m,
{ role: "assistant", text: "(后端未连接,这是本地占位回复)" },
]);
} finally {
setLoading(false);
}
}
return (
<div className="app">
<header className="header">
<h1 className="header-title">{HEADER_TITLE}</h1>
<p className="header-subtitle">{HEADER_SUBTITLE}</p>
</header>
<main className="chat-window">
{messages.map((m, i) => (
<div key={i} className={`bubble bubble-${m.role}`}>
{m.text}
</div>
))}
{loading && <div className="bubble bubble-assistant">思考中</div>}
</main>
<footer className="composer">
<input
className="composer-input"
value={input}
placeholder="输入消息…"
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSend()}
/>
<button className="send-button" onClick={handleSend}>
{SEND_BUTTON_LABEL}
</button>
</footer>
</div>
);
}
@@ -0,0 +1,10 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./theme.css";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
@@ -0,0 +1,108 @@
/* ==========================================================================
主题样式表(Agent 定制"颜色 / 字体 / 布局"需求时修改这里)
为方便自然语言定制,关键视觉变量集中在 :root 里。
========================================================================== */
:root {
--color-primary: #2563eb; /* 主色(发送按钮、用户气泡)——初始为绿色 */
--color-primary-text: #ffffff; /* 主色上的文字颜色 */
--color-bg: #f5f5f5; /* 页面背景 */
--color-panel: #ffffff; /* 面板/卡片背景 */
--color-text: #1f2937; /* 正文文字 */
--font-family: monospace; /* 将字体改为等宽字体 */
--radius: 12px;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--color-bg);
color: var(--color-text);
font-family: var(--font-family);
}
.app {
max-width: 640px;
margin: 0 auto;
min-height: 100vh;
display: flex;
flex-direction: column;
background: var(--color-panel);
}
.header {
padding: 20px 24px;
border-bottom: 1px solid #e5e7eb;
}
.header-title {
margin: 0;
font-size: 20px;
}
.header-subtitle {
margin: 4px 0 0;
font-size: 13px;
color: #6b7280;
}
.chat-window {
flex: 1;
padding: 20px 24px;
display: flex;
flex-direction: column;
gap: 12px;
overflow-y: auto;
}
.bubble {
max-width: 75%;
padding: 10px 14px;
border-radius: var(--radius);
line-height: 1.5;
white-space: pre-wrap;
}
.bubble-assistant {
align-self: flex-start;
background: #eef2f5;
color: var(--color-text);
}
.bubble-user {
align-self: flex-end;
background: var(--color-primary);
color: var(--color-primary-text);
}
.composer {
display: flex;
gap: 8px;
padding: 16px 24px;
border-top: 1px solid #e5e7eb;
}
.composer-input {
flex: 1;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: var(--radius);
font-size: 14px;
font-family: inherit;
}
.send-button {
padding: 10px 20px;
border: none;
border-radius: var(--radius);
background: var(--color-primary);
color: var(--color-primary-text);
font-size: 14px;
cursor: pointer;
}
.send-button:hover {
opacity: 0.9;
}
@@ -0,0 +1,15 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// Vite 开发服务器默认开启 HMR(热模块替换)。
// 当 Agent 修改 src/ 下的源码时,浏览器无需整页刷新即可即时看到界面变化。
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
// 后端 FastAPI 跑在 8000,前端把 /api 请求代理过去,避免跨域。
proxy: {
"/api": "http://127.0.0.1:55670",
},
},
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

@@ -0,0 +1,219 @@
"""实验 5-11:对话式界面定制系统 —— FastAPI 后端。
一个最小的 chatbot 后端:前端把用户消息 POST 到 /api/chat,后端返回回复。
开发模式下用 `uvicorn main:app --reload` 启动,改动后端代码会自动 reload
(对应书中所说的"FastAPI 的热加载")。
两种回复模式(默认保持"回声式"占位,聚焦 UI 定制这一主题):
- **echo(默认)**:后端把用户消息原样回显,无需任何模型 Key,开箱即用;
- **llm(可选)**:设置模型后走真实 LLM 对话,让运行起来的 chatbot 真会说话。
通过命令行 `--model` 或环境变量 `CHAT_MODEL` 打开,复用与 agent.py 相同的
OPENAI_API_KEY / OPENAI_BASE_URL 配置。
启动方式(二选一,行为一致):
uvicorn main:app --reload --port 8000 # 书中示例:模块级 app + uvicorn 热加载
python main.py --reload --port 8000 # 本文件自带的命令行入口(见 --help)
"""
import os
import argparse
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
try: # dotenv 可选:让 --model 模式也能读到 .env 里的 OPENAI_API_KEY
from dotenv import load_dotenv
load_dotenv()
except Exception:
pass
app = FastAPI(title="Conversational UI Backend")
# 允许前端(Vite dev server, 5173)直接跨域访问,方便本地开发。
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
class ChatRequest(BaseModel):
message: str
def _chat_model() -> str:
"""当前回复模式:返回模型名则走真实 LLM,返回空串则为默认 echo 模式。
从环境变量 CHAT_MODEL 读取(而非模块级常量),这样即使 `--reload` 触发的
子进程重新 import 本模块,也能透过环境变量拿到命令行传入的模型设置。
"""
return (os.getenv("CHAT_MODEL") or "").strip()
def _llm_reply(message: str, model: str) -> str:
"""走真实 LLM 生成回复,复用 agent.py 同款 OPENAI_* 配置。
任何异常都降级为清晰的提示(绝不编造回复),保证前端不至于白屏。
"""
try:
from openai import OpenAI
except Exception:
return "(未安装 openai 依赖,无法启用 LLM 模式;已回退占位回复)"
api_key = os.getenv("OPENAI_API_KEY")
base_url = os.getenv("OPENAI_BASE_URL")
orkey = os.getenv("OPENROUTER_API_KEY")
# 通用 OpenRouter 兜底:无直连 key,或 gpt-5.x(直连需组织实名认证)时改走 OpenRouter。
prefer_or = bool(orkey) and (model or "").lower().startswith("gpt-5")
if prefer_or or (not api_key and orkey):
api_key, base_url = orkey, "https://openrouter.ai/api/v1"
if "/" not in model:
model = ("openai/" + model) if model.lower().startswith(("gpt-", "o1", "o3", "o4")) else "openai/gpt-5.6-luna"
if not api_key:
return "(未配置 OPENAI_API_KEY 或 OPENROUTER_API_KEY,无法启用 LLM 模式;已回退占位回复)"
client_kwargs = {"api_key": api_key, "timeout": 60.0, "max_retries": 2}
if base_url:
client_kwargs["base_url"] = base_url
try:
client = OpenAI(**client_kwargs)
resp = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": "你是一个乐于助人的中文智能助手,回答简洁友好。"},
{"role": "user", "content": message},
],
temperature=(1 if any(k in (model or "").lower()
for k in ("gpt-5", "o1", "o3", "o4", "thinking", "reasoner", "kimi-k3"))
else 0.7),
)
return resp.choices[0].message.content or "(模型返回了空回复)"
except Exception as e: # 网络/鉴权/模型名等问题都在此兜底
return f"(调用 LLM 失败:{e};已回退占位回复)"
@app.get("/api/health")
def health():
model = _chat_model()
return {"status": "ok", "mode": "llm" if model else "echo", "model": model or None}
@app.post("/api/chat")
def chat(req: ChatRequest):
"""默认回声式回复;设置 CHAT_MODEL 后走真实 LLM 对话。
本实验聚焦"对话式 UI 定制",后端逻辑刻意保持最小;
如需真实客服体验,用 `python main.py --model <模型名>` 打开 LLM 模式即可。
"""
model = _chat_model()
if model:
return {"reply": _llm_reply(req.message, model)}
return {"reply": f"我收到了你的消息:{req.message}"}
# ---------------------------------------------------------------------------
# 命令行入口:让后端既能 `uvicorn main:app --reload` 启动,也能 `python main.py`
# 启动,并通过参数控制 host/port/热加载/回复模式/日志。
# ---------------------------------------------------------------------------
def parse_args(argv=None):
parser = argparse.ArgumentParser(
prog="main.py",
description="实验 5-11:对话式界面定制系统 —— FastAPI 后端(最小 chatbot 服务)。"
"为可对话定制的前端提供 /api/chat 载体,开发模式下配合 --reload 演示后端热加载。",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--host",
default="127.0.0.1",
help="监听地址;对外可用 0.0.0.0。",
)
parser.add_argument(
"--port",
type=int,
default=8000,
help="监听端口(前端 vite.config.js 默认把 /api 代理到 8000)。",
)
reload_group = parser.add_mutually_exclusive_group()
reload_group.add_argument(
"--reload",
dest="reload",
action="store_true",
default=True,
help="开启热加载:改动后端 .py 自动重启(开发默认开启)。",
)
reload_group.add_argument(
"--no-reload",
dest="reload",
action="store_false",
help="关闭热加载(更接近生产运行)。",
)
parser.add_argument(
"--model",
default=os.getenv("CHAT_MODEL") or None,
metavar="NAME",
help="打开真实 LLM 对话并指定模型名(如 gpt-5.6-luna);"
"缺省则为默认的 echo 回声模式。也可用环境变量 CHAT_MODEL 设置。",
)
parser.add_argument(
"--log-level",
default="info",
choices=["critical", "error", "warning", "info", "debug", "trace"],
help="uvicorn 日志/输出级别。",
)
parser.add_argument(
"--print-config",
action="store_true",
help="只打印生效配置(JSON)后退出,不真正监听端口(便于无网络/无端口环境下校验)。",
)
return parser.parse_args(argv)
def main(argv=None):
import json
args = parse_args(argv)
# 把 --model 写回环境变量:这样 --reload 派生的子进程重新 import 本模块时,
# 也能通过 CHAT_MODEL 感知到 LLM 模式(子进程不共享本函数的局部状态)。
if args.model:
os.environ["CHAT_MODEL"] = args.model
else:
os.environ.pop("CHAT_MODEL", None)
config = {
"host": args.host,
"port": args.port,
"reload": args.reload,
"mode": "llm" if args.model else "echo",
"model": args.model,
"log_level": args.log_level,
}
if args.print_config:
print(json.dumps(config, ensure_ascii=False, indent=2))
return 0
import uvicorn
print(
f"启动 FastAPI 后端:http://{args.host}:{args.port}"
f" 模式={config['mode']}"
f" 热加载={'' if args.reload else ''}"
)
# 用 import string 才能在 --reload 下工作;从 backend/ 目录运行 `python main.py`。
uvicorn.run(
"main:app",
host=args.host,
port=args.port,
reload=args.reload,
log_level=args.log_level,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1 @@
:root{--color-primary: #2563eb;--color-primary-text: #ffffff;--color-bg: #f5f5f5;--color-panel: #ffffff;--color-text: #1f2937;--font-family: monospace;--radius: 12px}*{box-sizing:border-box}body{margin:0;background:var(--color-bg);color:var(--color-text);font-family:var(--font-family)}.app{max-width:640px;margin:0 auto;min-height:100vh;display:flex;flex-direction:column;background:var(--color-panel)}.header{padding:20px 24px;border-bottom:1px solid #e5e7eb}.header-title{margin:0;font-size:20px}.header-subtitle{margin:4px 0 0;font-size:13px;color:#6b7280}.chat-window{flex:1;padding:20px 24px;display:flex;flex-direction:column;gap:12px;overflow-y:auto}.bubble{max-width:75%;padding:10px 14px;border-radius:var(--radius);line-height:1.5;white-space:pre-wrap}.bubble-assistant{align-self:flex-start;background:#eef2f5;color:var(--color-text)}.bubble-user{align-self:flex-end;background:var(--color-primary);color:var(--color-primary-text)}.composer{display:flex;gap:8px;padding:16px 24px;border-top:1px solid #e5e7eb}.composer-input{flex:1;padding:10px 12px;border:1px solid #d1d5db;border-radius:var(--radius);font-size:14px;font-family:inherit}.send-button{padding:10px 20px;border:none;border-radius:var(--radius);background:var(--color-primary);color:var(--color-primary-text);font-size:14px;cursor:pointer}.send-button:hover{opacity:.9}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>对话式界面定制系统</title>
<script type="module" crossorigin src="/assets/index-CvgOOKX3.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CCn-3TaK.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>对话式界面定制系统</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
@@ -0,0 +1,20 @@
{
"name": "conversational-ui-frontend",
"private": true,
"version": "0.1.0",
"type": "module",
"description": "实验 5-11:对话式界面定制系统 —— React(Vite) 前端",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
"vite": "^6.0.7"
}
}
@@ -0,0 +1,78 @@
import { useState } from "react";
// ===========================================================================
// 基础 chatbot 界面。
// 这是一个"可被自然语言定制"的最小 React 应用:
// - 标题文案、按钮文字等 UI 文本都写在这里(Agent 可按需求改文案);
// - 颜色、字体、布局等样式集中在 theme.css(Agent 可按需求改样式)。
// 用户在对话中说"把发送按钮改成蓝色 / 换成等宽字体 / 标题改成 XXX"
// Agent 会定位并修改这些源码文件,Vite HMR 让改动即时生效。
// ===========================================================================
// UI 文案(Agent 定制"文案"需求时修改这里)
const HEADER_TITLE = "我的专属客服";
const HEADER_SUBTITLE = "有什么可以帮你的吗?";
const SEND_BUTTON_LABEL = "发送";
export default function App() {
const [messages, setMessages] = useState([
{ role: "assistant", text: "你好!我是你的智能助手,随时为你服务。" },
]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
async function handleSend() {
const text = input.trim();
if (!text || loading) return;
setMessages((m) => [...m, { role: "user", text }]);
setInput("");
setLoading(true);
try {
const resp = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: text }),
});
const data = await resp.json();
setMessages((m) => [...m, { role: "assistant", text: data.reply }]);
} catch (e) {
setMessages((m) => [
...m,
{ role: "assistant", text: "(后端未连接,这是本地占位回复)" },
]);
} finally {
setLoading(false);
}
}
return (
<div className="app">
<header className="header">
<h1 className="header-title">{HEADER_TITLE}</h1>
<p className="header-subtitle">{HEADER_SUBTITLE}</p>
</header>
<main className="chat-window">
{messages.map((m, i) => (
<div key={i} className={`bubble bubble-${m.role}`}>
{m.text}
</div>
))}
{loading && <div className="bubble bubble-assistant">思考中</div>}
</main>
<footer className="composer">
<input
className="composer-input"
value={input}
placeholder="输入消息…"
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSend()}
/>
<button className="send-button" onClick={handleSend}>
{SEND_BUTTON_LABEL}
</button>
</footer>
</div>
);
}
@@ -0,0 +1,10 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./theme.css";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
@@ -0,0 +1,108 @@
/* ==========================================================================
主题样式表(Agent 定制"颜色 / 字体 / 布局"需求时修改这里)
为方便自然语言定制,关键视觉变量集中在 :root 里。
========================================================================== */
:root {
--color-primary: #2563eb; /* 主色(发送按钮、用户气泡)——改为蓝色 */
--color-primary-text: #ffffff; /* 主色上的文字颜色 */
--color-bg: #f5f5f5; /* 页面背景 */
--color-panel: #ffffff; /* 面板/卡片背景 */
--color-text: #1f2937; /* 正文文字 */
--font-family: monospace; /* 将字体改为等宽字体 */
--radius: 12px;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--color-bg);
color: var(--color-text);
font-family: var(--font-family);
}
.app {
max-width: 640px;
margin: 0 auto;
min-height: 100vh;
display: flex;
flex-direction: column;
background: var(--color-panel);
}
.header {
padding: 20px 24px;
border-bottom: 1px solid #e5e7eb;
}
.header-title {
margin: 0;
font-size: 20px;
}
.header-subtitle {
margin: 4px 0 0;
font-size: 13px;
color: #6b7280;
}
.chat-window {
flex: 1;
padding: 20px 24px;
display: flex;
flex-direction: column;
gap: 12px;
overflow-y: auto;
}
.bubble {
max-width: 75%;
padding: 10px 14px;
border-radius: var(--radius);
line-height: 1.5;
white-space: pre-wrap;
}
.bubble-assistant {
align-self: flex-start;
background: #eef2f5;
color: var(--color-text);
}
.bubble-user {
align-self: flex-end;
background: var(--color-primary);
color: var(--color-primary-text);
}
.composer {
display: flex;
gap: 8px;
padding: 16px 24px;
border-top: 1px solid #e5e7eb;
}
.composer-input {
flex: 1;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: var(--radius);
font-size: 14px;
font-family: inherit;
}
.send-button {
padding: 10px 20px;
border: none;
border-radius: var(--radius);
background: var(--color-primary);
color: var(--color-primary-text);
font-size: 14px;
cursor: pointer;
}
.send-button:hover {
opacity: 0.9;
}
@@ -0,0 +1,15 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// Vite 开发服务器默认开启 HMR(热模块替换)。
// 当 Agent 修改 src/ 下的源码时,浏览器无需整页刷新即可即时看到界面变化。
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
// 后端 FastAPI 跑在 8000,前端把 /api 请求代理过去,避免跨域。
proxy: {
"/api": "http://127.0.0.1:56044",
},
},
});
@@ -0,0 +1,6 @@
{
"returncode": 0,
"latency_s": 0.819,
"stdout": "\n> conversational-ui-frontend@0.1.0 build\n> vite build\n\nvite v6.4.3 building for production...\ntransforming...\n✓ 27 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.42 kB │ gzip: 0.31 kB\ndist/assets/index-CCn-3TaK.css 1.35 kB │ gzip: 0.56 kB\ndist/assets/index-CvgOOKX3.js 145.07 kB │ gzip: 46.92 kB\n✓ built in 405ms\n",
"stderr": ""
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

@@ -0,0 +1,211 @@
{
"schema_version": "1.0",
"experiment": "5-11",
"run_id": "20260729T212933Z-5_11-hmr",
"started_at_utc": "2026-07-29T21:29:33.901387+00:00",
"completed_at_utc": "2026-07-29T21:30:12.405005+00:00",
"provider": "ark",
"endpoint": "https://ark.cn-beijing.volces.com/api/v3",
"model": "doubao-seed-1-6-flash-250615",
"source": {
"manuscript": "book/chapter5.md#实验-5-11",
"campaign_sha256": "11972f611b1ce9cbeff086955c29e562b0e516b8b468a55d3c072ddafcf1d3dc"
},
"servers": {
"frontend": {
"command": [
"npm",
"run",
"dev",
"--",
"--host",
"127.0.0.1",
"--port",
"56043",
"--strictPort"
]
},
"backend": {
"command": [
"python",
"main.py",
"--reload",
"--port",
"56044"
],
"health": {
"status": "ok",
"mode": "echo",
"model": null
}
}
},
"rounds": [
{
"round": 1,
"requirement": "把发送按钮和用户消息气泡的主题色从绿色改成蓝色,必须使用 #2563eb。",
"kind": "color",
"expected": "rgb(37, 99, 235)",
"observed": "rgb(37, 99, 235)",
"changed_files": [
{
"path": "src/App.jsx",
"before_sha256": "87a2587c89fed419d4109b7db931e0b0177a3c907d20a0c6b08f7e3ffe161545",
"after_sha256": "87a2587c89fed419d4109b7db931e0b0177a3c907d20a0c6b08f7e3ffe161545"
},
{
"path": "src/theme.css",
"before_sha256": "499a1d01a5dfe83bb8496349a51a9a9abd8c49df94afcb7d9615920c5653ff16",
"after_sha256": "c494cac4e0c0eb23b5b7f91925cb0997ab7eceae5e3d517736b8a71b61338a3b"
}
],
"chat_state_retained": true,
"screenshot": "round-1.png"
},
{
"round": 2,
"requirement": "把整个界面的字体换成等宽字体(monospace),保留上一轮蓝色主题。",
"kind": "font",
"expected": "monospace",
"observed": "monospace",
"changed_files": [
{
"path": "src/App.jsx",
"before_sha256": "87a2587c89fed419d4109b7db931e0b0177a3c907d20a0c6b08f7e3ffe161545",
"after_sha256": "87a2587c89fed419d4109b7db931e0b0177a3c907d20a0c6b08f7e3ffe161545"
},
{
"path": "src/theme.css",
"before_sha256": "c494cac4e0c0eb23b5b7f91925cb0997ab7eceae5e3d517736b8a71b61338a3b",
"after_sha256": "ab503f9f2821a97af18c0386893bf93cba4d9da92debad5cd740e37614a1eb68"
}
],
"chat_state_retained": true,
"screenshot": "round-2.png"
},
{
"round": 3,
"requirement": "把顶部标题改成“我的专属客服”,保留前两轮的蓝色和等宽字体。",
"kind": "title",
"expected": "我的专属客服",
"observed": "我的专属客服",
"changed_files": [
{
"path": "src/App.jsx",
"before_sha256": "87a2587c89fed419d4109b7db931e0b0177a3c907d20a0c6b08f7e3ffe161545",
"after_sha256": "76af82f286e3a6affb85a7c2835e78d5c97708aa5eb9088ae5693256aac76e11"
},
{
"path": "src/theme.css",
"before_sha256": "ab503f9f2821a97af18c0386893bf93cba4d9da92debad5cd740e37614a1eb68",
"after_sha256": "4b217ab0f2c33dda062217f7090b1c0da9d3cc4192add8ca8acb72a70b649e3b"
}
],
"chat_state_retained": true,
"screenshot": "round-3.png"
}
],
"browser": {
"browser": "Chromium",
"version": "139.0.7258.5",
"vite_hmr_websockets": [
{
"event": "opened",
"url": "ws://127.0.0.1:56043/?token=ylSMD3fv1F2P"
}
],
"navigation_count_after_initial_load": 0,
"sentinel_chat_state_retained": true,
"final_title": "我的专属客服",
"final_color": "rgb(37, 99, 235)",
"final_font": "monospace"
},
"build": {
"returncode": 0,
"latency_s": 0.819,
"stdout": "\n> conversational-ui-frontend@0.1.0 build\n> vite build\n\nvite v6.4.3 building for production...\ntransforming...\n✓ 27 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.42 kB │ gzip: 0.31 kB\ndist/assets/index-CCn-3TaK.css 1.35 kB │ gzip: 0.56 kB\ndist/assets/index-CvgOOKX3.js 145.07 kB │ gzip: 46.92 kB\n✓ built in 405ms\n",
"stderr": ""
},
"usage": {
"calls": 4,
"prompt_tokens": 9157,
"completion_tokens": 6656,
"total_tokens": 15813,
"latency_s": 34.724
},
"artifacts": {
"backend.log": {
"path": "backend.log",
"sha256": "5f932b9b3bb7dfe8069dfb88d5bec4cdeacc454fbdd7472dd066999089b3c090",
"bytes": 822
},
"build.json": {
"path": "build.json",
"sha256": "bc9bd81991925315ad5a9045ec8db3d6fa025f6cf7de810770f27e3804b857a7",
"bytes": 472
},
"final.png": {
"path": "final.png",
"sha256": "06d905eaf43a7db3730ae2aebcb65976a880e1761bedf9854ceb3c6bf85ce352",
"bytes": 30534
},
"frontend.log": {
"path": "frontend.log",
"sha256": "c38d7e06f6656d8a14ab216329383fedd4feb924eee00dfbd6ac371669cdc7fb",
"bytes": 598
},
"receipts.checkpoint.json": {
"path": "receipts.checkpoint.json",
"sha256": "88664e677ef9a1665dd4b64d94163fe9d75188e2a34eedd833802f66882756c2",
"bytes": 58118
},
"receipts.json": {
"path": "receipts.json",
"sha256": "88664e677ef9a1665dd4b64d94163fe9d75188e2a34eedd833802f66882756c2",
"bytes": 58118
},
"round-1.png": {
"path": "round-1.png",
"sha256": "76c8b80499d55d5a2247ceed6064fcea3470fe516b4efa17a37fb3b3b0539c2c",
"bytes": 31747
},
"round-2.png": {
"path": "round-2.png",
"sha256": "cf092ba62997a36b21e129dc252b1dd18c37c7a8b572c4aec98d0f8a6c3d9395",
"bytes": 29210
},
"round-3.png": {
"path": "round-3.png",
"sha256": "06d905eaf43a7db3730ae2aebcb65976a880e1761bedf9854ceb3c6bf85ce352",
"bytes": 30534
},
"rounds.json": {
"path": "rounds.json",
"sha256": "253ff0223525af311c3ed8eb279d37f24cdf570b3b27e83c1fae039362335d91",
"bytes": 2369
},
"final-source/src/App.jsx": {
"path": "app/frontend/src/App.jsx",
"sha256": "76af82f286e3a6affb85a7c2835e78d5c97708aa5eb9088ae5693256aac76e11",
"bytes": 2750
},
"final-source/src/theme.css": {
"path": "app/frontend/src/theme.css",
"sha256": "4b217ab0f2c33dda062217f7090b1c0da9d3cc4192add8ca8acb72a70b649e3b",
"bytes": 2209
}
},
"acceptance_gates": {
"real_react_vite_dev_server": true,
"real_fastapi_backend_reload_mode": true,
"real_model_generated_three_sequential_edits": true,
"vite_hmr_websocket_observed": true,
"no_full_page_navigation_during_three_edits": true,
"react_chat_state_preserved_across_hmr": true,
"all_color_font_title_requests_visible": true,
"final_vite_build_passed": true,
"raw_provider_receipts_complete": true,
"rendered_browser_images_retained": true
},
"official_complete": true
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

@@ -0,0 +1,65 @@
[
{
"round": 1,
"requirement": "把发送按钮和用户消息气泡的主题色从绿色改成蓝色,必须使用 #2563eb。",
"kind": "color",
"expected": "rgb(37, 99, 235)",
"observed": "rgb(37, 99, 235)",
"changed_files": [
{
"path": "src/App.jsx",
"before_sha256": "87a2587c89fed419d4109b7db931e0b0177a3c907d20a0c6b08f7e3ffe161545",
"after_sha256": "87a2587c89fed419d4109b7db931e0b0177a3c907d20a0c6b08f7e3ffe161545"
},
{
"path": "src/theme.css",
"before_sha256": "499a1d01a5dfe83bb8496349a51a9a9abd8c49df94afcb7d9615920c5653ff16",
"after_sha256": "c494cac4e0c0eb23b5b7f91925cb0997ab7eceae5e3d517736b8a71b61338a3b"
}
],
"chat_state_retained": true,
"screenshot": "round-1.png"
},
{
"round": 2,
"requirement": "把整个界面的字体换成等宽字体(monospace),保留上一轮蓝色主题。",
"kind": "font",
"expected": "monospace",
"observed": "monospace",
"changed_files": [
{
"path": "src/App.jsx",
"before_sha256": "87a2587c89fed419d4109b7db931e0b0177a3c907d20a0c6b08f7e3ffe161545",
"after_sha256": "87a2587c89fed419d4109b7db931e0b0177a3c907d20a0c6b08f7e3ffe161545"
},
{
"path": "src/theme.css",
"before_sha256": "c494cac4e0c0eb23b5b7f91925cb0997ab7eceae5e3d517736b8a71b61338a3b",
"after_sha256": "ab503f9f2821a97af18c0386893bf93cba4d9da92debad5cd740e37614a1eb68"
}
],
"chat_state_retained": true,
"screenshot": "round-2.png"
},
{
"round": 3,
"requirement": "把顶部标题改成“我的专属客服”,保留前两轮的蓝色和等宽字体。",
"kind": "title",
"expected": "我的专属客服",
"observed": "我的专属客服",
"changed_files": [
{
"path": "src/App.jsx",
"before_sha256": "87a2587c89fed419d4109b7db931e0b0177a3c907d20a0c6b08f7e3ffe161545",
"after_sha256": "76af82f286e3a6affb85a7c2835e78d5c97708aa5eb9088ae5693256aac76e11"
},
{
"path": "src/theme.css",
"before_sha256": "ab503f9f2821a97af18c0386893bf93cba4d9da92debad5cd740e37614a1eb68",
"after_sha256": "4b217ab0f2c33dda062217f7090b1c0da9d3cc4192add8ca8acb72a70b649e3b"
}
],
"chat_state_retained": true,
"screenshot": "round-3.png"
}
]