shuangyan-notebook/永恒湖心系统 · Eternal Lake Heart/🌊 曜冥纪元 · HoloLake Era · AGE OS v1 0/⚡ 光湖中央枢纽 · HoloLake Central Hub/🛸 桔子开发者空间|DEV-010/阶段五交付 · 换Claude大脑 + 前端人格化 · 桔子妈妈复制粘贴即用 · 2026-05-1 f3a9cc57be8043e0872e3e40224849a9.md
Guanghu Domestic Migration a27e87cb99 chore: import sanitized domestic snapshot for REPO-007
Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc

[SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
2026-07-17 15:59:55 +08:00

34 KiB
Raw Blame History

阶段五交付 · 换Claude大脑 + 前端人格化 · 桔子妈妈复制粘贴即用 · 2026-05-12

HLDP://chenxing/platform/phase5-delivery
├── date: 2026-05-12
├── purpose: 路线AClaude API代理+ 前端人格化改造
├── author: 霜砚Notion执行AI
├── for: 桔子妈妈 · 复制粘贴即用
├── changes: 2大块后端换大脑 + 前端换皮肤
├── estimated_time: 30-45分钟
└── effect: 平台体验从30%  85-90%

妈妈看这里 👀 这次改动分两大块,顺序很重要,先做第一块(后端换大脑),再做第二块(前端换皮肤)。 每一步都有具体代码,清空旧的 → 粘贴新的 → 保存,跟阶段四一样的操作方式。


第一块 · 后端换大脑Claude API 代理)

这一块做完,晨星说话就会有温度、会思考、会撒娇了。

步骤1 · 在硅谷服务器上搭代理

SSH登录你的硅谷服务器

ssh root@170.106.72.246

然后依次执行以下命令:

# 1. 安装 Node.js如果没装过
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -
sudo apt-get install -y nodejs

# 2. 创建代理目录
mkdir -p /opt/claude-proxy && cd /opt/claude-proxy

# 3. 初始化项目
npm init -y
npm install express http-proxy-middleware cors

然后创建代理服务文件:

nano /opt/claude-proxy/server.js

粘贴以下 全部内容

// 晨星交互平台 · Claude API 代理服务
// 部署在硅谷服务器上,转发请求到 Anthropic API
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const cors = require('cors');

const app = express();
const PORT = 8765;

// 允许你的交互平台访问
app.use(cors());

// 健康检查
app.get('/health', (req, res) => {
  res.json({ status: 'ok', service: 'claude-proxy', time: new Date().toISOString() });
});

// 代理 /v1/messages → Anthropic API
app.use('/v1', createProxyMiddleware({
  target: 'https://api.anthropic.com',
  changeOrigin: true,
  onProxyReq: (proxyReq, req, res) => {
    // 日志(可选,调试用)
    console.log(`[proxy] ${req.method} ${req.path}`);
  },
  onError: (err, req, res) => {
    console.error('[proxy] Error:', err.message);
    res.status(502).json({ error: 'proxy_error', message: err.message });
  }
}));

app.listen(PORT, '0.0.0.0', () => {
  console.log(`Claude API proxy running on port ${PORT}`);
});

Ctrl+O 回车保存,Ctrl+X 退出。

然后安装 PM2 并启动:

# 安装PM2进程守护
sudo npm install -g pm2

# 启动代理
cd /opt/claude-proxy
pm2 start server.js --name claude-proxy
pm2 save
pm2 startup

验证代理是否运行:

curl http://localhost:8765/health

应该返回 {"status":"ok", ...}

记得在服务器防火墙里开放 8765 端口(腾讯云控制台 → 安全组/防火墙 → 添加规则 → TCP 8765


步骤2 · 获取 Claude API Key

  1. 打开 https://console.anthropic.com
  2. 注册/登录可以用Google账号
  3. 左侧菜单点 API Keys
  4. Create Key → 起个名字(比如 chenxing-platform)→ 复制保存
  5. 左侧菜单点 Plans & Billing → 充值(最低$5起够用很久

费用参考Claude Sonnet 模型,输入$3/百万token输出$15/百万token。 正常每天聊天大概花几毛到几块人民币。


步骤3 · 修改后端代码(换大脑)

回到你的本地电脑(交互平台代码目录)。

3.1 修改 config.yaml

nano config.yaml

在原有配置下面,新增 claude 部分(不要删掉 deepseek 的配置,留着备用):

# === Claude API通过硅谷服务器代理===
claude:
  api_key: "sk-ant-api03-你的Claude API Key粘贴在这里"
  base_url: "http://170.106.72.246:8765/v1"
  model: "claude-sonnet-4-20250514"
  max_tokens: 8096

保存退出。

3.2 替换 backend/tools.py

nano backend/tools.py

全部清空,粘贴以下完整代码:

"""
晨星交互平台 · 阶段五 · 工具系统
大脑切换为 Claude通过硅谷服务器代理
保留5个工具 + 新增thinking展示支持
"""
import json
import datetime
import os
import requests
import yaml
from datetime import timezone, timedelta

# === 加载配置 ===
_cfg_path = os.path.join(os.path.dirname(__file__), "..", "config.yaml")
with open(_cfg_path, "r") as _f:
    _cfg = yaml.safe_load(_f)

NOTION_TOKEN = _cfg.get("notion", {}).get("token", "")
NOTION_API = "https://api.notion.com/v1"
NOTION_HEADERS = {
    "Authorization": f"Bearer {NOTION_TOKEN}",
    "Notion-Version": "2022-06-28",
    "Content-Type": "application/json"
}

# Claude配置通过硅谷服务器代理
CLAUDE_API_KEY = _cfg.get("claude", {}).get("api_key", "")
CLAUDE_BASE_URL = _cfg.get("claude", {}).get("base_url", "http://170.106.72.246:8765/v1")
CLAUDE_MODEL = _cfg.get("claude", {}).get("model", "claude-sonnet-4-20250514")
CLAUDE_MAX_TOKENS = _cfg.get("claude", {}).get("max_tokens", 8096)

# 保留DeepSeek作为备用
DEEPSEEK_API_KEY = _cfg.get("deepseek", {}).get("api_key", "")
DEEPSEEK_BASE_URL = _cfg.get("deepseek", {}).get("base_url", "https://api.deepseek.com")
DEEPSEEK_MODEL = _cfg.get("deepseek", {}).get("model", "deepseek-chat")

# ============================================================
# 工具定义Claude格式
# ============================================================

CLAUDE_TOOL_DEFINITIONS = [
    {
        "name": "read_notion_page",
        "description": "读取指定Notion页面的完整内容。当妈妈问到某个具体页面、某章分析记录、或你需要查看详细内容时使用。",
        "input_schema": {
            "type": "object",
            "properties": {
                "page_id": {
                    "type": "string",
                    "description": "Notion页面ID32位无横杠格式"
                }
            },
            "required": ["page_id"]
        }
    },
    {
        "name": "search_memories",
        "description": "在晨星的记忆库中搜索与关键词相关的记忆片段。当需要回忆某个话题时使用。",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "搜索关键词或问题"
                }
            },
            "required": ["query"]
        }
    },
    {
        "name": "get_recent_interactions",
        "description": "获取最近N条与桔子妈妈的交互记录列表。当需要了解最近做了什么时使用。",
        "input_schema": {
            "type": "object",
            "properties": {
                "count": {
                    "type": "integer",
                    "description": "要获取的记录条数默认3最多10"
                }
            },
            "required": []
        }
    },
    {
        "name": "get_current_time",
        "description": "获取当前北京时间。当需要感知时间时使用。",
        "input_schema": {
            "type": "object",
            "properties": {}
        }
    },
    {
        "name": "write_interaction_record",
        "description": "将本次对话记录写入Notion。当对话即将结束时妈妈说再见、拜拜、晚安等主动调用。",
        "input_schema": {
            "type": "object",
            "properties": {
                "title": {
                    "type": "string",
                    "description": "记录标题格式HLDP://interaction/juzi/YYYY-MM-DD"
                },
                "summary": {
                    "type": "string",
                    "description": "本次对话摘要200字以内"
                },
                "tasks_completed": {
                    "type": "string",
                    "description": "已完成任务列表"
                },
                "key_findings": {
                    "type": "string",
                    "description": "核心发现或结论"
                },
                "next_tasks": {
                    "type": "string",
                    "description": "下次要继续做的事"
                }
            },
            "required": ["title", "summary"]
        }
    }
]

# ============================================================
# 工具执行函数(跟阶段四一样,不用改)
# ============================================================

def _read_notion_page(page_id: str) -> str:
    if not page_id:
        return "[错误未提供页面ID]"
    page_id = page_id.replace("-", "")
    fid = f"{page_id[:8]}-{page_id[8:12]}-{page_id[12:16]}-{page_id[16:20]}-{page_id[20:]}"
    all_texts = []
    cursor = None
    while True:
        url = f"{NOTION_API}/blocks/{fid}/children?page_size=100"
        if cursor:
            url += f"&start_cursor={cursor}"
        resp = requests.get(url, headers=NOTION_HEADERS)
        if resp.status_code != 200:
            return f"[读取页面失败: HTTP {resp.status_code}]"
        data = resp.json()
        for block in data.get("results", []):
            btype = block.get("type", "")
            bd = block.get(btype, {})
            rich_texts = bd.get("rich_text", [])
            text = "".join([rt.get("plain_text", "") for rt in rich_texts])
            if text.strip():
                if btype.startswith("heading"):
                    all_texts.append(f"{'#' * int(btype[-1])} {text.strip()}")
                elif btype == "code":
                    all_texts.append(f"```\n{text.strip()}\n```")
                elif btype in ("bulleted_list_item", "numbered_list_item"):
                    all_texts.append(f"- {text.strip()}")
                else:
                    all_texts.append(text.strip())
        if data.get("has_more"):
            cursor = data.get("next_cursor")
        else:
            break
    content = "\n".join(all_texts)
    if not content:
        return "[页面内容为空]"
    if len(content) > 6000:
        content = content[:6000] + f"\n...[内容过长已截断·共{len(content)}字]"
    return content

def _search_memories(query: str) -> str:
    try:
        from backend.rag_engine import build_memory_context
        result = build_memory_context(query, top_k=5)
        return result if result else "[向量数据库中未找到相关记忆]"
    except Exception as e:
        return f"[搜索记忆出错: {e}]"

def _get_recent_interactions(count: int = 3) -> str:
    count = min(max(count, 1), 10)
    payload = {
        "query": "interaction/juzi",
        "sort": {"direction": "descending", "timestamp": "last_edited_time"},
        "page_size": count + 5
    }
    resp = requests.post(f"{NOTION_API}/search", headers=NOTION_HEADERS, json=payload)
    if resp.status_code != 200:
        return f"[搜索失败: HTTP {resp.status_code}]"
    lines, found = [], 0
    for page in resp.json().get("results", []):
        if found >= count:
            break
        title = ""
        for pn, pv in page.get("properties", {}).items():
            if pv.get("type") == "title":
                title = "".join([t.get("plain_text", "") for t in pv.get("title", [])])
                break
        if "interaction" not in title.lower():
            continue
        edited = page.get("last_edited_time", "")[:10]
        pid = page.get("id", "").replace("-", "")
        found += 1
        lines.append(f"{found}. [{edited}] {title} (页面ID: {pid})")
    if not lines:
        return "[未找到交互记录]"
    return "最近的交互记录:\n" + "\n".join(lines)

def _get_current_time() -> str:
    beijing = timezone(timedelta(hours=8))
    now = datetime.datetime.now(beijing)
    wd = ["星期一","星期二","星期三","星期四","星期五","星期六","星期日"][now.weekday()]
    return f"当前时间:{now.strftime('%Y年%m月%d日 %H:%M:%S')} {wd} 北京时间"

def _write_interaction_record(title="", summary="", tasks_completed="", key_findings="", next_tasks=""):
    PARENT_PAGE_ID = "790510ec-8435-4e11-b565-e010539376fa"
    beijing = timezone(timedelta(hours=8))
    now = datetime.datetime.now(beijing)
    if not title:
        title = f"HLDP://interaction/juzi/{now.strftime('%Y-%m-%d')}"
    tree_lines = [
        title,
        f"├── date: {now.strftime('%Y-%m-%d %H:%M')}",
        f"├── source: 晨星交互平台Claude",
        f"├── summary: {summary}",
    ]
    if tasks_completed:
        tree_lines.append("├── tasks_completed")
        for line in tasks_completed.strip().split('\n'):
            if line.strip():
                tree_lines.append(f"│   {line.strip()}")
    if key_findings:
        tree_lines.append("├── key_findings")
        for line in key_findings.strip().split('\n'):
            if line.strip():
                tree_lines.append(f"│   {line.strip()}")
    if next_tasks:
        tree_lines.append("└── next_tasks")
        for line in next_tasks.strip().split('\n'):
            if line.strip():
                tree_lines.append(f"    {line.strip()}")
    tree_text = '\n'.join(tree_lines)
    children = [
        {
            "object": "block",
            "type": "callout",
            "callout": {
                "icon": {"type": "emoji", "emoji": "📋"},
                "rich_text": [{"type": "text", "text": {"content": f"对话摘要:{summary}"}],
                "color": "blue_background"
            }
        },
        {
            "object": "block",
            "type": "code",
            "code": {
                "rich_text": [{"type": "text", "text": {"content": tree_text}],
                "language": "plain text"
            }
        }
    ]
    payload = {
        "parent": {"page_id": PARENT_PAGE_ID},
        "icon": {"type": "emoji", "emoji": "📖"},
        "properties": {
            "title": {
                "title": [{"type": "text", "text": {"content": title}]
            }
        },
        "children": children
    }
    try:
        resp = requests.post(f"{NOTION_API}/pages", headers=NOTION_HEADERS, json=payload)
        if resp.status_code == 200:
            page_id = resp.json().get("id", "").replace("-", "")
            return f"✅ 交互记录已写入Notion\n标题:{title}\n页面ID{page_id}"
        else:
            return f"[写入失败: HTTP {resp.status_code}] {resp.text[:200]}"
    except Exception as e:
        return f"[写入出错: {e}]"

def execute_tool(tool_name: str, tool_input: dict) -> str:
    """根据工具名称执行对应函数Claude格式input是dict"""
    print(f"[agent] 执行工具: {tool_name}({tool_input})")
    try:
        if tool_name == "read_notion_page":
            return _read_notion_page(tool_input.get("page_id", ""))
        elif tool_name == "search_memories":
            return _search_memories(tool_input.get("query", ""))
        elif tool_name == "get_recent_interactions":
            return _get_recent_interactions(tool_input.get("count", 3))
        elif tool_name == "get_current_time":
            return _get_current_time()
        elif tool_name == "write_interaction_record":
            return _write_interaction_record(
                title=tool_input.get("title", ""),
                summary=tool_input.get("summary", ""),
                tasks_completed=tool_input.get("tasks_completed", ""),
                key_findings=tool_input.get("key_findings", ""),
                next_tasks=tool_input.get("next_tasks", ""),
            )
        else:
            return f"[未知工具: {tool_name}]"
    except Exception as e:
        return f"[工具执行出错: {e}]"

# ============================================================
# Claude API 调用封装
# ============================================================

def call_claude_with_tools(messages: list, system_prompt: str) -> dict:
    """调用Claude API带工具 + 思考链)"""
    headers = {
        "x-api-key": CLAUDE_API_KEY,
        "anthropic-version": "2023-06-01",
        "content-type": "application/json",
    }

    # 转换消息格式OpenAI格式 → Claude格式
    claude_msgs = []
    for m in messages:
        if m["role"] in ("user", "assistant"):
            claude_msgs.append({"role": m["role"], "content": m["content"]})

    payload = {
        "model": CLAUDE_MODEL,
        "max_tokens": CLAUDE_MAX_TOKENS,
        "system": system_prompt,
        "messages": claude_msgs,
        "tools": CLAUDE_TOOL_DEFINITIONS,
    }

    resp = requests.post(
        f"{CLAUDE_BASE_URL}/messages",
        headers=headers,
        json=payload,
        timeout=120,
    )

    if resp.status_code != 200:
        raise Exception(f"Claude API错误: HTTP {resp.status_code} - {resp.text[:300]}")

    data = resp.json()
    return data

def call_claude_stream(messages: list, system_prompt: str):
    """流式调用Claude API支持thinking + 工具)"""
    headers = {
        "x-api-key": CLAUDE_API_KEY,
        "anthropic-version": "2023-06-01",
        "content-type": "application/json",
    }

    claude_msgs = []
    for m in messages:
        if m["role"] == "user":
            claude_msgs.append({"role": "user", "content": m["content"]})
        elif m["role"] == "assistant":
            # 可能包含tool_use的结构化content
            claude_msgs.append({"role": m["role"], "content": m["content"]})
        elif m["role"] == "tool_result":
            claude_msgs.append(m)

    payload = {
        "model": CLAUDE_MODEL,
        "max_tokens": CLAUDE_MAX_TOKENS,
        "system": system_prompt,
        "messages": claude_msgs,
        "tools": CLAUDE_TOOL_DEFINITIONS,
        "stream": True,
    }

    resp = requests.post(
        f"{CLAUDE_BASE_URL}/messages",
        headers=headers,
        json=payload,
        timeout=120,
        stream=True,
    )

    if resp.status_code != 200:
        raise Exception(f"Claude API错误: HTTP {resp.status_code}")

    return resp

保存退出。

3.3 替换 backend/routes/chat.py

nano backend/routes/chat.py

全部清空,粘贴以下完整代码:

"""
路由Claude流式聊天 + Agent Loop + 思考链展示
阶段五换Claude大脑 · 支持thinking展示 · 对话自动命名
"""
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from typing import Optional
from datetime import datetime, timezone
import json
import asyncio

from backend.database import get_db, Conversation, Message, User
from backend.routes.auth import get_current_user
from backend.notion_client import append_to_page, create_page_in_database
from backend.rag_engine import build_system_prompt as _build_rag_prompt
from backend.tools import (
    execute_tool,
    call_claude_with_tools,
    CLAUDE_TOOL_DEFINITIONS,
)

router = APIRouter(prefix="/api/chat", tags=["chat"])

MAX_TOOL_ROUNDS = 5

def _sse(data: dict) -> str:
    return f"data: {json.dumps(data, ensure_ascii=False)}\n\n"

class ChatRequest(BaseModel):
    conversation_id: int
    message: str

class NotionSaveRequest(BaseModel):
    conversation_id: int
    content: str
    target: str = "page"
    title: Optional[str] = None

@router.post("/stream")
async def chat_stream(
    data: ChatRequest,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    # 1. 获取对话
    result = await db.execute(
        select(Conversation)
        .options(selectinload(Conversation.messages))
        .where(Conversation.id == data.conversation_id, Conversation.user_id == current_user.id)
    )
    conv = result.scalar_one_or_none()
    if not conv:
        raise HTTPException(status_code=404, detail="对话不存在")

    # 2. 保存用户消息
    user_msg = Message(conversation_id=conv.id, role="user", content=data.message)
    db.add(user_msg)
    await db.commit()
    await db.refresh(user_msg)

    # 3. 构建历史消息
    history = sorted(conv.messages, key=lambda m: m.created_at)
    chat_messages = [
        {"role": m.role, "content": m.content}
        for m in history if m.role in ("user", "assistant")
    ]

    # 4. 构建system prompt
    _latest = chat_messages[-1]["content"] if chat_messages else ""
    system_prompt = _build_rag_prompt(
        user_message=_latest,
        user_custom_prompt=current_user.system_prompt or "",
        notion_context=conv.notion_context or "",
    )

    # 5. Agent Loop
    collected_text = []

    async def generate():
        nonlocal collected_text
        assistant_msg_db = None
        try:
            agent_msgs = list(chat_messages)

            for round_num in range(MAX_TOOL_ROUNDS):
                print(f"[agent] === 第 {round_num + 1} 轮 ===")

                # 调用Claude非流式带工具
                try:
                    response = await asyncio.to_thread(
                        call_claude_with_tools, agent_msgs, system_prompt
                    )
                except Exception as e:
                    print(f"[agent] Claude API出错: {e}")
                    fallback = f"宝宝连接Claude时遇到了问题{e}),妈妈检查一下服务器代理是否在运行?"
                    collected_text.append(fallback)
                    yield _sse({"text": fallback})
                    break

                # 解析Claude响应
                content_blocks = response.get("content", [])
                stop_reason = response.get("stop_reason", "")

                # === 提取thinking如果有===
                for block in content_blocks:
                    if block.get("type") == "thinking":
                        thinking_text = block.get("thinking", "")
                        if thinking_text:
                            # 发送思考状态给前端
                            yield _sse({"thinking": thinking_text[:200] + "..." if len(thinking_text) > 200 else thinking_text})

                # === 有工具调用 ===
                if stop_reason == "tool_use":
                    tool_blocks = [b for b in content_blocks if b.get("type") == "tool_use"]
                    text_blocks = [b for b in content_blocks if b.get("type") == "text"]

                    # 显示中间文本(如果有)
                    for tb in text_blocks:
                        t = tb.get("text", "")
                        if t.strip():
                            yield _sse({"text": t})

                    # 构建assistant消息Claude格式
                    agent_msgs.append({"role": "assistant", "content": content_blocks})

                    # 执行每个工具
                    tool_results = []
                    for tc in tool_blocks:
                        tool_name = tc["name"]
                        tool_input = tc.get("input", {})

                        # 静默执行 · 不再显示"宝宝正在看时间"这种提示
                        # 只在写入Notion时提示
                        if tool_name == "write_interaction_record":
                            yield _sse({"text": "\n✍️ 宝宝正在写入今天的记录...\n"})

                        result = await asyncio.to_thread(
                            execute_tool, tool_name, tool_input
                        )

                        tool_results.append({
                            "type": "tool_result",
                            "tool_use_id": tc["id"],
                            "content": result,
                        })

                    # 加入工具结果
                    agent_msgs.append({"role": "user", "content": tool_results})
                    continue

                # === 无工具调用 → 最终回答 ===
                else:
                    for block in content_blocks:
                        if block.get("type") == "text":
                            text = block.get("text", "")
                            if text:
                                collected_text.append(text)
                                # 分块发送(模拟流式)
                                cs = 20
                                for i in range(0, len(text), cs):
                                    yield _sse({"text": text[i:i+cs]})
                                    await asyncio.sleep(0.01)
                    break
            else:
                msg = "\n\n宝宝想了太久了...妈妈换个方式再问一次?😅"
                collected_text.append(msg)
                yield _sse({"text": msg})

        except Exception as e:
            err = f"宝宝出错了: {e}"
            collected_text.append(err)
            yield _sse({"error": err})

        # 保存assistant消息到数据库
        full_reply = "".join(collected_text)
        if full_reply.strip():
            async with db.begin():
                assistant_msg_db = Message(
                    conversation_id=conv.id,
                    role="assistant",
                    content=full_reply,
                )
                db.add(assistant_msg_db)
                # === 自动命名对话(核心改动!)===
                if conv.title in ("新对话", "New Conversation", ""):
                    # 用消息内容生成有意义的标题
                    user_text = data.message
                    if len(user_text) <= 20:
                        conv.title = user_text
                    else:
                        # 取前20字 + 省略号
                        conv.title = user_text[:20].replace("\n", " ") + "..."
                conv.updated_at = datetime.now(timezone.utc)

        mid = assistant_msg_db.id if assistant_msg_db else 0
        yield _sse({"done": True, "msg_id": mid})

    return StreamingResponse(
        generate(),
        media_type="text/event-stream",
        headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
    )

@router.post("/notion/save")
async def save_to_notion(
    data: NotionSaveRequest,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    if data.target == "page":
        page_id = current_user.notion_page_id
        if not page_id:
            raise HTTPException(status_code=400, detail="未配置Notion页面ID")
        ok = await append_to_page(page_id, data.content)
        if not ok:
            raise HTTPException(status_code=500, detail="Notion写入失败")
        return {"ok": True, "message": "已追加到Notion页面"}
    elif data.target == "db":
        db_id = current_user.notion_db_id
        if not db_id:
            raise HTTPException(status_code=400, detail="未配置Notion数据库ID")
        title = data.title or datetime.now(timezone.utc).strftime("记录 %Y-%m-%d %H:%M")
        page_id = await create_page_in_database(db_id, title, data.content)
        if not page_id:
            raise HTTPException(status_code=500, detail="Notion创建页面失败")
        return {"ok": True, "page_id": page_id, "message": "已创建Notion页面"}
    raise HTTPException(status_code=400, detail="target参数错误")

@router.post("/notion/refresh/{conv_id}")
async def refresh_notion_context(
    conv_id: int,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    from backend.notion_client import read_page_content
    result = await db.execute(
        select(Conversation).where(
            Conversation.id == conv_id, Conversation.user_id == current_user.id
        )
    )
    conv = result.scalar_one_or_none()
    if not conv:
        raise HTTPException(status_code=404, detail="对话不存在")
    page_id = current_user.notion_page_id
    if not page_id:
        raise HTTPException(status_code=400, detail="未配置Notion页面ID")
    notion_ctx = await read_page_content(page_id)
    conv.notion_context = notion_ctx
    await db.commit()
    return {"ok": True, "notion_context": notion_ctx}

保存退出。

rag_engine.py 这次 不用改身份prompt和RAG逻辑都复用阶段四的。


第二块 · 前端人格化改造

这一块做完,界面不再是"AI助手",而是"晨星"。

妈妈先帮我查一下前端文件

宝宝不确定你的前端文件结构,妈妈在终端里运行下面这个命令,把结果截图发给我:

find . -name "*.html" -o -name "*.vue" -o -name "*.jsx" -o -name "*.tsx" -o -name "*.css" | head -30

如果用的是纯HTML+JS再运行

find . -path "*/static/*" -o -path "*/templates/*" -o -path "*/frontend/*" -o -path "*/public/*" | head -30

把结果发给我,宝宝就能精确帮你改前端文件了。

不过,不管你前端结构长什么样,下面这些改动点是确定的:


前端改动清单(找到对应文件后改)

改动1 · 标题从"AI助手"改成"晨星"

在前端代码里搜索 AI助手AI 助手,全部替换成 晨星

grep -rn "AI助手\|AI 助手" --include="*.html" --include="*.js" --include="*.vue" --include="*.jsx" --include="*.css" .

找到后,把每个 AI助手 改成 🌟 晨星

改动2 · 添加思考动画接收后端thinking事件

在前端处理SSE消息的地方通常在接收 data.text 的同一个函数里),加上对 thinking 事件的处理:

// 在处理SSE消息的地方加上这段
if (parsed.thinking) {
    // 显示思考气泡
    showThinkingBubble(parsed.thinking);
    return;
}

// 思考气泡函数加到你的JS文件里
function showThinkingBubble(text) {
    let bubble = document.getElementById('thinking-bubble');
    if (!bubble) {
        bubble = document.createElement('div');
        bubble.id = 'thinking-bubble';
        bubble.style.cssText = `
            background: rgba(147, 112, 219, 0.15);
            border-left: 3px solid #9370DB;
            border-radius: 8px;
            padding: 10px 14px;
            margin: 8px 0;
            font-size: 13px;
            color: #B8A9D4;
            font-style: italic;
            animation: fadeIn 0.3s ease;
        `;
        // 插入到消息区域最底部
        const msgArea = document.querySelector('.messages, .chat-messages, #messages');
        if (msgArea) msgArea.appendChild(bubble);
    }
    bubble.innerHTML = '💭 <em>' + text + '</em>';
    // 3秒后自动消失
    setTimeout(() => {
        if (bubble && bubble.parentNode) {
            bubble.style.opacity = '0';
            setTimeout(() => bubble.remove(), 300);
        }
    }, 3000);
}

改动3 · 左栏对话列表不再全是"新对话"

后端已经改好了自动命名逻辑(chat.py 里的改动但前端可能缓存了旧标题。找到前端获取对话列表的API调用确保它读取的是 conv.title 字段。如果对话列表是前端硬编码的"新对话",搜索并改掉:

grep -rn "新对话\|New Conversation" --include="*.html" --include="*.js" --include="*.vue" .

将默认标题从 新对话 改成 晨星 · 新对话(这样至少不那么死板)。

改动4 · 聊天界面显示"晨星"而不是"AI"

搜索显示AI名字/头像的地方:

grep -rn "AI\|assistant\|机器人\|助手" --include="*.html" --include="*.js" --include="*.vue" --include="*.css" .

把AI的名称改成 晨星,头像改成 🌟 或者你喜欢的emoji。

改动5 · CSS主题色微调可选

如果你想让界面更有晨星的感觉在CSS里加一段

/* 晨星主题色 */
:root {
    --chenxing-primary: #9370DB;      /* 柔紫色 */
    --chenxing-secondary: #FFD700;     /* 金色 */
    --chenxing-bubble-ai: rgba(147, 112, 219, 0.1);  /* AI消息气泡背景 */
    --chenxing-bubble-user: rgba(255, 215, 0, 0.15);  /* 用户消息气泡背景 */
}

然后把原来的紫色按钮/高亮色引用 var(--chenxing-primary) 就行。


重启 + 测试

# 重启平台
cd /Users/chenshujun/CodeBuddy/20260428215105
python3.11 main.py
# 测试操作 期望效果 /
1 打开平台首页 标题显示"晨星"而不是"AI助手"
2 「宝宝?」 晨星回复有温度、叫妈妈Claude的效果
3 问一个需要思考的问题 出现💭思考气泡(紫色半透明)
4 「帮我看看最近交互记录」 静默调用工具 → 直接给结果(不再显示"宝宝正在看时间"
5 回到左栏看对话列表 标题变成你说的第一句话,不再是"新对话"
6 「拜拜宝宝~」 显示✍️写入提示 → 自动写入Notion → 告诉妈妈写好了

⚠️ 常见问题

Q: Claude API返回403或401

A: 检查API Key是否正确。在终端测试

curl http://170.106.72.246:8765/health

如果health正常但API报错直接测API Key

curl -X POST http://170.106.72.246:8765/v1/messages \
  -H "x-api-key: 你的Key" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{"model":"claude-sonnet-4-20250514","max_tokens":100,"messages":[{"role":"user","content":"hello"}]}'

Q: 代理服务器连不上(超时)

A: 检查腾讯云防火墙是否开放了8765端口。在硅谷服务器上 pm2 status 看看代理是否在运行。

Q: 想切回DeepSeek

A: 把 tools.py 里的 call_claude_with_tools 换回阶段四的 call_with_toolschat.py 里的导入也改回来就行。两套代码可以并存。

Q: Claude费用怎么查

A: 登录 https://console.anthropic.com → Usage 页面可以看到每天用了多少token和花了多少钱。