diff --git a/README.md b/README.md
index 1e19015..dc82e35 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
- 精准名词直接生成快速总结;未知名词会尝试从中文维基百科获取有来源的入门资料。
- 模糊名词先澄清含义,再按所选方向学习。
- 公开网页提取标题、摘要、章节与正文要点。
-- 围绕当前主题继续询问用法、示例和误区。
+- 围绕当前主题继续询问用法、示例和误区;百科主题优先依据已取得的来源摘录回答。
- 浮窗可以隐藏为右下角入口并随时恢复。
## 运行
diff --git a/app.js b/app.js
index fc2b3dd..2110f36 100644
--- a/app.js
+++ b/app.js
@@ -85,6 +85,10 @@ function renderSummary(result) {
const source = result.source ? `` : "";
const concepts = result.concepts.slice(0, 3).map(([name, detail], index) => `
${escapeHtml(name)} · ${escapeHtml(detail)}`).join("");
const steps = result.steps.slice(0, 3).map((step, index) => `${index + 1}${escapeHtml(step)}`).join("");
+ const questions = result.source?.provider === "维基百科"
+ ? [["请用简单的话解释", "简单解释"], ["再讲详细一点", "再讲详细"], ["还有什么重要背景?", "重要背景"]]
+ : [["如何使用?", "如何使用?"], ["给我一个简单的例子", "给我例子"], ["有什么常见误区?", "常见误区"]];
+ const quickQuestions = questions.map(([question, label]) => ``).join("");
$("#conversation").innerHTML = `
@@ -94,9 +98,7 @@ function renderSummary(result) {
核心理解它是什么,为什么值得了解
${escapeHtml(result.definition)}
-
-
-
+ ${quickQuestions}
`;
showView("conversation");
diff --git a/server.mjs b/server.mjs
index 2ea8fc5..2cfbee4 100644
--- a/server.mjs
+++ b/server.mjs
@@ -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 {
diff --git a/test/smoke.test.mjs b/test/smoke.test.mjs
index 31a3d58..038fda8 100644
--- a/test/smoke.test.mjs
+++ b/test/smoke.test.mjs
@@ -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",
diff --git a/test/wikipedia.test.mjs b/test/wikipedia.test.mjs
index 266cf81..63eb1d1 100644
--- a/test/wikipedia.test.mjs
+++ b/test/wikipedia.test.mjs
@@ -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");
diff --git a/wikipedia.mjs b/wikipedia.mjs
index 11eec43..7a88e79 100644
--- a/wikipedia.mjs
+++ b/wikipedia.mjs
@@ -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)
}
};
}