想要搜索结果没有广告吗?想要一个不会被 SEO 垃圾淹没的界面吗?想要 AI 直接帮你把结果读一遍再总结吗?想......好吧我开个玩笑,这个页面不能帮你摆脱房贷,也不能替你上班。
它就是一个搜索页面。一个单 HTML 文件,打开就能用,填个 API Key 就能搜,结果干净、卡片清爽、还带一个可选的 AI 综合回答。功能不多,但每一个都够用。
下面正经介绍。
一、它到底做了什么
核心就两件事:
第一,调 DeepSeek 的 /v1/tools/search 接口 ,拿到结构化的搜索结果,渲染成卡片列表。每条结果包含站点图标、站点名、标题、摘要、日期,以及接口返回的 rerank_score 和 authority 两个字段衍生出来的「优质」「权威」标签。
第二,可选地调 /chat/completions 接口,把搜索结果作为上下文喂给模型,让它生成一段 Markdown 格式的综合回答。默认关闭,需要时在设置里打开。
除了这两个核心,剩下都是围绕「用得舒服」做的小事:
- 四套界面风格:暗色、极简白、科技蓝、暖砂,设置里点一下即时切换,不用刷新。
- 配置本地保存 :API Key、结果数量、风格偏好存在浏览器
localStorage,下次打开还在。 - 快捷键 :
⌘K/Ctrl+K聚焦搜索框,Esc关闭设置。 - 骨架屏:搜索期间显示 shimmer 占位,不是干等一个转圈。
- 可中断:等待时按钮变成「停止」,随时可以取消请求。
- 流式输出 :AI 回答边生成边显示,带打字光标,引用角标
[1][2]自动高亮。
二、四套风格
| 风格 | 定位 | 主色 | 圆角 |
|---|---|---|---|
| 暗色 | 默认,克制 | 蓝紫 #6e8bff |
16px |
| 极简白 | 纯白 + 胶囊,参考 MiniMax 官网 | 近黑 #18181b + 品红渐变 |
9999px |
| 科技蓝 | 深蓝 + 网格 + 青光,门户感 | 青蓝 #38bdf8 |
16px |
| 暖砂 | 米白低对比,护眼 | 暖棕 #b45309 |
14px |
所有组件的圆角、阴影、强调色都走 CSS 变量。想加第五套风格,只需要在 :root 下面再写一段 [data-style="xxx"]{ ... } 覆盖变量,组件样式一行都不用动。
三、完整源码
整份代码就是下面这一个 index.html。保存,双击打开,填 Key,开始用。
html
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark" data-style="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DeepSeek Search</title>
<style>
/* ============================================================
风格变量层
每套风格只覆盖这些 token,组件 CSS 全部引用变量
============================================================ */
/* 暗色(默认) */
:root, [data-style="dark"]{
--bg:#0a0a0c;
--surface:#111114;
--surface-2:#17171b;
--surface-3:#1e1e24;
--border:rgba(255,255,255,.065);
--border-hover:rgba(255,255,255,.14);
--text:#ededf0;
--text-2:#a0a0ab;
--text-3:#5c5c68;
--accent:#6e8bff;
--accent-soft:rgba(110,139,255,.12);
--accent-border:rgba(110,139,255,.35);
--accent-gradient:linear-gradient(180deg,#7b95ff,#5f7df0);
--green:#3ecf8e;
--amber:#f5a623;
--radius:16px;
--radius-sm:11px;
--ease:cubic-bezier(.4,0,.2,1);
--spring:cubic-bezier(.34,1.3,.64,1);
--shadow-card:0 1px 2px rgba(0,0,0,.3),0 8px 32px -12px rgba(0,0,0,.5);
--shadow-float:0 24px 80px -20px rgba(0,0,0,.8);
--ambient:
radial-gradient(720px 480px at 18% -6%, rgba(110,139,255,.09), transparent 64%),
radial-gradient(600px 400px at 84% 4%, rgba(110,139,255,.05), transparent 64%),
radial-gradient(800px 600px at 50% 110%, rgba(110,139,255,.04), transparent 64%);
}
/* 极简白 · MiniMax 路线 */
[data-style="minimax"]{
--bg:#ffffff;
--surface:#ffffff;
--surface-2:#f7f7f9;
--surface-3:#efeff3;
--border:rgba(0,0,0,.075);
--border-hover:rgba(0,0,0,.16);
--text:#18181b;
--text-2:#52525b;
--text-3:#a1a1aa;
--accent:#18181b;
--accent-soft:rgba(234,94,193,.08);
--accent-border:rgba(234,94,193,.35);
--accent-gradient:linear-gradient(180deg,#2a2a2e,#0f0f11);
--green:#0d9488;
--amber:#d97706;
--radius:9999px;
--radius-sm:12px;
--shadow-card:0 1px 2px rgba(44,30,116,.04),0 8px 28px -12px rgba(44,30,116,.10);
--shadow-float:0 24px 70px -18px rgba(44,30,116,.22);
--ambient:
radial-gradient(640px 420px at 78% -10%, rgba(234,94,193,.06), transparent 62%),
radial-gradient(560px 380px at 12% 6%, rgba(20,86,240,.05), transparent 62%);
}
[data-style="minimax"] .search-btn,
[data-style="minimax"] .primary-btn{
border-radius:9999px;
background:var(--accent-gradient);
box-shadow:0 1px 2px rgba(0,0,0,.12),inset 0 1px 0 rgba(255,255,255,.14);
}
[data-style="minimax"] .search-box{
border-radius:9999px;
padding-left:20px;
}
[data-style="minimax"] .chip{
border-radius:9999px;
}
[data-style="minimax"] .result-card{
border-radius:20px;
}
[data-style="minimax"] .result-card:hover{
border-color:rgba(234,94,193,.28);
box-shadow:0 1px 2px rgba(44,30,116,.04),0 14px 36px -14px rgba(234,94,193,.18);
}
[data-style="minimax"] .result-card:hover .result-title{
color:#ea5ec1;
}
[data-style="minimax"] .brand-mark{
background:linear-gradient(135deg,#ff5530,#ea5ec1,#a855f7);
box-shadow:0 0 20px rgba(234,94,193,.28);
}
[data-style="minimax"] .answer{
border-radius:22px;
}
[data-style="minimax"] .badge.auth{
background:rgba(234,94,193,.09);
color:#ea5ec1;
}
[data-style="minimax"] .cite{
background:rgba(234,94,193,.1);
color:#ea5ec1;
}
[data-style="minimax"] .caret{
background:#ea5ec1;
}
[data-style="minimax"] .answer-head::before{
background:#ea5ec1;
box-shadow:0 0 8px rgba(234,94,193,.5);
}
/* 科技蓝 · 参考站门户路线 */
[data-style="tech"]{
--bg:#081226;
--surface:#0d1b33;
--surface-2:#122442;
--surface-3:#1a3054;
--border:rgba(125,211,252,.10);
--border-hover:rgba(125,211,252,.24);
--text:#eef6ff;
--text-2:#9ab3cf;
--text-3:#5c7898;
--accent:#38bdf8;
--accent-soft:rgba(56,189,248,.10);
--accent-border:rgba(56,189,248,.35);
--accent-gradient:linear-gradient(180deg,#38bdf8,#0ea5e9);
--green:#34d399;
--amber:#fbbf24;
--shadow-card:0 1px 2px rgba(0,0,0,.4),0 8px 32px -12px rgba(0,0,0,.6);
--shadow-float:0 24px 80px -20px rgba(0,0,0,.8);
--ambient:
radial-gradient(900px 480px at 10% -12%, rgba(56,189,248,.18), transparent 60%),
radial-gradient(760px 420px at 90% 0%, rgba(129,140,248,.14), transparent 60%),
linear-gradient(180deg, rgba(11,30,60,.0), rgba(8,18,38,.6));
}
[data-style="tech"] .ambient::after{
content:'';
position:absolute;inset:0;
opacity:.4;
background-image:
linear-gradient(rgba(255,255,255,.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,.04) 1px, transparent 1px);
background-size:44px 44px;
mask-image:radial-gradient(ellipse 80% 60% at 50% 0%, #000 30%, transparent 75%);
}
[data-style="tech"] .brand-mark{
background:linear-gradient(135deg,#38bdf8,#818cf8);
box-shadow:0 0 22px rgba(56,189,248,.45);
}
[data-style="tech"] .hero-title span{
background:linear-gradient(90deg,#7dd3fc,#c4b5fd,#f0abfc);
-webkit-background-clip:text;background-clip:text;color:transparent;
}
[data-style="tech"] .search-box{
background:rgba(13,27,51,.72);
backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
}
[data-style="tech"] .result-card:hover{
border-color:rgba(56,189,248,.35);
box-shadow:0 0 0 1px rgba(56,189,248,.12),0 12px 40px -14px rgba(56,189,248,.25);
}
[data-style="tech"] .badge.good{
background:rgba(52,211,153,.1);color:#34d399;
}
/* 暖砂 · 中性低对比 */
[data-style="warm"]{
--bg:#faf8f5;
--surface:#fffdfa;
--surface-2:#f4f0e9;
--surface-3:#ebe5da;
--border:rgba(120,100,80,.10);
--border-hover:rgba(120,100,80,.22);
--text:#2c241b;
--text-2:#6b5f52;
--text-3:#a89a88;
--accent:#b45309;
--accent-soft:rgba(180,83,9,.07);
--accent-border:rgba(180,83,9,.3);
--accent-gradient:linear-gradient(180deg,#c2701a,#a34a06);
--green:#4d7c0f;
--amber:#b45309;
--radius:14px;
--radius-sm:10px;
--shadow-card:0 1px 2px rgba(80,60,40,.04),0 8px 28px -12px rgba(80,60,40,.08);
--shadow-float:0 24px 70px -18px rgba(80,60,40,.18);
--ambient:
radial-gradient(640px 420px at 80% -10%, rgba(180,83,9,.05), transparent 62%),
radial-gradient(520px 360px at 15% 8%, rgba(77,124,15,.04), transparent 62%);
}
[data-style="warm"] .search-btn,
[data-style="warm"] .primary-btn{
color:#fff;
}
[data-style="warm"] .brand-mark{
background:linear-gradient(135deg,#d97706,#92400e);
box-shadow:0 0 18px rgba(180,83,9,.25);
}
/* ============================================================
基础
============================================================ */
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","Segoe UI",Inter,Roboto,"Helvetica Neue",Arial,sans-serif;
background:var(--bg);
color:var(--text);
-webkit-font-smoothing:antialiased;
-moz-osx-font-smoothing:grayscale;
font-feature-settings:"cv02","cv03","cv04","ss01","tnum";
overflow-x:hidden;
transition:background .35s var(--ease),color .35s var(--ease);
}
.ambient{
position:fixed;inset:0;pointer-events:none;z-index:0;
background:var(--ambient);
transition:background .45s var(--ease),opacity .4s var(--ease);
}
[data-style="tech"] .ambient{position:fixed}
.shell{max-width:780px;margin:0 auto;padding:0 24px;position:relative;z-index:1}
/* 顶栏 */
.top{display:flex;align-items:center;justify-content:space-between;padding:18px 0 0}
.brand{display:flex;align-items:center;gap:9px;font-size:13.5px;font-weight:600;letter-spacing:-.01em}
.brand-mark{
width:22px;height:22px;border-radius:6px;flex:none;position:relative;
background:linear-gradient(135deg,#6e8bff,#9b8bff);
box-shadow:0 0 18px rgba(110,139,255,.4),inset 0 1px 0 rgba(255,255,255,.3);
transition:background .35s var(--ease),box-shadow .35s var(--ease);
}
.brand-mark::after{
content:'';position:absolute;inset:0;border-radius:6px;
background:linear-gradient(180deg,rgba(255,255,255,.25),transparent 55%);
}
.brand em{font-style:normal;color:var(--text-3);font-weight:500}
.actions{display:flex;gap:4px}
.icon-btn{
width:32px;height:32px;border-radius:8px;border:none;background:transparent;
color:var(--text-2);cursor:pointer;display:flex;align-items:center;justify-content:center;
transition:all .16s var(--ease);
}
.icon-btn:hover{background:var(--surface-2);color:var(--text)}
.icon-btn:active{transform:scale(.9)}
.icon-btn svg{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;pointer-events:none}
/* 主区 */
.main{min-height:calc(100vh - 52px);display:flex;flex-direction:column}
.hero{flex:1;display:flex;flex-direction:column;justify-content:center;padding-bottom:8vh;transition:flex .5s var(--ease),padding .5s var(--ease)}
.main.active .hero{flex:0 0 auto;padding:16px 0 24px}
.hero-title{
font-size:clamp(26px,4.2vw,34px);
font-weight:600;letter-spacing:-.03em;line-height:1.2;
text-align:center;margin-bottom:26px;
background:linear-gradient(180deg,var(--text) 30%,var(--text-3));
-webkit-background-clip:text;background-clip:text;color:transparent;
transition:opacity .25s var(--ease),transform .4s var(--ease),height .45s var(--ease),margin .45s var(--ease);
}
.main.active .hero-title{opacity:0;transform:translateY(-8px);height:0;margin:0;pointer-events:none;overflow:hidden}
/* 搜索框 */
.search-box{
display:flex;align-items:center;gap:9px;
background:var(--surface);
border:1px solid var(--border);
border-radius:var(--radius);
padding:5px 5px 5px 16px;
box-shadow:var(--shadow-card);
transition:border-color .2s var(--ease),box-shadow .25s var(--ease),background .2s var(--ease),border-radius .3s var(--ease);
}
.search-box:focus-within{
border-color:var(--accent-border);
box-shadow:0 0 0 3px var(--accent-soft),var(--shadow-card);
}
.search-icon{width:17px;height:17px;flex:none;stroke:var(--text-3);fill:none;stroke-width:2;stroke-linecap:round;transition:stroke .2s}
.search-box:focus-within .search-icon{stroke:var(--accent)}
.search-input{flex:1;border:none;outline:none;background:transparent;font-size:15px;color:var(--text);padding:10px 0;font-family:inherit;letter-spacing:-.01em;min-width:0}
.search-input::placeholder{color:var(--text-3)}
.kbd{
font-family:inherit;font-size:10.5px;font-weight:500;color:var(--text-3);
border:1px solid var(--border);border-radius:5px;padding:2px 5px;
background:var(--surface-2);flex:none;transition:opacity .15s;letter-spacing:.02em;
}
.search-box:focus-within .kbd{opacity:0}
.search-btn{
flex:none;border:none;cursor:pointer;font-family:inherit;
font-size:13px;font-weight:600;letter-spacing:-.01em;
color:#fff;padding:0 18px;height:36px;border-radius:10px;
background:var(--accent-gradient);
box-shadow:0 1px 2px rgba(0,0,0,.2),inset 0 1px 0 rgba(255,255,255,.15);
transition:all .16s var(--ease);white-space:nowrap;
}
.search-btn:hover{filter:brightness(1.08)}
.search-btn:active{transform:scale(.97)}
.search-btn.stop{background:linear-gradient(180deg,#ef4444,#dc2626)}
.chips{display:flex;gap:7px;flex-wrap:wrap;justify-content:center;margin-top:18px;transition:opacity .25s var(--ease),height .35s var(--ease),margin .35s var(--ease)}
.main.active .chips{opacity:0;height:0;overflow:hidden;margin:0;pointer-events:none}
.chip{
font-family:inherit;font-size:12px;color:var(--text-2);cursor:pointer;
background:transparent;border:1px solid var(--border);
padding:6px 12px;border-radius:999px;
transition:all .16s var(--ease);
}
.chip:hover{color:var(--text);border-color:var(--border-hover);background:var(--surface-2)}
/* 内容 */
.content{display:none;padding-bottom:70px;animation:rise .4s var(--ease)}
.main.active .content{display:block}
@keyframes rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.notice{display:none;font-size:12px;line-height:1.55;padding:9px 13px;border-radius:9px;margin-bottom:12px;background:rgba(245,166,35,.07);border:1px solid rgba(245,166,35,.18);color:var(--amber)}
.notice.visible{display:block}
.error-box{display:none;font-size:12px;line-height:1.55;white-space:pre-wrap;word-break:break-word;padding:9px 13px;border-radius:9px;margin-bottom:12px;background:rgba(239,68,68,.07);border:1px solid rgba(239,68,68,.18);color:#f87171}
.error-box.visible{display:block}
/* 回答 */
.answer{
display:none;position:relative;
background:var(--surface);border:1px solid var(--border);
border-radius:var(--radius);padding:20px 22px;margin-bottom:18px;
box-shadow:var(--shadow-card);
transition:border-radius .3s var(--ease);
}
.answer.visible{display:block;animation:rise .4s var(--ease)}
.answer-head{display:flex;align-items:center;gap:7px;margin-bottom:13px;font-size:10.5px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--text-3)}
.answer-head::before{content:'';width:5px;height:5px;border-radius:50%;background:var(--accent);box-shadow:0 0 8px var(--accent);animation:pulse 2s ease-in-out infinite}
@keyframes pulse{0%,100%{opacity:.4}50%{opacity:1}}
.answer-body{font-size:14.5px;line-height:1.75;color:var(--text);word-break:break-word}
.answer-body p{margin:0 0 11px}.answer-body p:last-child{margin-bottom:0}
.answer-body h3{font-size:14.5px;font-weight:600;margin:16px 0 7px}
.answer-body ul{margin:7px 0 11px;padding-left:18px}
.answer-body li{margin:3px 0;color:var(--text-2)}
.answer-body strong{font-weight:600;color:var(--text)}
.answer-body code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.86em;background:var(--accent-soft);color:var(--accent);padding:1.5px 4px;border-radius:4px}
.answer-body pre{background:var(--surface-2);border:1px solid var(--border);border-radius:8px;padding:13px 15px;margin:11px 0;overflow-x:auto}
.answer-body pre code{background:none;color:var(--text-2);padding:0;font-size:12px;line-height:1.6}
.cite{display:inline-flex;align-items:center;justify-content:center;min-width:16px;height:16px;padding:0 4px;margin:0 1px;border-radius:4px;font-size:10px;font-weight:700;background:var(--accent-soft);color:var(--accent);vertical-align:1.5px;font-variant-numeric:tabular-nums}
.caret{display:inline-block;width:2px;height:1em;background:var(--accent);vertical-align:-2px;margin-left:1px;border-radius:1px;animation:blink 1.05s steps(2) infinite}
@keyframes blink{0%,50%{opacity:1}50.1%,100%{opacity:0}}
/* 结果头 */
.results-head{display:none;align-items:center;justify-content:space-between;margin-bottom:10px;padding:0 1px}
.results-head.visible{display:flex}
.results-title{font-size:10.5px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--text-3)}
.results-count{font-size:11px;color:var(--text-3);font-variant-numeric:tabular-nums}
/* 结果卡片 */
.result-list{display:flex;flex-direction:column;gap:8px}
.result-card{
display:flex;gap:12px;
background:var(--surface);border:1px solid var(--border);
border-radius:13px;padding:15px 16px;
text-decoration:none;color:inherit;
transition:border-color .18s var(--ease),background .18s var(--ease),transform .18s var(--ease),box-shadow .18s var(--ease),border-radius .3s var(--ease);
position:relative;overflow:hidden;
animation:rise .35s var(--ease) backwards;
}
.result-card:hover{border-color:var(--border-hover);background:var(--surface-2);transform:translateY(-1px);box-shadow:var(--shadow-card)}
.result-card.is-disabled{cursor:default}
.result-card.is-disabled:hover{transform:none}
.result-icon{
flex:none;width:24px;height:24px;border-radius:6px;margin-top:1px;
background:var(--surface-2);border:1px solid var(--border);
display:flex;align-items:center;justify-content:center;
overflow:hidden;font-size:10px;font-weight:600;color:var(--text-3);
}
.result-icon img{width:100%;height:100%;object-fit:contain}
.result-main{flex:1;min-width:0}
.result-meta{display:flex;align-items:center;gap:7px;flex-wrap:wrap;font-size:11px;color:var(--text-3);margin-bottom:4px}
.result-site{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:180px}
.dot{width:2.5px;height:2.5px;border-radius:50%;background:var(--text-3);opacity:.5;flex:none}
.badge{font-size:9.5px;font-weight:600;padding:1px 5px;border-radius:4px;letter-spacing:.02em}
.badge.good{background:rgba(62,207,142,.1);color:var(--green)}
.badge.auth{background:var(--accent-soft);color:var(--accent)}
.result-title{
font-size:14.5px;font-weight:600;line-height:1.4;color:var(--text);
margin-bottom:4px;letter-spacing:-.01em;
display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden;
transition:color .16s;
}
.result-card:hover .result-title{color:var(--accent)}
.result-card.is-disabled:hover .result-title{color:var(--text)}
.result-snippet{font-size:12.5px;line-height:1.6;color:var(--text-2);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
/* 骨架 */
.skeleton{display:flex;gap:12px;padding:15px 16px;background:var(--surface);border:1px solid var(--border);border-radius:13px}
.sk{background:linear-gradient(90deg,var(--surface-2) 25%,var(--surface-3) 50%,var(--surface-2) 75%);background-size:200% 100%;animation:shimmer 1.4s infinite;border-radius:5px}
@keyframes shimmer{to{background-position:-200% 0}}
.sk-icon{width:24px;height:24px;flex:none;border-radius:6px}
.sk-body{flex:1;display:flex;flex-direction:column;gap:7px}
.sk-line{height:10px}.sk-line.short{width:30%}.sk-line.med{width:65%}
/* 模态 */
.modal-backdrop{
position:fixed;inset:0;z-index:100;padding:20px;
background:rgba(0,0,0,.55);
backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
display:flex;align-items:center;justify-content:center;
opacity:0;pointer-events:none;transition:opacity .2s var(--ease);
}
.modal-backdrop.open{opacity:1;pointer-events:auto}
.modal{
width:100%;max-width:430px;max-height:86vh;overflow-y:auto;
background:var(--surface);
border:1px solid var(--border);border-radius:18px;padding:24px;
box-shadow:var(--shadow-float);
transform:scale(.97) translateY(6px);
transition:transform .3s var(--spring),border-radius .3s var(--ease);
}
.modal-backdrop.open .modal{transform:scale(1) translateY(0)}
.modal-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}
.modal-title{font-size:14px;font-weight:600;letter-spacing:-.01em}
.modal-close{width:26px;height:26px;border-radius:6px;border:none;background:transparent;color:var(--text-2);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s}
.modal-close:hover{background:var(--surface-2);color:var(--text)}
.modal-close svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;pointer-events:none}
.field{margin-bottom:15px}
.field label{display:block;font-size:11px;font-weight:500;color:var(--text-2);margin-bottom:6px;letter-spacing:.01em}
.field input,.field select{
width:100%;font-family:inherit;padding:9px 11px;font-size:13px;
color:var(--text);background:var(--surface-2);
border:1px solid var(--border);border-radius:8px;outline:none;
transition:border-color .16s var(--ease),box-shadow .16s var(--ease);
}
.field input:focus,.field select:focus{border-color:var(--accent-border);box-shadow:0 0 0 3px var(--accent-soft)}
.field .hint{font-size:10.5px;color:var(--text-3);margin-top:5px;line-height:1.5}
/* 风格选择器 */
.style-grid{
display:grid;
grid-template-columns:repeat(4,1fr);
gap:7px;
margin-top:2px;
}
.style-option{
position:relative;
display:flex;flex-direction:column;align-items:center;gap:6px;
padding:10px 4px 8px;
border:1px solid var(--border);
border-radius:10px;
background:var(--surface-2);
cursor:pointer;
transition:all .16s var(--ease);
font-family:inherit;
}
.style-option:hover{border-color:var(--border-hover)}
.style-option.active{
border-color:var(--accent-border);
box-shadow:0 0 0 2px var(--accent-soft);
}
.style-swatch{
width:26px;height:26px;border-radius:7px;flex:none;
box-shadow:inset 0 0 0 1px rgba(128,128,128,.15);
position:relative;overflow:hidden;
}
.style-swatch.dark-swatch{background:linear-gradient(135deg,#18181b 50%,#3b3b42 50%)}
.style-swatch.minimax-swatch{background:linear-gradient(135deg,#ffffff 45%,#ea5ec1 100%)}
.style-swatch.tech-swatch{background:linear-gradient(135deg,#0b1e3c 45%,#38bdf8 100%)}
.style-swatch.warm-swatch{background:linear-gradient(135deg,#f4f0e9 45%,#b45309 100%)}
.style-name{font-size:10.5px;font-weight:500;color:var(--text-2);letter-spacing:.01em}
.style-option.active .style-name{color:var(--accent)}
.style-check{
position:absolute;top:5px;right:5px;
width:12px;height:12px;border-radius:50%;
background:var(--accent);
display:flex;align-items:center;justify-content:center;
opacity:0;transform:scale(.5);
transition:opacity .15s,transform .2s var(--spring);
}
.style-option.active .style-check{opacity:1;transform:scale(1)}
.style-check svg{width:7px;height:7px;stroke:#fff;fill:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:round}
/* 开关 */
.switch-row{display:flex;align-items:center;justify-content:space-between;padding:11px 0;border-top:1px solid var(--border);margin-top:4px}
.switch-label{font-size:12px;font-weight:500;color:var(--text)}
.switch-desc{font-size:10.5px;color:var(--text-3);margin-top:2px;line-height:1.45}
.switch{position:relative;width:36px;height:20px;flex:none;cursor:pointer}
.switch input{opacity:0;width:0;height:0;position:absolute}
.switch-track{position:absolute;inset:0;border-radius:999px;background:var(--surface-3);border:1px solid var(--border);transition:background .2s var(--ease),border-color .2s var(--ease)}
.switch-track::after{content:'';position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:var(--text-3);transition:transform .2s var(--spring),background .2s}
.switch input:checked + .switch-track{background:var(--accent);border-color:var(--accent)}
.switch input:checked + .switch-track::after{transform:translateX(16px);background:#fff}
.modal-foot{display:flex;align-items:center;gap:10px;margin-top:20px}
.primary-btn{
font-family:inherit;font-size:12.5px;font-weight:600;color:#fff;
padding:9px 18px;border-radius:8px;border:none;cursor:pointer;
background:var(--accent-gradient);
box-shadow:0 1px 2px rgba(0,0,0,.2),inset 0 1px 0 rgba(255,255,255,.15);
transition:all .16s var(--ease);
}
.primary-btn:hover{filter:brightness(1.08)}
.primary-btn:active{transform:scale(.97)}
.save-status{font-size:11px;color:var(--green);opacity:0;transition:opacity .2s}
.save-status.show{opacity:1}
.model-fields{max-height:0;overflow:hidden;transition:max-height .35s var(--ease),opacity .25s var(--ease),margin .25s var(--ease);opacity:0;margin:0}
.model-fields.open{max-height:200px;opacity:1;margin-top:4px}
@media(max-width:600px){
.shell{padding:0 16px}
.kbd{display:none}
.search-btn{padding:0 15px;height:34px;font-size:12.5px}
.search-box{padding:4px 4px 4px 14px;border-radius:13px}
.answer{padding:17px 18px;border-radius:14px}
.hero-title{margin-bottom:22px}
.result-card{padding:13px 14px}
.style-grid{grid-template-columns:repeat(2,1fr)}
}
</style>
</head>
<body>
<div class="ambient"></div>
<div class="shell">
<header class="top">
<div class="brand"><div class="brand-mark"></div><span>DeepSeek <em>Search</em></span></div>
<div class="actions">
<button type="button" class="icon-btn" id="themeBtn" title="切换明暗">
<svg viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
</button>
<button type="button" class="icon-btn" id="settingsBtn" title="设置">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9c.14.36.44.65.8.78H21a2 2 0 0 1 0 4h-.09c-.36.13-.66.42-.8.78z"/></svg>
</button>
</div>
</header>
<main class="main" id="main">
<div class="hero">
<h1 class="hero-title">搜索,然后理解</h1>
<div class="search-box">
<svg class="search-icon" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input class="search-input" id="searchInput" placeholder="搜索或输入问题..." autocomplete="off" spellcheck="false">
<kbd class="kbd">⌘K</kbd>
<button type="button" class="search-btn" id="searchBtn">搜索</button>
</div>
<div class="chips">
<button type="button" class="chip" data-q="DeepSeek 最新模型发布">DeepSeek 新模型</button>
<button type="button" class="chip" data-q="今天北京天气">今日天气</button>
<button type="button" class="chip" data-q="2026 年 AI 行业重要进展">AI 行业进展</button>
<button type="button" class="chip" data-q="Python 3.14 新特性">Python 3.14</button>
</div>
</div>
<div class="content" id="content">
<div class="notice" id="noticeBox"></div>
<div class="error-box" id="errorBox"></div>
<div class="answer" id="answerCard">
<div class="answer-head">综合回答</div>
<div class="answer-body" id="answerBody"></div>
</div>
<div class="results-head" id="resultsHead">
<span class="results-title">搜索结果</span>
<span class="results-count" id="resultsCount"></span>
</div>
<div class="result-list" id="resultList"></div>
</div>
</main>
</div>
<!-- 设置 -->
<div class="modal-backdrop" id="modal">
<div class="modal">
<div class="modal-head">
<div class="modal-title">设置</div>
<button type="button" class="modal-close" id="modalClose">
<svg viewBox="0 0 24 24"><path d="M18 6 6 18M6 6l12 12"/></svg>
</button>
</div>
<div class="field">
<label>API Key</label>
<input type="password" id="apiKeyInput" placeholder="sk-..." autocomplete="off">
<div class="hint">仅保存在你的浏览器里,不会上传到任何服务器。</div>
</div>
<div class="field">
<label>界面风格</label>
<div class="style-grid" id="styleGrid">
<button type="button" class="style-option" data-style="dark">
<div class="style-swatch dark-swatch"></div>
<span class="style-name">暗色</span>
<span class="style-check"><svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg></span>
</button>
<button type="button" class="style-option" data-style="minimax">
<div class="style-swatch minimax-swatch"></div>
<span class="style-name">极简白</span>
<span class="style-check"><svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg></span>
</button>
<button type="button" class="style-option" data-style="tech">
<div class="style-swatch tech-swatch"></div>
<span class="style-name">科技蓝</span>
<span class="style-check"><svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg></span>
</button>
<button type="button" class="style-option" data-style="warm">
<div class="style-swatch warm-swatch"></div>
<span class="style-name">暖砂</span>
<span class="style-check"><svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg></span>
</button>
</div>
<div class="hint">切换即时生效,不用保存。</div>
</div>
<div class="field">
<label>每次结果数量</label>
<select id="countSelect">
<option value="5">5 条</option>
<option value="10" selected>10 条</option>
<option value="20">20 条</option>
<option value="30">30 条</option>
<option value="50">50 条</option>
</select>
</div>
<div class="switch-row">
<div>
<div class="switch-label">AI 综合回答</div>
<div class="switch-desc">开启后,基于搜索结果生成一段综合回答。关闭则只显示结果列表。</div>
</div>
<label class="switch">
<input type="checkbox" id="modelToggle">
<span class="switch-track"></span>
</label>
</div>
<div class="model-fields" id="modelFields">
<div class="field">
<label>模型</label>
<input type="text" id="modelInput" list="modelList" value="deepseek-chat">
<datalist id="modelList">
<option value="deepseek-chat">
<option value="deepseek-reasoner">
</datalist>
</div>
</div>
<div class="modal-foot">
<button type="button" class="primary-btn" id="saveBtn">保存</button>
<span class="save-status" id="saveStatus">已保存</span>
</div>
</div>
</div>
<script>
(function(){
"use strict";
function log(){ try{ console.log.apply(console, ["[DS]"].concat([].slice.call(arguments))); }catch(_){} }
function showFatal(msg){
log("FATAL:", msg);
try{
var box = document.getElementById("errorBox");
if(box){
box.textContent = "脚本错误:" + msg + "\n(F12 查看控制台)";
box.classList.add("visible");
box.style.display = "block";
}
}catch(_){}
}
window.addEventListener("error", function(e){
showFatal((e && (e.message || String(e.error))) || "未知错误");
});
var store = {
get: function(k){ try{ return localStorage.getItem(k); }catch(e){ log("storage.get 失败:", e.message); return null; } },
set: function(k,v){ try{ localStorage.setItem(k,v); }catch(e){ log("storage.set 失败:", e.message); } }
};
function onClick(el, fn){
if(!el) return;
el.addEventListener("click", function(e){
try{ fn(e); }catch(err){ showFatal("点击处理出错: " + err.message); }
});
}
function ready(fn){
if(document.readyState === "loading") document.addEventListener("DOMContentLoaded", fn);
else fn();
}
ready(function(){
try{ init(); }catch(e){ showFatal(e.message || String(e)); }
});
function init(){
var $ = function(s){ return document.querySelector(s); };
var $$ = function(s){ return Array.prototype.slice.call(document.querySelectorAll(s)); };
var els = {
main: $("#main"),
searchInput: $("#searchInput"),
searchBtn: $("#searchBtn"),
answerCard: $("#answerCard"),
answerBody: $("#answerBody"),
resultsHead: $("#resultsHead"),
resultsCount: $("#resultsCount"),
resultList: $("#resultList"),
noticeBox: $("#noticeBox"),
errorBox: $("#errorBox"),
modal: $("#modal"),
themeBtn: $("#themeBtn"),
settingsBtn: $("#settingsBtn"),
modalClose: $("#modalClose"),
saveBtn: $("#saveBtn"),
saveStatus: $("#saveStatus"),
apiKeyInput: $("#apiKeyInput"),
countSelect: $("#countSelect"),
modelToggle: $("#modelToggle"),
modelFields: $("#modelFields"),
modelInput: $("#modelInput"),
styleGrid: $("#styleGrid"),
styleOptions: $$(".style-option")
};
var missing = [];
for(var k in els){ if(!els[k]) missing.push(k); }
if(missing.length){ showFatal("关键元素缺失: " + missing.join(", ")); return; }
var STORE_KEY = "ds_search_v6";
var controller = null;
var busy = false;
var currentStyle = "dark";
/* ============================================================
安全:URL 白名单校验
只放行 http / https。javascript:、data:、vbscript:、
blob:、file: 等一律返回空串,由调用方做降级处理。
============================================================ */
function safeUrl(u){
if(!u || typeof u !== "string") return "";
try{
var p = new URL(u, location.href);
if(p.protocol === "http:" || p.protocol === "https:"){
return p.href;
}
}catch(_){}
return "";
}
/* ============================================================
风格切换
============================================================ */
function applyStyle(name){
document.documentElement.setAttribute("data-style", name);
currentStyle = name;
els.styleOptions.forEach(function(btn){
btn.classList.toggle("active", btn.getAttribute("data-style") === name);
});
var isDark = (name === "dark" || name === "tech");
document.documentElement.setAttribute("data-theme", isDark ? "dark" : "light");
store.set(STORE_KEY + "_style", name);
log("风格切换为:", name);
}
(function initStyle(){
try{
var saved = store.get(STORE_KEY + "_style") || "dark";
applyStyle(saved);
els.styleOptions.forEach(function(btn){
onClick(btn, function(){
applyStyle(btn.getAttribute("data-style"));
});
});
}catch(e){ showFatal("风格初始化失败: " + e.message); }
})();
/* ============================================================
明暗快捷切换
============================================================ */
(function initTheme(){
try{
onClick(els.themeBtn, function(){
var next;
if(currentStyle === "dark") next = "minimax";
else if(currentStyle === "minimax") next = "dark";
else if(currentStyle === "tech") next = "warm";
else next = "tech";
applyStyle(next);
});
}catch(e){ showFatal("主题初始化失败: " + e.message); }
})();
/* ============================================================
设置
============================================================ */
(function initSettings(){
try{
var raw = store.get(STORE_KEY);
if(raw){
try{
var p = JSON.parse(raw);
if(p.apiKey) els.apiKeyInput.value = p.apiKey;
if(p.count) els.countSelect.value = String(p.count);
if(p.model) els.modelInput.value = p.model;
if(p.useModel === true){
els.modelToggle.checked = true;
els.modelFields.classList.add("open");
}
}catch(_){}
}
els.modelToggle.addEventListener("change", function(){
els.modelFields.classList.toggle("open", els.modelToggle.checked);
});
onClick(els.settingsBtn, function(){ els.modal.classList.add("open"); });
onClick(els.modalClose, function(){ els.modal.classList.remove("open"); });
onClick(els.modal, function(e){ if(e.target === els.modal) els.modal.classList.remove("open"); });
onClick(els.saveBtn, function(){
store.set(STORE_KEY, JSON.stringify({
apiKey: els.apiKeyInput.value.trim(),
count: parseInt(els.countSelect.value, 10) || 10,
useModel: els.modelToggle.checked,
model: els.modelInput.value.trim() || "deepseek-chat"
}));
els.saveStatus.classList.add("show");
setTimeout(function(){ els.saveStatus.classList.remove("show"); }, 1600);
});
}catch(e){ showFatal("设置初始化失败: " + e.message); }
})();
/* ============================================================
配置 / 工具
============================================================ */
function getConfig(){
var key = els.apiKeyInput.value.trim();
if(!key){
showErr("请先在设置中填入 API Key。");
els.modal.classList.add("open");
return null;
}
return {
key: key,
count: parseInt(els.countSelect.value, 10) || 10,
useModel: els.modelToggle.checked,
model: els.modelInput.value.trim() || "deepseek-chat"
};
}
function showErr(msg){ els.errorBox.textContent = msg; els.errorBox.classList.add("visible"); }
function clearErr(){ els.errorBox.classList.remove("visible"); els.errorBox.textContent = ""; }
function showNotice(msg){ els.noticeBox.textContent = msg; els.noticeBox.classList.add("visible"); }
function clearNotice(){ els.noticeBox.classList.remove("visible"); els.noticeBox.textContent = ""; }
function setBusy(v){
busy = v;
els.searchBtn.textContent = v ? "停止" : "搜索";
els.searchBtn.classList.toggle("stop", v);
}
function escapeHtml(s){
return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
}
function safeHost(u){
try{ return new URL(u, location.href).hostname.replace(/^www\./,""); }catch(_){ return ""; }
}
function formatDate(d){
if(!d) return "";
try{
var dt = new Date(d);
if(isNaN(dt.getTime())) return "";
return dt.getFullYear() + "-" + String(dt.getMonth()+1).padStart(2,"0") + "-" + String(dt.getDate()).padStart(2,"0");
}catch(_){ return ""; }
}
function escapeRegex(s){
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/* ============================================================
Markdown
注意:占位符每次随机生成,避免 AI 输出中偶然包含私有区
字符导致误替换。先 escapeHtml 再生成标签,顺序不能反。
============================================================ */
function mdToHtml(text){
var s = escapeHtml(text);
var nonce = "\uE000" + Math.random().toString(36).slice(2, 10) + "\uE000";
var nonceRe = escapeRegex(nonce);
var blocks = [];
s = s.replace(/```(\w*)\n?([\s\S]*?)```/g, function(_, lang, code){
var i = blocks.length;
blocks.push("<pre><code>" + code.replace(/\n$/,"") + "</code></pre>");
return nonce + "B" + i + nonce;
});
var inlines = [];
s = s.replace(/`([^`\n]+)`/g, function(_, code){
var i = inlines.length;
inlines.push("<code>" + code + "</code>");
return nonce + "I" + i + nonce;
});
s = s.replace(/^#{1,3} (.+)$/gm, "<h3>$1</h3>");
s = s.replace(/\*\*([^*\n]+)\*\*/g, "<strong>$1</strong>");
s = s.replace(/\[(\d{1,2})\]/g, '<span class="cite">$1</span>');
s = s.replace(/^[-*] (.+)$/gm, "<li>$1</li>");
s = s.replace(/(?:<li>.*<\/li>\n?)+/g, function(m){ return "<ul>" + m + "</ul>"; });
s = s.split(/\n{2,}/).map(function(par){
var t = par.trim();
if(!t) return "";
if(/^<(h3|ul|pre)/.test(t)) return t;
return "<p>" + t.replace(/\n/g, "<br>") + "</p>";
}).join("");
s = s.replace(new RegExp(nonceRe + "B(\\d+)" + nonceRe, "g"), function(_, i){ return blocks[+i]; });
s = s.replace(new RegExp(nonceRe + "I(\\d+)" + nonceRe, "g"), function(_, i){ return inlines[+i]; });
return s;
}
function renderStream(text, isStreaming){
if(isStreaming){
var fences = (text.match(/```/g) || []).length;
if(fences % 2 === 1) text += "\n```";
}
var html = mdToHtml(text);
if(isStreaming){
var idx = html.lastIndexOf("</p>");
if(idx > -1) html = html.slice(0, idx) + '<span class="caret"></span>' + html.slice(idx);
else html += '<span class="caret"></span>';
}
return html;
}
/* ============================================================
结果渲染
所有外部文本走 textContent,URL 走 safeUrl 白名单。
============================================================ */
function renderSkeleton(){
els.resultList.innerHTML = "";
els.resultsHead.classList.add("visible");
els.resultsCount.textContent = "";
for(var i = 0; i < 4; i++){
var el = document.createElement("div");
el.className = "skeleton";
el.innerHTML = '<div class="sk sk-icon"></div><div class="sk-body"><div class="sk sk-line short"></div><div class="sk sk-line med"></div><div class="sk sk-line"></div></div>';
els.resultList.appendChild(el);
}
}
function renderResults(results){
els.resultList.innerHTML = "";
if(!results || results.length === 0){
els.resultsHead.classList.remove("visible");
return;
}
els.resultsHead.classList.add("visible");
els.resultsCount.textContent = results.length + " 条结果";
var frag = document.createDocumentFragment();
results.forEach(function(r, i){
var href = safeUrl(r.url);
var a = document.createElement("a");
a.className = "result-card";
a.style.animationDelay = (i * 25) + "ms";
if(href){
a.href = href;
a.target = "_blank";
a.rel = "noopener noreferrer";
}else{
a.href = "#";
a.classList.add("is-disabled");
a.addEventListener("click", function(e){ e.preventDefault(); });
}
var icon = document.createElement("div");
icon.className = "result-icon";
var iconSrc = safeUrl(r.site_icon);
if(iconSrc){
var img = document.createElement("img");
img.src = iconSrc;
img.alt = "";
img.loading = "lazy";
img.referrerPolicy = "no-referrer";
img.onerror = function(){
img.style.display = "none";
icon.textContent = (r.site_name || safeHost(r.url) || "?").charAt(0).toUpperCase();
};
icon.appendChild(img);
}else{
icon.textContent = (r.site_name || safeHost(r.url) || "?").charAt(0).toUpperCase();
}
var main = document.createElement("div");
main.className = "result-main";
var meta = document.createElement("div");
meta.className = "result-meta";
var site = document.createElement("span");
site.className = "result-site";
site.textContent = r.site_name || safeHost(r.url) || "网页";
meta.appendChild(site);
if(r.rerank_score >= 1.5){
var d1 = document.createElement("span"); d1.className = "dot"; meta.appendChild(d1);
var b1 = document.createElement("span"); b1.className = "badge good"; b1.textContent = "优质";
meta.appendChild(b1);
}
if(r.authority >= 3){
var d2 = document.createElement("span"); d2.className = "dot"; meta.appendChild(d2);
var b2 = document.createElement("span"); b2.className = "badge auth"; b2.textContent = "权威";
meta.appendChild(b2);
}
var fd = formatDate(r.date);
if(fd){
var d3 = document.createElement("span"); d3.className = "dot"; meta.appendChild(d3);
var ds = document.createElement("span"); ds.textContent = fd;
meta.appendChild(ds);
}
var title = document.createElement("div");
title.className = "result-title";
title.textContent = r.title || "(无标题)";
main.appendChild(meta);
main.appendChild(title);
if(r.snippet){
var snip = document.createElement("div");
snip.className = "result-snippet";
snip.textContent = r.snippet;
main.appendChild(snip);
}
a.appendChild(icon);
a.appendChild(main);
frag.appendChild(a);
});
els.resultList.appendChild(frag);
}
/* ============================================================
搜索 / 模型
============================================================ */
function searchAPI(cfg, query){
return fetch("https://api.deepseek.com/v1/tools/search", {
method: "POST",
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + cfg.key },
body: JSON.stringify({ query: query, count: cfg.count }),
signal: controller ? controller.signal : undefined
}).then(function(res){
if(!res.ok){
return res.text().then(function(t){
throw new Error("搜索接口 " + res.status + (t ? ":" + t.slice(0, 200) : ""));
});
}
return res.json();
}).then(function(data){
return (data && data.results) ? data.results : [];
});
}
function chatStream(cfg, query, results){
var ctx = results.slice(0, 8).map(function(r, i){
return "[" + (i+1) + "] " + r.title + "\n" + (r.summary || r.snippet || "") + "\n来源:" + r.url;
}).join("\n\n");
var messages = [
{ role: "system", content: "你是一个专业的搜索助手。请基于提供的搜索结果,用中文给出准确、结构清晰的综合回答。可以使用 Markdown。引用来源时用 [1] [2] 这样的角标对应结果序号。若结果不足以回答,如实说明。" },
{ role: "user", content: "用户问题:" + query + "\n\n搜索结果:\n" + (ctx || "(本次没有返回搜索结果,请基于你的知识回答)") }
];
return fetch("https://api.deepseek.com/chat/completions", {
method: "POST",
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + cfg.key },
body: JSON.stringify({ model: cfg.model, messages: messages, stream: true, temperature: 0.3 }),
signal: controller.signal
}).then(function(res){
if(!res.ok){
return res.text().then(function(t){
throw new Error("对话接口 " + res.status + (t ? ":" + t.slice(0, 300) : ""));
});
}
if(!res.body) throw new Error("浏览器不支持流式响应。");
return res.body;
});
}
function readStream(body, onDelta){
var reader = body.getReader();
var decoder = new TextDecoder();
var buffer = "";
var full = "";
var raf = 0;
function flush(){ raf = 0; onDelta(full, true); }
function pump(){
return reader.read().then(function(chunk){
if(chunk.done){
if(raf) cancelAnimationFrame(raf);
onDelta(full, false);
return;
}
buffer += decoder.decode(chunk.value, { stream: true });
var lines = buffer.split("\n");
buffer = lines.pop() || "";
for(var i = 0; i < lines.length; i++){
var t = lines[i].trim();
if(t.indexOf("data:") !== 0) continue;
var payload = t.slice(5).trim();
if(payload === "[DONE]") continue;
try{
var json = JSON.parse(payload);
if(json.error) throw new Error(json.error.message || JSON.stringify(json.error));
var delta = json.choices && json.choices[0] && json.choices[0].delta;
var piece = delta && delta.content;
if(piece){ full += piece; if(!raf) raf = requestAnimationFrame(flush); }
}catch(e){
if(e.message && e.message.indexOf("JSON") === -1) throw e;
}
}
return pump();
});
}
return pump();
}
/* ============================================================
主流程
============================================================ */
function doSearch(query){
if(busy){
if(controller) try{ controller.abort(); }catch(_){}
return;
}
var cfg = getConfig();
if(!cfg) return;
clearErr();
clearNotice();
els.main.classList.add("active");
els.answerCard.classList.remove("visible");
els.answerBody.innerHTML = "";
els.resultList.innerHTML = "";
els.resultsHead.classList.remove("visible");
setBusy(true);
controller = new AbortController();
renderSkeleton();
var results = [];
searchAPI(cfg, query).then(function(r){
results = r;
renderResults(results);
}, function(e){
if(e.name === "AbortError") throw e;
renderResults([]);
showNotice("搜索失败(" + e.message + "),请检查 API Key 或网络。");
}).then(function(){
if(!cfg.useModel) return null;
els.answerCard.classList.add("visible");
els.answerBody.innerHTML = renderStream("", true);
return chatStream(cfg, query, results).then(function(body){
return readStream(body, function(text, streaming){
els.answerBody.innerHTML = renderStream(text, streaming);
});
});
}).catch(function(err){
if(err.name === "AbortError"){
var cur = els.answerBody.textContent || "";
if(cur) els.answerBody.innerHTML = renderStream(cur + "\n\n(已停止)", false);
}else{
showErr(err.message || "请求失败。");
}
}).then(function(){
controller = null;
setBusy(false);
});
}
/* ============================================================
事件
============================================================ */
onClick(els.searchBtn, function(){
if(busy){
if(controller) try{ controller.abort(); }catch(_){}
return;
}
var q = els.searchInput.value.trim();
if(!q){ els.searchInput.focus(); return; }
doSearch(q);
});
els.searchInput.addEventListener("keydown", function(e){
if(e.key === "Enter" && !e.isComposing){
e.preventDefault();
els.searchBtn.click();
}
});
var chips = document.querySelectorAll(".chip");
for(var i = 0; i < chips.length; i++){
(function(chip){
onClick(chip, function(){
var q = chip.getAttribute("data-q");
els.searchInput.value = q;
doSearch(q);
});
})(chips[i]);
}
document.addEventListener("keydown", function(e){
if((e.metaKey || e.ctrlKey) && e.key && e.key.toLowerCase() === "k"){
e.preventDefault();
els.searchInput.focus();
els.searchInput.select();
}
if(e.key === "Escape"){
if(els.modal.classList.contains("open")) els.modal.classList.remove("open");
else els.searchInput.blur();
}
});
try{ els.searchInput.focus(); }catch(_){}
log("初始化完成,当前风格:", currentStyle);
}
})();
</script>
</body>
</html>
四、使用步骤
- 新建文件
index.html,把上面的代码完整保存进去。 - 用浏览器打开(双击即可)。
- 点右上角齿轮,填入 DeepSeek API Key,保存。
- 在搜索框输入内容,回车。
API Key 在 platform.deepseek.com → API Keys 里创建,格式是 sk- 开头。
五、先说缺陷,再谈部署
缺陷一:要钱。
这是最重要的一条,放在最前面。DeepSeek 的 Search API 和 Chat Completions 都是计费接口,按调用量或 token 计费,不是免费额度无限薅的那种。
- 只开搜索:每次搜索消耗一次 Search API 调用。
- 同时开 AI 回答:除了 Search,还会再消耗一次 Chat 的 token(输入 + 输出)。
所以如果你一天搜几百次、还开着 AI 综合回答,账单是真的会来的。这不是一个「免费替代搜索引擎」的项目,它更像「用 API 额度换一个干净的搜索界面」。日常偶尔用问题不大,高频使用请先看一下 DeepSeek 的定价页面,心里有个数。
缺陷二:纯前端直连,Key 在浏览器里。
这个项目没有后端,API Key 存在 localStorage,请求直接从浏览器发到 api.deepseek.com。这意味着:
localStorage按源隔离,你存在自己浏览器里的 Key,别人看不到。部署到公网也不会把你的 Key 暴露给访客。- 但如果页面存在 XSS 漏洞、或者装了恶意浏览器扩展,当前浏览器里的 Key 就有被读走的风险。这份代码已经做了
safeUrl白名单、textContent渲染、Markdown 先转义等防护,堵掉了主路径,但localStorage对同源脚本可读这一点是架构决定的,前端代码再怎么改也改不掉。 - 如果你想用自己的 Key 给所有访客用,那必须加后端代理,Key 放服务端。这种情况纯前端方案不成立。
缺陷三:Search API 的字段和端点可能变。
代码里写死了 https://api.deepseek.com/v1/tools/search,读的是 data.results。DeepSeek 如果调整端点路径、请求体字段或者返回结构,这份代码需要跟着改。目前它对 results、data、documents 几种常见返回结构做了兼容,字段也做了 title/name、url/link、snippet/summary 多路读取,但没法覆盖所有未来的变化。
缺陷四:没有历史记录、没有分页、没有高级语法。
它是一个「一次搜索一次展示」的页面。不存搜索历史,不支持 site: 之类的搜索语法(取决于 API 本身),结果数量在设置里固定,没有「加载更多」。这些不是 bug,是刻意没做------加了就不是「单文件打开即用」了。
六、关于部署和密钥安全
把缺陷说清楚之后,部署这件事就简单了:
- 个人本地用:没问题。Key 存自己浏览器,页面本身有 XSS 防护,风险可控。
- 部署到公网、每个用户填自己的 Key :架构成立。每个人各填各的、各存各的
localStorage,互相不可见。你的 Key 不会因为部署而泄露。 - 部署到公网、用自己的 Key 给所有人用:不成立。这种情况必须上后端代理,Key 放服务端环境变量,前端只和你的代理通信。
- 无论哪种情况:不要把 Key 硬编码在源码里发布。
代理本身很轻,Node.js + Express 三四十行就够,只做转发和 Authorization 头注入。需要的话可以后面单独写一篇。
七、实现细节
为什么用 Search API,而不是让 chat 模型直接联网? Search API 返回的是结构化结果,有 title、url、snippet、site_name、site_icon、rerank_score、authority、date,可以直接渲染成卡片。让 chat 模型联网的话,搜索结果是个黑盒,客户端拿不到 URL 和标题,做不成结果列表。
AI 回答为什么默认关闭? 搜一个具体名词、查一个网页,看列表就够了。默认只做搜索,更快、更省钱;需要综合回答的时候在设置里打开。
引用角标怎么做的? 在 system prompt 里要求模型用 [1] [2] 对应结果序号,前端用正则 \[(\d{1,2})\] 替换成 <span class="cite">1</span>,加个底色和字号就成了角标。
流式输出为什么用 requestAnimationFrame? 模型吐字可能比浏览器帧率还快,每个 chunk 都触发 DOM 更新会卡。用 rAF 做节流,一帧内多次 chunk 只渲染一次。
Markdown 为什么不用第三方库? 只做了代码块、行内代码、标题、粗体、列表、引用角标、段落这几个最常用的。够用就行,引一个 marked.js 进来反而是杀鸡用牛刀,还多一个外部依赖。
结语
这个项目的定位很明确:一个不需要部署、打开就能用的搜索界面,用 API 额度换干净的体验。它不免费,不能替代搜索引擎,也没有历史记录和账号体系。但如果你本来就在用 DeepSeek 的 API,想要一个比官方控制台舒服得多的搜索界面,这份代码拿去改改就能用。
CSS 变量和 JS 结构都往好改的方向写了。想加第五套风格、想换默认模型、想调整结果卡片的信息密度,都在对应位置改就行。
本文源码基于 DeepSeek Search API 编写,使用时请遵守 DeepSeek 的服务条款。API Key 请妥善保管,不要泄露;Search 与 Chat 接口均为计费服务,请留意用量。