Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
- 精准名词直接生成快速总结;未知名词会尝试从中文维基百科获取有来源的入门资料。
- 模糊名词先澄清含义,再按所选方向学习。
- 公开网页提取标题、摘要、章节与正文要点。
- 围绕当前主题继续询问用法、示例和误区。
- 围绕当前主题继续询问用法、示例和误区;百科主题优先依据已取得的来源摘录回答
- 浮窗可以隐藏为右下角入口并随时恢复。

## 运行
Expand Down
8 changes: 5 additions & 3 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ function renderSummary(result) {
const source = result.source ? `<a class="source-button" href="${escapeHtml(result.source.url)}" target="_blank" rel="noreferrer" aria-label="打开来源:${escapeHtml(sourceName)}" title="${escapeHtml(sourceName)}"><i data-lucide="external-link"></i></a>` : "";
const concepts = result.concepts.slice(0, 3).map(([name, detail], index) => `<li data-index="${String(index + 1).padStart(2, "0")}"><span><strong>${escapeHtml(name)}</strong> · ${escapeHtml(detail)}</span></li>`).join("");
const steps = result.steps.slice(0, 3).map((step, index) => `<li><span>${index + 1}</span>${escapeHtml(step)}</li>`).join("");
const questions = result.source?.provider === "维基百科"
? [["请用简单的话解释", "简单解释"], ["再讲详细一点", "再讲详细"], ["还有什么重要背景?", "重要背景"]]
: [["如何使用?", "如何使用?"], ["给我一个简单的例子", "给我例子"], ["有什么常见误区?", "常见误区"]];
const quickQuestions = questions.map(([question, label]) => `<button data-question="${escapeHtml(question)}">${escapeHtml(label)}</button>`).join("");

$("#conversation").innerHTML = `
<article class="assistant-message">
Expand All @@ -94,9 +98,7 @@ function renderSummary(result) {
<section class="mini-section"><span>核心理解</span><h2>它是什么,为什么值得了解</h2><p>${escapeHtml(result.definition)}</p><ul class="concept-list">${concepts}</ul></section>
<section class="mini-section"><span>马上上手</span><ul class="step-list">${steps}</ul></section>
<div class="quick-questions">
<button data-question="如何使用?">如何使用?</button>
<button data-question="给我一个简单的例子">给我一个例子</button>
<button data-question="有什么常见误区?">常见误区</button>
${quickQuestions}
</div>
</article>`;
showView("conversation");
Expand Down
36 changes: 36 additions & 0 deletions server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -309,12 +309,48 @@ const exampleLibrary = new Map([
["ai 智能体", ["做一个只读日程助手:它读取今天的日程,判断冲突,再给出调整建议;真正改动日程前必须请用户确认。", "观察:读取日程", "判断:识别冲突", "行动:确认后再修改"]]
]);

const sourceIntents = [
{ question: /怎么用|如何用|如何使用|应用|用途|上手/, facts: /应用|用于|用来|领域|技术|实践/, title: "来源中的应用" },
{ question: /例子|示例|案例|举例/, facts: /例如|比如|包括|像|譬如|举例/, title: "来源中的例子" },
{ question: /历史|起源|发明|提出|发现|谁|何时|哪年/, facts: /年|世纪|提出|发现|发明|创立|发展|历史/, title: "背景与历史" },
{ question: /原理|机制|为什么|如何工作|怎么工作/, facts: /由于|通过|因此|原理|机制|作用|导致|关系/, title: "原理与机制" },
{ question: /限制|风险|缺点|争议|问题|误区/, facts: /限制|风险|缺点|争议|问题|误解|错误/, title: "边界与限制" },
{ question: /简单|没懂|解释|白话|是什么|什么意思/, title: "根据来源解释", start: 0 },
{ question: /更多|详细|深入|背景|继续/, title: "进一步理解", start: 2 }
];

function sourceFollowUp(question, context) {
const excerpts = Array.isArray(context?.source?.excerpts)
? context.source.excerpts
.filter((item) => typeof item === "string")
.map((item) => item.trim().slice(0, 280))
.filter(Boolean)
.slice(0, 6)
: [];
if (context?.source?.provider !== "维基百科" || !excerpts.length) return null;

const intent = sourceIntents.find((item) => item.question.test(question));
const matches = intent?.facts
? excerpts.filter((item) => intent.facts.test(item))
: intent ? excerpts.slice(intent.start ?? 0) : [];
if (!intent || !matches.length) {
return {
title: "现有资料还不够",
text: `当前来源摘要没有足够信息可靠回答“${question}”。我不会根据缺失内容补写结论。`,
bullets: excerpts.slice(0, 2).map((item) => `已确认:${item}`)
};
}
return { title: intent.title, text: matches[0], bullets: matches.slice(1, 4) };
}

function followUpResult(question, context) {
const topic = context?.title || "这个主题";
const concepts = Array.isArray(context?.concepts) ? context.concepts : [];
const steps = Array.isArray(context?.steps) ? context.steps : [];
const takeaways = Array.isArray(context?.takeaways) ? context.takeaways : [];
const normalized = question.toLowerCase();
const sourced = sourceFollowUp(question, context);
if (sourced) return sourced;

if (/怎么用|如何用|如何使用|上手|开始/.test(normalized)) {
return {
Expand Down
26 changes: 26 additions & 0 deletions test/smoke.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,32 @@ test("answers usage and example follow-ups", async () => {
assert.match(example.text, /MCP Server/);
});

test("grounds Wikipedia follow-ups in bounded source excerpts", async () => {
const context = {
title: "量子纠缠",
source: {
provider: "维基百科",
excerpts: [
"量子纠缠是多个量子系统之间无法独立描述的关联现象。",
"量子纠缠可以用于量子通信、量子计算和量子密码学等技术领域。",
"例如,纠缠光子可以用来检验两个测量结果之间的统计关联。"
]
}
};

const usage = await post("/api/ask", { question: "它有哪些应用?", context });
assert.equal(usage.title, "来源中的应用");
assert.match(usage.text, /量子通信/);

const example = await post("/api/ask", { question: "再给我一个例子", context });
assert.equal(example.title, "来源中的例子");
assert.match(example.text, /纠缠光子/);

const unsupported = await post("/api/ask", { question: "它在火星上的售价是多少?", context });
assert.equal(unsupported.title, "现有资料还不够");
assert.match(unsupported.text, /不会根据缺失内容补写结论/);
});

test("does not fetch non-public network pages", async () => {
const blockedUrls = [
"http://127.0.0.1:4173",
Expand Down
2 changes: 2 additions & 0 deletions test/wikipedia.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ test("builds a sourced learning result from the most relevant page", async () =>
assert.equal(result.steps.length, 3);
assert.equal(result.source.provider, "维基百科");
assert.equal(result.source.license, "CC BY-SA 4.0");
assert.ok(result.source.excerpts.length >= 3);
assert.ok(result.source.excerpts.every((item) => item.length <= 280));
assert.match(result.source.url, /%E9%87%8F%E5%AD%90%E7%B3%BE%E7%BA%8F/);
assert.equal(requestedUrl.searchParams.get("gsrsearch"), "量子纠缠");
assert.equal(requestedUrl.searchParams.get("gsrnamespace"), "0");
Expand Down
3 changes: 2 additions & 1 deletion wikipedia.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ function learningResult(page) {
host: "zh.wikipedia.org",
title: page.title,
provider: "维基百科",
license: "CC BY-SA 4.0"
license: "CC BY-SA 4.0",
excerpts: facts.map((item) => item.slice(0, 280)).slice(0, 6)
}
};
}
Expand Down