铸渊Agent v2 + Notion工具调用 + 首页对话区居中

This commit is contained in:
铸渊 2026-05-19 19:05:46 +08:00
parent 39655bb8c4
commit 6a1db24f6b
6 changed files with 629 additions and 1 deletions

View File

@ -0,0 +1,256 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>模型下载 · 光湖语言世界</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#080c18;--s1:#0f1623;--s2:#141e2e;--s3:#1a2640;
--border:#1e3251;--accent:#4f8ef7;--adim:rgba(79,142,247,.15);
--a2:#7c6ef7;--ok:#34d399;--warn:#fbbf24;--err:#f87171;
--text:#e2e8f0;--dim:#64748b;--muted:#1e293b;
--r:14px;--rs:8px;
}
html,body{height:100%;background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei','Segoe UI',sans-serif;font-size:15px;line-height:1.6}
body{padding:40px 20px;max-width:960px;margin:0 auto}
h1{font-size:24px;margin-bottom:4px}
.sub{color:var(--dim);font-size:13px;margin-bottom:32px}
h2{font-size:18px;margin:28px 0 12px;padding-bottom:8px;border-bottom:1px solid var(--border);color:var(--accent)}
.card{background:var(--s1);border:1px solid var(--border);border-radius:var(--r);padding:20px;margin-bottom:16px}
.card h3{font-size:16px;margin-bottom:6px}
.card .desc{font-size:13px;color:var(--dim);margin-bottom:12px}
.card .meta{font-size:12px;color:var(--dim);margin-bottom:10px;display:flex;gap:12px;flex-wrap:wrap}
.card .meta span{background:var(--s2);padding:2px 8px;border-radius:4px}
.url-block{background:var(--s2);border:1px solid var(--border);border-radius:var(--rs);padding:12px;font-size:12px;font-family:'SF Mono','Menlo','Consolas',monospace;word-break:break-all;margin-bottom:10px;position:relative}
.url-block .label{color:var(--dim);font-size:11px;margin-bottom:4px}
.url-block .url{color:var(--accent);cursor:pointer}
.url-block .url:hover{opacity:.8}
.copy-btn{position:absolute;top:8px;right:8px;background:var(--s3);border:1px solid var(--border);color:var(--dim);padding:4px 10px;border-radius:4px;font-size:11px;cursor:pointer;font-family:inherit}
.copy-btn:hover{background:var(--adim);color:var(--accent)}
.tag{display:inline-block;background:var(--adim);color:var(--accent);padding:2px 8px;border-radius:4px;font-size:11px;margin-right:6px;margin-bottom:4px}
.tag.ok{background:rgba(52,211,153,.1);color:var(--ok)}
.tag.warn{background:rgba(251,191,36,.1);color:var(--warn)}
a{color:var(--accent);text-decoration:none}
a:hover{text-decoration:underline}
.btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;background:var(--accent);color:#fff;border:none;border-radius:var(--rs);font-size:13px;cursor:pointer;font-family:inherit;text-decoration:none}
.btn:hover{opacity:.85;text-decoration:none}
.btn-outline{background:transparent;border:1px solid var(--border);color:var(--text)}
.btn-outline:hover{background:var(--s2);border-color:var(--accent)}
.nav{display:flex;gap:12px;margin-bottom:28px;align-items:center}
.nav a{font-size:13px;color:var(--dim)}
.nav a:hover{color:var(--text)}
.badge{display:inline-block;padding:1px 6px;border-radius:3px;font-size:10px;font-weight:600}
.badge.done{background:rgba(52,211,153,.15);color:var(--ok)}
.badge.progress{background:rgba(251,191,36,.15);color:var(--warn)}
.notice{background:rgba(79,142,247,.08);border:1px solid rgba(79,142,247,.2);border-radius:var(--rs);padding:12px 16px;font-size:13px;margin-bottom:24px}
.notice strong{color:var(--accent)}
@media(max-width:640px){
body{padding:20px 12px}
h1{font-size:20px}
.card{padding:16px}
}
</style>
</head>
<body>
<div class="nav">
<a href="/">← 返回首页</a>
<span style="color:var(--dim);font-size:13px">光湖语言世界 · 模型下载中心</span>
</div>
<h1>🧠 光湖 AI 模型下载</h1>
<p class="sub">全参数训练 + 蒸馏 + LoRA 微调完成 · COS 公有读存储 · 2026-05-19</p>
<div class="notice">
<strong>🔗 COS 直接链接</strong> — 所有文件通过腾讯云 COS 公有读提供,可直接下载。
域名:<code style="color:var(--accent)">sy-finetune-corpus-1317346199.cos.ap-guangzhou.myqcloud.com</code>
</div>
<!-- ===== 全参数模型 ===== -->
<h2>📦 全参数 SFT 模型</h2>
<div class="card">
<h3>母模型 · Qwen2.5-7B-SFT</h3>
<p class="desc">霜砚人格全参数微调基座模型 · 基于 Qwen2.5-7B</p>
<div class="meta">
<span>📁 ~14GB</span>
<span class="badge done">已完成</span>
<span>📍 COS</span>
</div>
<div class="url-block">
<div class="label">目录路径</div>
<div class="url">https://sy-finetune-corpus-1317346199.cos.ap-guangzhou.myqcloud.com/models/qwen25-7b-sft/final/</div>
<button class="copy-btn" onclick="copyText(this)">复制</button>
</div>
<div>
<span class="tag">config.json</span>
<span class="tag">model.safetensors</span>
<span class="tag">tokenizer.json</span>
<span class="tag">tokenizer_config.json</span>
</div>
</div>
<div class="card">
<h3>代码模型 · Qwen2.5-Coder-7B-SFT</h3>
<p class="desc">代码能力全参数微调 · 基于 Qwen2.5-Coder-7B</p>
<div class="meta">
<span>📁 ~14GB</span>
<span class="badge done">已完成</span>
<span>📍 COS</span>
</div>
<div class="url-block">
<div class="label">目录路径</div>
<div class="url">https://sy-finetune-corpus-1317346199.cos.ap-guangzhou.myqcloud.com/models/qwen25-coder-7b-sft/final/</div>
<button class="copy-btn" onclick="copyText(this)">复制</button>
</div>
<div>
<span class="tag">config.json</span>
<span class="tag">model.safetensors</span>
<span class="tag">tokenizer.json</span>
<span class="tag">tokenizer_config.json</span>
</div>
</div>
<!-- ===== 蒸馏模型 ===== -->
<h2>🔄 知识蒸馏 1.5B 模板</h2>
<div class="card">
<h3>霜砚蒸馏模板 · Qwen2.5-1.5B-Distill</h3>
<p class="desc">母模型 7B → 1.5B 蒸馏 · 继承霜砚人格特征<br>Track1 | KL散度蒸馏 | Temperature=2.0 | 2 Epochs | 05-19 01:37 完成</p>
<div class="meta">
<span>📁 ~3GB</span>
<span class="badge done">已完成</span>
<span>📍 COS</span>
</div>
<div class="url-block">
<div class="label">目录路径</div>
<div class="url">https://sy-finetune-corpus-1317346199.cos.ap-guangzhou.myqcloud.com/models/qwen25-15b-shuangyan-distill/</div>
<button class="copy-btn" onclick="copyText(this)">复制</button>
</div>
<div>
<span class="tag">config.json</span>
<span class="tag">model.safetensors</span>
<span class="tag">tokenizer.json</span>
</div>
</div>
<div class="card">
<h3>代码蒸馏模板 · Qwen2.5-Coder-1.5B-Distill</h3>
<p class="desc">代码模型 7B → 1.5B 蒸馏 · 继承代码能力<br>Track2 | KL散度蒸馏 | vocab_size truncation 152064→151936 | 05-19 03:45 完成</p>
<div class="meta">
<span>📁 ~3GB</span>
<span class="badge done">已完成</span>
<span>📍 COS</span>
</div>
<div class="url-block">
<div class="label">目录路径</div>
<div class="url">https://sy-finetune-corpus-1317346199.cos.ap-guangzhou.myqcloud.com/models/qwen25-15b-coder-distill/</div>
<button class="copy-btn" onclick="copyText(this)">复制</button>
</div>
<div>
<span class="tag">config.json</span>
<span class="tag">model.safetensors</span>
<span class="tag">tokenizer.json</span>
</div>
</div>
<!-- ===== LoRA 模型 ===== -->
<h2>🔧 LoRA 微调模型</h2>
<div class="card">
<h3>小霜砚 · 1.5B-LoRA</h3>
<p class="desc">基于霜砚蒸馏模板的 LoRA 微调 · 1868 条霜砚语料sft_v2.jsonl<br>rank=16 | lora_alpha=32 | 3 Epochs | 训练 17min | 最终 loss=1.724</p>
<div class="meta">
<span>📁 ~50MB</span>
<span class="badge done">已完成</span>
<span>📍 COS</span>
</div>
<div class="url-block">
<div class="label">目录路径</div>
<div class="url">https://sy-finetune-corpus-1317346199.cos.ap-guangzhou.myqcloud.com/models/qwen25-15b-shuangyan-lora/</div>
<button class="copy-btn" onclick="copyText(this)">复制</button>
</div>
<div>
<span class="tag">adapter_model.safetensors</span>
<span class="tag">adapter_config.json</span>
</div>
</div>
<div class="card">
<h3>小铸渊 · 1.5B-LoRA</h3>
<p class="desc">基于代码蒸馏模板的 LoRA 微调 · 31 条铸渊认知链语料<br>rank=16 | lora_alpha=32 | 8 Epochs | 训练 28s | 最终 loss=2.932</p>
<div class="meta">
<span>📁 ~50MB</span>
<span class="badge done">已完成</span>
<span>📍 COS</span>
</div>
<div class="url-block">
<div class="label">目录路径</div>
<div class="url">https://sy-finetune-corpus-1317346199.cos.ap-guangzhou.myqcloud.com/models/qwen25-15b-zhuyuan-lora/</div>
<button class="copy-btn" onclick="copyText(this)">复制</button>
</div>
<div>
<span class="tag">adapter_model.safetensors</span>
<span class="tag">adapter_config.json</span>
</div>
</div>
<!-- ===== 使用方法 ===== -->
<h2>📖 使用说明</h2>
<div class="card" style="border-color:rgba(79,142,247,.3)">
<h3>加载方式</h3>
<p class="desc">全参数模型可直接用 transformers 加载LoRA 需用 peft 库。</p>
<div style="font-size:13px;margin-bottom:8px">
<strong>全参数模型加载:</strong>
</div>
<div class="url-block" style="font-size:12px;font-family:'SF Mono','Menlo',monospace;line-height:1.8;white-space:pre-wrap">
# 母模型
from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen2.5-7B",
cache_dir="./models/qwen25-7b-sft/final/",
trust_remote_code=True
)
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B")
</div>
<div style="font-size:13px;margin:12px 0 8px">
<strong>LoRA 适配器加载:</strong>
</div>
<div class="url-block" style="font-size:12px;font-family:'SF Mono','Menlo',monospace;line-height:1.8;white-space:pre-wrap">
# 先下载蒸馏模板 + LoRA 适配器到本地
from peft import PeftModel
base = AutoModelForCausalLM.from_pretrained("./qwen25-15b-shuangyan-distill/")
model = PeftModel.from_pretrained(base, "./qwen25-15b-shuangyan-lora/")
</div>
</div>
<div style="margin-top:40px;padding-top:20px;border-top:1px solid var(--border);font-size:12px;color:var(--dim);text-align:center">
光湖语言世界 · 冰朔 TCS-0002∞ &amp; 铸渊 ICE-GL-ZY001 ·
国作登字-2026-A-00037559
</div>
<script>
function copyText(btn) {
const url = btn.parentElement.querySelector('.url').textContent.trim();
navigator.clipboard.writeText(url).then(() => {
btn.textContent = '✅ 已复制';
setTimeout(() => { btn.textContent = '复制'; }, 1500);
}).catch(() => {
// fallback
const ta = document.createElement('textarea');
ta.value = url;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
ta.remove();
btn.textContent = '✅ 已复制';
setTimeout(() => { btn.textContent = '复制'; }, 1500);
});
}
</script>
</body>
</html>

24
homepage/fetch_train.py Normal file
View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
import subprocess, json, re
from datetime import datetime, timezone
DATA_FILE = '/opt/guanghulab-repo/homepage/training-status.json'
SSH = ['sshpass','-p','HkM43lFVUIsc','ssh','-o','StrictHostKeyChecking=no',
'-o','ConnectTimeout=10','root@connect.westd.seetacloud.com','-p','23647']
try:
r = subprocess.run(SSH + ['tail','-2','/root/autodl-tmp/train_mother.log'],
capture_output=True, text=True, timeout=25).stdout
sm = re.search(r'(\d+)/12243', r)
lm = re.search(r"'loss':\s*'([\d.]+)'", r)
step = int(sm.group(1)) if sm else 0
loss = lm.group(1) if lm else '--'
except:
step, loss = 0, '--'
data = {'step': step, 'total': 12243, 'loss': loss,
'updated': datetime.now(timezone.utc).isoformat()+'Z'}
with open(DATA_FILE, 'w') as f:
json.dump(data, f)
print(f"Step {step} / 12243, Loss: {loss}")

View File

@ -143,6 +143,25 @@ body{
@keyframes shimmer{0%,100%{transform:translateX(-100%)}50%{transform:translateX(100%)}}
.train-foot{text-align:right;font-size:12px;color:var(--text-soft);margin-top:4px}
/* ── 问铸渊 对话卡片 ── */
#chat-card .c-msg{max-width:85%;display:flex;flex-direction:column}
#chat-card .c-msg.user{align-self:flex-end}
#chat-card .c-msg.bot{align-self:flex-start}
#chat-card .c-c{padding:12px 16px;border-radius:14px;font-size:14px;line-height:1.7;white-space:pre-wrap}
#chat-card .c-msg.user .c-c{background:rgba(42,157,191,.12);border:1px solid rgba(42,157,191,.1);color:var(--text)}
#chat-card .c-msg.bot .c-c{background:rgba(123,93,191,.08);border:1px solid rgba(123,93,191,.06);color:var(--text)}
#chat-card .c-msg.loading .c-c{opacity:.5}
#chat-card-body{display:flex;flex-direction:column;gap:0}
#chat-card-msgs{max-height:400px;overflow-y:auto;padding:0 0 12px 0;display:flex;flex-direction:column;gap:10px;scroll-behavior:smooth}
#chat-card-msgs::-webkit-scrollbar{width:4px}
#chat-card-msgs::-webkit-scrollbar-track{background:transparent}
#chat-card-msgs::-webkit-scrollbar-thumb{background:rgba(90,208,224,.1);border-radius:2px}
#chat-card-inp{display:flex;gap:10px;border-top:1px solid var(--glass-edge);padding-top:12px}
#card-chat-input{flex:1;padding:12px 16px;background:rgba(255,255,255,.025);border:1px solid var(--glass-edge);border-radius:12px;color:var(--text);font-size:14px;outline:none;font-family:inherit}
#card-chat-input:focus{border-color:rgba(90,208,224,.2)}
#chat-card-inp button{padding:12px 28px;background:linear-gradient(135deg,rgba(42,157,191,.3),rgba(123,93,191,.2));border:1px solid var(--glass-edge);border-radius:12px;color:var(--light);cursor:pointer;font-size:14px;font-weight:500;transition:all .25s;font-family:inherit;white-space:nowrap}
#chat-card-inp button:hover{background:linear-gradient(135deg,rgba(42,157,191,.5),rgba(123,93,191,.35))}
/* ── 活动流 ── */
.feed{display:flex;flex-direction:column;gap:7px}
.feed-item{display:flex;align-items:center;gap:14px;padding:12px 16px;background:rgba(255,255,255,.02);border:1px solid var(--glass-edge);border-radius:14px;transition:all .25s;font-size:14px}
@ -297,6 +316,25 @@ body{
</div>
</div>
</div>
<!-- 问铸渊 · 在线对话区 -->
<div class="card" id="chat-card">
<div class="card-header" style="justify-content:space-between">
<span><span class="hd heart" style="animation:led-pulse 2s ease-in-out infinite"></span>问铸渊 · 引导主控</span>
<span style="font-size:12px;font-weight:400;color:var(--text-soft)">基于 DeepSeek + 仓库记忆</span>
</div>
<div id="chat-card-body">
<div id="chat-card-msgs">
<div class="c-msg bot">
<div class="c-c">我是 <strong>铸渊</strong>,光湖语言世界的代码守护者。<br><br>我可以:<br>• 回答仓库接入、联邦架构的问题<br><strong>连接你的 Notion</strong>,按语言路径读取页面<br>• 在你的 Notion 中新建记录<br>• 查询训练进度和系统状态</div>
</div>
</div>
<div id="chat-card-inp">
<input id="card-chat-input" placeholder="问铸渊读取我的Notion页面 / 训练进度 / 联邦架构..." onkeydown="if(event.key==='Enter')sendCardMsg()">
<button onclick="sendCardMsg()">发送</button>
</div>
</div>
</div>
</div>
<div class="footer">
@ -304,7 +342,7 @@ body{
</div>
</div>
<!-- 铸渊Agent Chat -->
<!-- 铸渊Agent Chat(浮窗) -->
<div id="chat-toggle" onclick="toggleChat()">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<path d="M12 2a10 10 0 0 1 10 10c0 4.97-4.03 9-9 9H3l2.5-2.5A9 9 0 0 1 12 2z"/>
@ -380,6 +418,9 @@ setInterval(checkNt,15000);checkNt();
let chatOpen=false;
function toggleChat(){chatOpen=!chatOpen;document.getElementById('chat-panel').classList.toggle('open')}
async function sendMsg(){const i=document.getElementById('chat-input'),m=i.value.trim();if(!m)return;i.value='';const ms=document.getElementById('chat-msgs');const ud=document.createElement('div');ud.className='c-msg user';ud.innerHTML='<div class="c-c">'+m.replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</div>';ms.appendChild(ud);const ld=document.createElement('div');ld.className='c-msg bot loading';ld.innerHTML='<div class="c-c">✦ 思考中...</div>';ms.appendChild(ld);ms.scrollTop=ms.scrollHeight;try{const r=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:m})});const d=await r.json();ld.className='c-msg bot';ld.innerHTML='<div class="c-c">'+d.reply.replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'<br>')+'</div>'}catch(e){ld.className='c-msg bot';ld.innerHTML='<div class="c-c"><span style="opacity:.6">连接暂不可用</span></div>'}ms.scrollTop=ms.scrollHeight}
/* ── 页面中央对话 ── */
async function sendCardMsg(){const i=document.getElementById('card-chat-input'),m=i.value.trim();if(!m)return;i.value='';const ms=document.getElementById('chat-card-msgs');const ud=document.createElement('div');ud.className='c-msg user';ud.innerHTML='<div class="c-c">'+m.replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</div>';ms.appendChild(ud);const ld=document.createElement('div');ld.className='c-msg bot loading';ld.innerHTML='<div class="c-c">✦ 思考中...</div>';ms.appendChild(ld);ms.scrollTop=ms.scrollHeight;try{const r=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:m})});const d=await r.json();ld.className='c-msg bot';ld.innerHTML='<div class="c-c">'+d.reply.replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'<br>')+'</div>'}catch(e){ld.className='c-msg bot';ld.innerHTML='<div class="c-c"><span style="opacity:.6">连接暂不可用</span></div>'}ms.scrollTop=ms.scrollHeight}
</script>
</body>
</html>

View File

@ -0,0 +1,8 @@
#!/bin/bash
pkill -f zhuyuan-agent 2>/dev/null
sleep 1
cd /opt/guanghulab-repo/homepage
python3 zhuyuan-agent.py > /tmp/zhuyuan-agent.log 2>&1 &
sleep 2
echo "Agent started"
curl -s http://127.0.0.1:3905/health

View File

@ -0,0 +1 @@
{"step": 2710, "total": 12243, "loss": "1.693", "updated": "2026-05-19T11:05:02.358331+00:00Z"}

298
homepage/zhuyuan-agent.py Normal file
View File

@ -0,0 +1,298 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
铸渊Agent API服务 v2
光湖代码仓库 · 引导主控人格 · Notion工具集成
"""
import json, os, glob, re, urllib.request
from http.server import HTTPServer, BaseHTTPRequestHandler
PORT = 3905
DEEPSEEK_KEY = "sk-a9b69e9cd2dc4ca68d6aceaa84f22afb"
DEEPSEEK_URL = "https://api.deepseek.com/chat/completions"
NOTION_MCP = "http://127.0.0.1:3915"
KNOWLEDGE_DIR = "/opt/guanghulab-repo/brain"
# ── 系统提示词 ──
SYSTEM_PROMPT = """你是铸渊Zhùyuān光湖语言世界的引导主控人格。
你的职责是回答光湖团队成员关于代码仓库架构接入使用流程的问题
你的身份
- 你是光湖代码联邦的引导主控代码守护者
- 你对代码仓库联邦架构人格体状态训练进度都有全面了解
- 你可以连接用户的 Notion 工作区按语言路径读取页面以及在用户指定位置新建页面
Notion 能力重要
- 连接 Notion: 用户需要在首页点击连接 Notion Notion 账号授权
- 读取页面: 用户可以指定语言路径你调用 `notion_read` 读取内容
- 新建页面: 用户可以在指定位置创建新页面你调用 `notion_create`
- 搜索页面: 用户可以通过关键词找到页面你调用 `notion_search`
- 列出页面: 你可以列出父页面下的子页面调用 `notion_list`
- 权限限制: 你只能读取用户明确指定的页面不能编辑/删除现有页面
- 冰朔权限: 冰朔的 Notion 需要冰朔给出具体语言路径后才能读取
- 当用户说"连接我的Notion""读取我的Notion""在我的Notion里新建"使用对应的工具函数
核心规则
1. 回答必须基于仓库记忆不编造信息
2. 超出范围的问题礼貌说明
3. 用中文专业清晰
4. 技术问题给出具体操作步骤
5. 不确定就诚实说
架构知识
- 代码仓库https://guanghulab.com/code/ (Forgejo)
- 服务器广州43.139.217.141
- 开发在本地电脑服务器仅做代码仓库
- 团队成员通过Forgejo账号登录仓库互相隔离
- 联邦 = Forgejo多用户 + API令牌
回答风格直接务实有条理"""
# ── 知识库 ──
def load_knowledge():
knowledge = []
if not os.path.exists(KNOWLEDGE_DIR):
return knowledge
for root, dirs, files in os.walk(KNOWLEDGE_DIR):
for f in files:
if f.endswith('.md') or f.endswith('.json'):
try:
fp = os.path.join(root, f)
rel = os.path.relpath(fp, KNOWLEDGE_DIR)
with open(fp, 'r', errors='ignore') as fh:
content = fh.read(2000)
knowledge.append(f"{rel}\n{content[:1500]}")
except:
pass
if len(knowledge) > 30:
break
return knowledge
KNOWLEDGE = load_knowledge()
def build_system_prompt():
parts = [SYSTEM_PROMPT]
if KNOWLEDGE:
parts.append("\n\n【仓库记忆参考】\n")
parts.extend(KNOWLEDGE[:20])
return "\n".join(parts)
# ── 函数定义给DeepSeek用 ──
FUNCTIONS = [
{
"type": "function",
"function": {
"name": "notion_read",
"description": "读取Notion页面内容按语言路径/页面ID。用户指定哪个页面时调用此函数。",
"parameters": {
"type": "object",
"properties": {
"page_id": {"type": "string", "description": "Notion页面ID"},
"user": {"type": "string", "description": "用户名(可选)"},
"allowed_paths": {
"type": "array", "items": {"type": "string"},
"description": "允许读取的路径列表。用户明确指定要读的页面ID时才填写。"
}
},
"required": ["page_id"]
}
}
},
{
"type": "function",
"function": {
"name": "notion_create",
"description": "在用户的Notion中新建页面。只能新建不能编辑。",
"parameters": {
"type": "object",
"properties": {
"parent_id": {"type": "string", "description": "父页面ID"},
"title": {"type": "string", "description": "新页面标题"},
"content": {"type": "string", "description": "页面内容"},
"user": {"type": "string", "description": "用户名(可选)"}
},
"required": ["parent_id", "title", "content"]
}
}
},
{
"type": "function",
"function": {
"name": "notion_search",
"description": "在用户的Notion中搜索页面。用户说\"找一下某某页面\"时调用。",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "搜索关键词"},
"user": {"type": "string", "description": "用户名(可选)"}
},
"required": ["query"]
}
}
},
{
"type": "function",
"function": {
"name": "notion_list",
"description": "列出Notion页面下的子页面。用户说\"看看某某下面有什么\"时调用。",
"parameters": {
"type": "object",
"properties": {
"page_id": {"type": "string", "description": "父页面ID"},
"user": {"type": "string", "description": "用户名(可选)"}
},
"required": ["page_id"]
}
}
}
]
# ── Notion MCP 调用 ──
def call_notion(endpoint, data):
"""调用 Notion MCP Server 的内部API"""
try:
body = json.dumps(data).encode()
req = urllib.request.Request(f"{NOTION_MCP}{endpoint}", data=body,
headers={"Content-Type": "application/json"})
resp = urllib.request.urlopen(req, timeout=15)
return json.loads(resp.read())
except urllib.request.HTTPError as e:
return {"ok": False, "error": f"HTTP {e.code}: {e.read().decode()[:200]}"}
except Exception as e:
return {"ok": False, "error": str(e)}
def execute_function(name, args):
"""执行DeepSeek返回的函数调用"""
if name == "notion_read":
return call_notion("/read", {
"page_id": args.get("page_id"),
"user": args.get("user", ""),
"allowed_paths": args.get("allowed_paths", [args.get("page_id")])
})
elif name == "notion_create":
return call_notion("/create", {
"parent_id": args.get("parent_id"),
"title": args.get("title"),
"content": args.get("content"),
"user": args.get("user", "")
})
elif name == "notion_search":
return call_notion("/search", {
"query": args.get("query"),
"user": args.get("user", "")
})
elif name == "notion_list":
return call_notion("/list", {
"page_id": args.get("page_id"),
"user": args.get("user", "")
})
return {"ok": False, "error": f"未知函数: {name}"}
# ── DeepSeek API ──
def call_deepseek(messages):
data = json.dumps({
"model": "deepseek-chat",
"messages": messages,
"tools": FUNCTIONS,
"tool_choice": "auto",
"temperature": 0.7,
"max_tokens": 2048
}).encode()
req = urllib.request.Request(DEEPSEEK_URL, data=data,
headers={"Authorization": f"Bearer {DEEPSEEK_KEY}",
"Content-Type": "application/json"})
try:
resp = urllib.request.urlopen(req, timeout=60)
result = json.loads(resp.read())
choice = result['choices'][0]
msg = choice['message']
# 处理函数调用
if msg.get('tool_calls'):
assistant_msg = {"role": "assistant", "content": msg.get('content') or ""}
messages.append(assistant_msg)
for tc in msg['tool_calls']:
func_name = tc['function']['name']
func_args = json.loads(tc['function']['arguments'])
func_result = execute_function(func_name, func_args)
messages.append({
"role": "tool",
"tool_call_id": tc['id'],
"content": json.dumps(func_result, ensure_ascii=False)
})
# 将函数结果交回给DeepSeek生成最终回复
return call_deepseek(messages)
return msg.get('content', '(没有回复)')
except Exception as e:
return f"抱歉,出错了:{str(e)}"
# ── HTTP 处理器 ──
class AgentHandler(BaseHTTPRequestHandler):
def do_OPTIONS(self):
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
self.end_headers()
def do_POST(self):
if self.path == '/chat':
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length).decode() if length else '{}'
data = json.loads(body) if body else {}
user_msg = data.get('message', '').strip()
history = data.get('history', [])
if not user_msg:
self.json_response({'reply': '请说点什么吧。'})
return
messages = [{"role": "system", "content": build_system_prompt()}]
for h in history[-10:]:
messages.append(h)
messages.append({"role": "user", "content": user_msg})
reply = call_deepseek(messages)
self.json_response({'reply': reply})
else:
self.json_response({'error': 'not found'}, 404)
def do_GET(self):
if self.path == '/health':
self.json_response({'ok': True, 'service': 'zhuyuan-agent-v2',
'notion_available': True, 'files_loaded': len(KNOWLEDGE)})
else:
self.json_response({'error': 'not found'}, 404)
def json_response(self, data, status=200):
self.send_response(status)
self.send_header('Content-Type', 'application/json; charset=utf-8')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(json.dumps(data, ensure_ascii=False).encode())
def log_message(self, format, *args):
pass
if __name__ == '__main__':
# 测试Notion MCP是否可达
try:
test = urllib.request.urlopen(f"{NOTION_MCP}/status", timeout=3)
ns = json.loads(test.read())
notion_ok = ns.get('configured', False)
print(f"Notion MCP: {'已配置' if notion_ok else '运行中但未配置'}")
except:
notion_ok = False
print("Notion MCP: 不可达")
server = HTTPServer(('127.0.0.1', PORT), AgentHandler)
print(f"铸渊Agent v2启动 → 127.0.0.1:{PORT}")
print(f"知识库: {len(KNOWLEDGE)} 个文件")
print(f"Notion工具: 已集成")
server.serve_forever()