-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.mjs
More file actions
558 lines (523 loc) · 34 KB
/
Copy pathserver.mjs
File metadata and controls
558 lines (523 loc) · 34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
import http from "node:http";
import { appendFile, chmod, mkdir, readFile } from "node:fs/promises";
import { dirname, extname, join, normalize } from "node:path";
import { fileURLToPath } from "node:url";
import { randomUUID } from "node:crypto";
import { fetchPage } from "./url-fetch.mjs";
import { lookupWikipedia } from "./wikipedia.mjs";
const root = fileURLToPath(new URL(".", import.meta.url));
const port = Number(process.env.PORT || 4173);
const host = process.env.HOST || "127.0.0.1";
const dataDirectory = process.env.QUICKLEARN_DATA_DIR || join(root, ".data");
const publicBinding = !["127.0.0.1", "localhost", "::1"].includes(host);
const commercialStorageReady = !publicBinding || Boolean(process.env.QUICKLEARN_DATA_DIR);
const leadFile = process.env.QUICKLEARN_LEAD_FILE || join(dataDirectory, "paid-pack-interests.jsonl");
const commercialEventFile = process.env.QUICKLEARN_EVENT_FILE || join(dataDirectory, "paid-pack-events.jsonl");
let leadRequests = [];
let commercialEventRequests = [];
const leadRateWindowMs = 10 * 60 * 1000;
const leadRateLimit = 30;
const commercialEvents = new Set(["offer_view", "offer_open"]);
const publicFiles = new Set(["/index.html", "/app.js", "/styles.css"]);
function cleanLeadText(value) {
return typeof value === "string"
? value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim()
: "";
}
const mimeTypes = {
".html": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".svg": "image/svg+xml",
".png": "image/png",
".ico": "image/x-icon"
};
const knowledge = [
{
match: ["prompt engineering", "提示词工程", "prompt"],
title: "提示词工程",
category: "AI 基础",
summary: "提示词工程,是把任务背景、目标、限制和输出标准组织成清晰指令,让模型更稳定地交付可验证结果。",
definition: "它不是寻找一句万能咒语,而是一套需求表达、上下文管理、结果约束和迭代评估的方法。",
why: "当任务复杂或需要重复执行时,结构化提示能减少歧义、提高一致性,也让失败更容易定位。",
concepts: [
["任务边界", "明确要解决什么,以及哪些事情不在本次范围内。"],
["上下文", "补足模型做判断所需的事实、样例和受众信息。"],
["输出契约", "规定格式、质量标准、长度和必须包含的内容。"],
["验证回路", "用测试案例或评分标准检查结果,再定向迭代。"]
],
takeaways: ["先写清目标,再补背景", "用示例固定抽象标准", "把复杂任务拆成可检查步骤"],
misconception: "提示越长不一定越好。有效信息密度和明确的验收标准,比堆叠修饰语更重要。",
steps: ["认识提示的四个组成部分", "把模糊需求改写成结构化任务", "加入示例与边界条件", "建立一套结果评分表"],
cards: [
["一个可靠提示的核心结构?", "背景 + 目标 + 约束 + 输出格式 + 验收标准。"],
["什么时候应该提供示例?", "当输出风格、分类边界或抽象标准难以只用语言描述时。"],
["如何减少模型编造?", "限定可用信息源,要求区分事实与推断,并对关键结论给出依据。"]
],
quiz: {
question: "下面哪一种做法最能提升重复任务的一致性?",
options: ["不断增加形容词", "定义输出结构和评分标准", "每次换一种表达", "删掉全部上下文"],
answer: 1,
explanation: "输出契约和评分标准让结果变得可比较,也让后续迭代有明确方向。"
}
},
{
match: ["mcp", "model context protocol", "模型上下文协议"],
title: "MCP",
category: "AI 工程",
summary: "MCP 是一种让 AI 应用以统一方式连接工具、数据和外部能力的开放协议。",
definition: "可以把它理解为 AI 应用的通用扩展接口:服务端暴露工具、资源或提示,客户端发现并调用它们。",
why: "它把每个 AI 应用与每个数据源之间的定制集成,收敛成可复用的协议边界。",
concepts: [["Host", "承载 AI 体验并管理连接。"], ["Client", "与一个 MCP Server 保持协议会话。"], ["Server", "提供工具、资源和提示能力。"], ["Transport", "负责本地或远程消息传递。"]],
takeaways: ["协议统一能力描述", "工具执行仍需权限控制", "服务端应保持边界清晰"],
misconception: "MCP 本身不是模型,也不会自动保证工具安全;权限、审计和输入校验仍由应用负责。",
steps: ["理解 Host、Client、Server 的关系", "运行一个本地 MCP Server", "暴露只读工具并连接客户端", "加入鉴权与错误处理"],
cards: [["MCP 解决的核心问题?", "让 AI 应用用统一协议发现和调用外部能力。"], ["工具和资源有什么区别?", "工具执行动作;资源向模型提供可读取的上下文。"], ["为什么需要权限确认?", "工具可能产生外部副作用,调用前需要明确授权边界。"]],
quiz: { question: "在 MCP 中,通常由谁提供可调用工具?", options: ["Host", "Server", "模型权重", "浏览器 DOM"], answer: 1, explanation: "MCP Server 声明并实现工具,Host 通过 Client 与它建立连接。" }
},
{
match: ["react", "react.js", "reactjs"],
title: "React",
category: "前端开发",
summary: "React 是用组件描述用户界面、并根据状态变化更新页面的 JavaScript 库。",
definition: "开发者把界面拆成可组合组件,用 props 传入信息,用 state 表达会随交互改变的数据。",
why: "它让复杂交互界面可以按组件分工,并用单向数据流降低状态变化的理解成本。",
concepts: [["组件", "返回界面描述的可复用单元。"], ["Props", "父组件传给子组件的只读输入。"], ["State", "组件内部会随交互变化的数据。"], ["Effect", "与组件外部系统同步的机制。"]],
takeaways: ["界面是状态的映射", "数据默认从上向下流动", "Effect 只用于外部同步"],
misconception: "React 不是完整框架,也不要求所有数据都放进 state;可计算的数据通常应在渲染时得到。",
steps: ["用 JSX 创建第一个组件", "用 props 组合组件", "用 state 响应交互", "完成一个可筛选列表"],
cards: [["什么时候使用 state?", "当数据会随交互变化,并且变化需要触发界面更新时。"], ["key 的作用是什么?", "帮助 React 在列表更新时识别元素身份。"], ["Effect 适合处理什么?", "网络、订阅、浏览器 API 等组件外部系统的同步。"]],
quiz: { question: "哪类数据通常不需要单独存入 state?", options: ["输入框内容", "由现有 props 可直接计算的值", "当前选中的标签", "异步请求状态"], answer: 1, explanation: "可由已有数据直接计算的值应在渲染时得出,避免产生不同步的重复状态。" }
},
{
match: ["docker", "容器"],
title: "Docker",
category: "开发运维",
summary: "Docker 把应用及其依赖打包成可移植镜像,并以隔离的容器进程运行。",
definition: "镜像是只读模板,容器是镜像的运行实例;Dockerfile 则记录构建镜像的步骤。",
why: "它减少环境差异,让开发、测试和部署使用相同的软件与依赖组合。",
concepts: [["Image", "包含应用与依赖的分层模板。"], ["Container", "镜像启动后的隔离进程。"], ["Volume", "独立于容器生命周期保存数据。"], ["Network", "让容器之间或外部进行通信。"]],
takeaways: ["镜像应可重复构建", "容器默认是临时的", "持久数据交给 Volume"],
misconception: "容器不是轻量虚拟机。它共享宿主机内核,隔离边界和安全模型也不同。",
steps: ["运行一个现成镜像", "编写最小 Dockerfile", "挂载 Volume 保存数据", "用 Compose 连接两个服务"],
cards: [["镜像与容器的区别?", "镜像是模板,容器是它的运行实例。"], ["为什么使用 Volume?", "让数据在容器删除或重建后继续存在。"], ["Dockerfile 的价值?", "把环境构建过程写成可审查、可重复执行的步骤。"]],
quiz: { question: "容器删除后仍需保留的数据应该放在哪里?", options: ["镜像层", "容器临时层", "Volume", "环境变量"], answer: 2, explanation: "Volume 的生命周期独立于容器,适合保存数据库等持久数据。" }
}
];
function sendJson(res, status, data) {
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify(data));
}
function decodeEntities(value = "") {
return value
.replace(/ /gi, " ")
.replace(/&/gi, "&")
.replace(/"/gi, '"')
.replace(/'|'/gi, "'")
.replace(/</gi, "<")
.replace(/>/gi, ">");
}
function plainText(html = "") {
return decodeEntities(html)
.replace(/<script[\s\S]*?<\/script>/gi, " ")
.replace(/<style[\s\S]*?<\/style>/gi, " ")
.replace(/<svg[\s\S]*?<\/svg>/gi, " ")
.replace(/<[^>]+>/g, " ")
.replace(/\s+/g, " ")
.trim();
}
function firstMatch(html, pattern) {
const result = html.match(pattern);
return result ? plainText(result[1]) : "";
}
function extractList(html, tag, limit = 6) {
const matches = [...html.matchAll(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, "gi"))];
return [...new Set(matches.map((item) => plainText(item[1])).filter((item) => item.length > 8 && item.length < 180))].slice(0, limit);
}
function sentences(text, limit = 3) {
return text
.split(/(?<=[。!?.!?])\s*/)
.map((item) => item.trim())
.filter((item) => item.length > 25 && item.length < 280)
.slice(0, limit);
}
function genericResult(raw) {
const topic = raw.trim() || "新主题";
return {
title: topic,
category: "快速入门",
summary: `${topic} 可以从“它是什么、解决什么问题、如何工作、何时使用”四个问题快速建立整体认识。`,
definition: `${topic} 是本次学习对象。先建立准确的边界和基本词汇,再通过一个最小实例把概念连接起来。`,
why: `理解 ${topic} 的核心价值、适用场景与限制,可以更快判断它是否值得使用,并避免只记结论、不懂条件。`,
concepts: [["定义", `用自己的话说明 ${topic} 是什么。`], ["机制", "找到输入、处理过程和输出之间的关系。"], ["场景", "识别它最适合解决的三类问题。"], ["边界", "明确它不适用的条件和常见代价。"]],
takeaways: ["先建立概念地图", "用最小案例验证理解", "通过对比辨认使用边界"],
misconception: `不要把“知道 ${topic} 的定义”等同于会用。真正掌握需要能够解释、判断场景并完成一次最小实践。`,
steps: [`用一句话解释 ${topic}`, "找出四个核心概念", "完成一个 10 分钟最小案例", "向别人复述并修正盲点"],
cards: [[`${topic} 解决的核心问题是什么?`, "先从目标用户、输入条件和期望结果三个角度回答。"], [`学习 ${topic} 时先做什么?`, "建立术语边界,再用最小实例验证自己的理解。"], ["怎样判断是否真正掌握?", "能脱离资料解释概念、判断场景,并独立完成一次小实践。"]],
quiz: { question: `开始学习 ${topic} 时,最有效的第一步是什么?`, options: ["背下所有术语", "建立定义、价值、机制和边界的框架", "直接阅读最长的资料", "跳过案例只看结论"], answer: 1, explanation: "先有稳定的认知框架,后续细节才容易归位,也更容易发现理解缺口。" }
};
}
const topicProfiles = new Map([
["apple 公司", {
title: "Apple 公司", category: "商业与科技",
summary: "Apple 是一家把硬件、软件和服务整合为统一体验的科技公司,核心产品包括 iPhone、Mac、iPad 和围绕它们构建的服务生态。",
definition: "理解 Apple 的关键不是单看某一款设备,而是看它如何通过芯片、操作系统、设备、零售与服务形成端到端体验。",
why: "它是研究产品整合、品牌溢价、平台生态和供应链管理的典型案例。",
concepts: [["软硬整合", "同时控制核心硬件与操作系统体验。"], ["生态系统", "设备、账号、服务和开发者彼此增强。"], ["品牌与零售", "用一致的产品语言和渠道建立信任。"]],
takeaways: ["体验来自系统整合", "生态提高长期留存", "聚焦比产品数量更重要"],
misconception: "Apple 不只是硬件制造商,服务、平台规则和生态协同同样构成它的商业模式。",
steps: ["看懂 Apple 的产品矩阵", "分析一条软硬件体验链", "比较生态内外的使用差异"]
}],
["苹果(水果)", {
title: "苹果(水果)", category: "植物与饮食",
summary: "苹果是蔷薇科苹果属植物的果实,品种众多,风味与质地受品种、成熟度和储存条件影响。",
definition: "日常食用部分主要是花托发育形成的假果;真正的果实结构位于中央果核附近。",
why: "了解品种、成熟和储存方式,能更准确地选择鲜食或烹饪用途。",
concepts: [["品种", "甜度、酸度、硬度和香气差异明显。"], ["成熟", "影响淀粉转糖、香气和质地。"], ["储存", "低温与湿度会明显影响保鲜时间。"]],
takeaways: ["品种决定主要风味", "成熟度影响口感", "切开后应减少氧化"],
misconception: "苹果籽含有氰苷,但正常误食少量完整果籽通常不会造成中毒;不应大量咀嚼食用。",
steps: ["对比两种苹果的风味", "按鲜食或烹饪选择品种", "掌握冷藏与防氧化方法"]
}],
["苹果品牌与产品设计", {
title: "Apple 产品设计", category: "产品设计",
summary: "Apple 的产品设计强调减少认知负担,让硬件、软件、包装与服务使用同一套清晰的体验语言。",
definition: "它不是单纯的极简外观,而是从用户任务出发,对功能层级、材料、交互和供应链进行系统取舍。",
why: "它适合用来学习设计一致性、功能聚焦,以及如何把品牌价值落到每个接触点。",
concepts: [["聚焦", "保留最重要的任务与路径。"], ["一致性", "视觉、交互和实体细节相互呼应。"], ["端到端体验", "从购买、开箱到长期使用统一设计。"]],
takeaways: ["极简来自取舍", "一致性建立信任", "细节必须服务整体体验"],
misconception: "模仿留白、圆角或材质并不等于掌握 Apple 的设计方法,真正困难的是功能优先级和系统一致性。",
steps: ["选一个核心用户任务", "删除不必要的步骤", "统一界面与实体接触点"]
}],
["java 编程语言", {
title: "Java 编程语言", category: "软件开发",
summary: "Java 是一种静态类型、面向对象的编程语言,源码通常编译为字节码并运行在 Java 虚拟机上。",
definition: "JVM 把程序与具体操作系统隔开,使同一份字节码可以在不同环境运行。",
why: "Java 拥有成熟的工具、框架和运行时,常用于企业服务、后端系统与 Android 相关开发。",
concepts: [["JVM", "执行字节码并管理内存。"], ["类型系统", "在编译阶段发现一类错误。"], ["类与接口", "组织数据、行为和抽象边界。"]],
takeaways: ["源码先编译成字节码", "类型帮助约束程序", "生态适合大型工程"],
misconception: "Java 和 JavaScript 是两种不同语言,命名相似并不代表核心设计相同。",
steps: ["安装 JDK 并运行 Hello World", "理解变量、方法和类", "完成一个命令行小程序"]
}],
["ai 智能体", {
title: "AI 智能体", category: "人工智能",
summary: "AI 智能体是能围绕目标理解状态、选择行动、调用工具并根据结果继续调整的 AI 系统。",
definition: "它通常由模型、指令、工具、记忆和执行循环组成;关键区别是它不只回答,还会采取有边界的行动。",
why: "它适合处理需要多步骤判断、外部工具和动态反馈的任务。",
concepts: [["目标", "定义完成条件与行动边界。"], ["工具", "让系统读取信息或执行动作。"], ["循环", "观察结果并决定下一步。"]],
takeaways: ["目标必须可验证", "工具必须有权限边界", "循环需要停止条件"],
misconception: "更强的自主性不一定更好;高风险动作仍应保留确认、审计和失败恢复。",
steps: ["选一个边界清晰的任务", "接入一个只读工具", "加入结果检查和停止条件"]
}]
]);
async function termResult(input) {
const normalized = input.toLowerCase().trim();
const found = knowledge.find((item) => item.match.some((key) => normalized === key || normalized.includes(key)));
if (found) {
const { match, ...result } = found;
return result;
}
const profile = topicProfiles.get(normalized);
if (profile) return { ...genericResult(input), ...profile };
const wikipedia = await lookupWikipedia(input);
if (!wikipedia || wikipedia.needsClarification) return wikipedia || genericResult(input);
return { ...genericResult(input), ...wikipedia };
}
async function urlResult(input) {
const parsed = new URL(input);
try {
const { html, finalUrl } = await fetchPage(input);
const title = firstMatch(html, /<title[^>]*>([\s\S]*?)<\/title>/i) || parsed.hostname;
const description = firstMatch(html, /<meta[^>]+(?:name|property)=["'](?:description|og:description)["'][^>]+content=["']([^"']+)["'][^>]*>/i)
|| firstMatch(html, /<meta[^>]+content=["']([^"']+)["'][^>]+(?:name|property)=["'](?:description|og:description)["'][^>]*>/i);
const headings = [...extractList(html, "h1", 3), ...extractList(html, "h2", 6)].filter((item) => item !== title).slice(0, 5);
const paragraphs = extractList(html, "p", 12);
const usableSentences = sentences(paragraphs.join(" "), 4);
const summary = description || usableSentences.slice(0, 2).join(" ") || `这是来自 ${parsed.hostname} 的网页,建议先从标题、章节结构和核心结论开始阅读。`;
const topic = title.replace(/\s*[|–—-]\s*[^|–—-]+$/, "").trim();
const base = genericResult(topic);
base.category = "网页速读";
base.summary = summary.slice(0, 360);
base.definition = usableSentences[0] || `这篇网页围绕“${topic}”展开。先确认作者的核心主张,再区分事实、案例与观点。`;
base.why = usableSentences[1] || `快速理解这篇内容的价值,在于提取可迁移的结论,并判断它与你当前问题的相关性。`;
if (headings.length) {
base.concepts = headings.slice(0, 4).map((heading, index) => [heading, paragraphs[index]?.slice(0, 150) || "这是原文的关键章节,值得结合上下文重点阅读。"]).slice(0, 4);
}
base.takeaways = usableSentences.slice(0, 3).map((item) => item.slice(0, 92));
if (base.takeaways.length < 3) base.takeaways = ["先看标题与章节结构", "找到作者的核心主张", "区分结论、证据与案例"];
base.source = { url: finalUrl, host: new URL(finalUrl).hostname, title };
return base;
} catch (error) {
const base = genericResult(parsed.hostname);
base.title = parsed.hostname;
base.category = "网页概览";
base.summary = `暂时无法读取这个网页的正文,但已经为 ${parsed.hostname} 建立了一套快速审阅框架。`;
base.definition = "先确认页面的作者、发布日期和核心问题,再通过标题层级定位最值得阅读的部分。";
base.why = "网页可能限制自动读取,或需要登录。你仍可以沿着结构、主张、证据和行动四层框架快速分析。";
base.source = { url: input, host: parsed.hostname, title: parsed.hostname, warning: error.message };
return base;
}
}
const ambiguousTerms = new Map([
["苹果", ["Apple 公司", "苹果(水果)", "苹果品牌与产品设计"]],
["java", ["Java 编程语言", "Java 岛", "Java 咖啡"]],
["模型", ["AI 模型", "商业模型", "3D 模型"]],
["agent", ["AI 智能体", "软件代理模式", "经纪人与代理人"]],
["智能体", ["AI 智能体", "多智能体系统", "自主机器人"]],
["设计", ["产品设计", "UI 设计", "平面设计"]],
["平台", ["软件平台", "商业平台模式", "内容平台"]]
]);
function clarificationFor(input) {
const normalized = input.toLowerCase().trim();
const options = ambiguousTerms.get(normalized);
if (options) return { question: `“${input}”可能有几种意思,你想了解哪一个?`, options };
if (input.trim().length <= 1 || ["工具", "技术", "系统", "软件", "产品"].includes(normalized)) {
return { question: `可以再具体一点吗?比如它属于哪个领域,或者你在哪里看到“${input}”?`, options: [] };
}
return null;
}
async function handleAnalyze(req, res) {
let body = "";
for await (const chunk of req) {
body += chunk;
if (body.length > 20_000) return sendJson(res, 413, { error: "输入内容过长" });
}
try {
const { input } = JSON.parse(body || "{}");
if (typeof input !== "string" || !input.trim()) return sendJson(res, 400, { error: "请输入一个网址或名词" });
const value = input.trim();
const isUrl = /^https?:\/\//i.test(value);
const clarification = isUrl ? null : clarificationFor(value);
if (clarification) {
return sendJson(res, 200, { needsClarification: true, input: value, ...clarification });
}
const result = isUrl ? await urlResult(value) : await termResult(value);
if (result.needsClarification) {
return sendJson(res, 200, { input: value, ...result });
}
sendJson(res, 200, { ...result, input: value, kind: isUrl ? "url" : "term", createdAt: new Date().toISOString() });
} catch (error) {
sendJson(res, 400, { error: error.message || "分析失败,请稍后重试" });
}
}
const exampleLibrary = new Map([
["mcp", ["做一个“查询项目 Issue”的 MCP Server:智能体收到问题后调用 search_issues 工具,再把返回结果整理成人话。", "输入:用户询问某个项目问题", "工具:只读查询 Issue", "输出:带来源的简短答案"]],
["react", ["做一个计数器组件:按钮点击时更新 count 状态,React 根据新状态重新渲染数字。", "输入:用户点击按钮", "状态:count 增加 1", "界面:显示最新数字"]],
["docker", ["把一个 Node 服务打包进镜像:Dockerfile 安装依赖、复制代码并声明启动命令,然后用端口映射访问它。", "构建:docker build", "运行:docker run", "验证:浏览器访问映射端口"]],
["提示词工程", ["让模型总结会议:提供会议记录,要求输出“结论、负责人、截止时间”三栏,并规定不确定项标记为待确认。", "输入:原始会议记录", "约束:不得补写缺失事实", "输出:固定三栏结构"]],
["apple 公司", ["观察 iPhone 拍照后如何在 Mac 上继续编辑:硬件、账号、系统和应用共同完成一个连续任务,这就是生态整合。", "入口:iPhone 拍摄", "连接:iCloud 同步", "结果:Mac 继续处理"]],
["java 编程语言", ["写一个命令行待办程序:用类表示任务,用集合保存任务,再用方法完成新增和查看。", "数据:Task 类", "逻辑:新增与完成方法", "输出:终端任务列表"]],
["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 {
title: "最快上手方式",
text: `先不要追求完整掌握 ${topic},用一个最小任务跑通闭环。`,
bullets: (steps.length ? steps : ["确认目标", "完成最小案例", "复盘结果"]).slice(0, 3)
};
}
if (/例子|示例|案例|演示/.test(normalized)) {
const tailored = exampleLibrary.get(topic.toLowerCase());
if (tailored) {
return { title: "一个最小示例", text: tailored[0], bullets: tailored.slice(1) };
}
const first = concepts[0]?.[0] || "核心概念";
return {
title: "一个最小示例",
text: `假设你第一次使用 ${topic}:先选择一个只涉及“${first}”的小任务,给出明确输入,完成一次输出,再对照预期检查哪里不一致。`,
bullets: ["输入:一个明确、范围很小的问题", "过程:只使用一个核心概念", "结果:得到可观察、可比较的产出"]
};
}
if (/误区|注意|坑|风险/.test(normalized)) {
return {
title: "最容易踩的坑",
text: context?.misconception || `学习 ${topic} 时,最常见的问题是记住定义,却没有通过实际任务验证。`,
bullets: takeaways.slice(0, 2)
};
}
if (/简单|没懂|解释|白话|什么意思/.test(normalized)) {
return {
title: "换成大白话",
text: context?.summary || `${topic} 是一种帮助你更好完成特定任务的方法。`,
bullets: concepts.slice(0, 2).map(([name, detail]) => `${name}:${detail}`)
};
}
return {
title: "继续理解",
text: `关于“${question}”,可以先回到 ${topic} 的核心目标:${context?.why || "它解决什么问题,以及在什么条件下有效。"}`,
bullets: (takeaways.length ? takeaways : concepts.map(([name]) => name)).slice(0, 3)
};
}
async function handleAsk(req, res) {
let body = "";
for await (const chunk of req) {
body += chunk;
if (body.length > 30_000) return sendJson(res, 413, { error: "问题内容过长" });
}
try {
const { question, context } = JSON.parse(body || "{}");
if (typeof question !== "string" || !question.trim()) return sendJson(res, 400, { error: "请输入你的问题" });
sendJson(res, 200, followUpResult(question.trim(), context));
} catch (error) {
sendJson(res, 400, { error: error.message || "暂时无法回答" });
}
}
function withinRateLimit(requests, limit) {
const now = Date.now();
const recent = requests.filter((time) => now - time < leadRateWindowMs);
recent.push(now);
return { recent, limited: recent.length > limit };
}
async function handlePaidPackInterest(req, res) {
if (!commercialStorageReady) return sendJson(res, 503, { error: "商业数据存储尚未配置" });
let body = "";
for await (const chunk of req) {
body += chunk;
if (body.length > 5_000) return sendJson(res, 413, { error: "提交内容过长" });
}
const leadRate = withinRateLimit(leadRequests, leadRateLimit);
leadRequests = leadRate.recent;
if (leadRate.limited) return sendJson(res, 429, { error: "提交过于频繁,请稍后再试" });
try {
const { topic, goal, contact, consent, website } = JSON.parse(body || "{}");
if (typeof website === "string" && website.trim()) {
return sendJson(res, 201, { received: true, charged: false, reference: "QL-RECEIVED" });
}
if (consent !== true) return sendJson(res, 400, { error: "请先同意我们使用联系方式确认定制需求" });
const cleanTopic = cleanLeadText(topic);
const cleanGoal = cleanLeadText(goal);
const cleanContact = cleanLeadText(contact);
if (cleanTopic.length < 1 || cleanTopic.length > 160) {
return sendJson(res, 400, { error: "学习主题无效" });
}
if (cleanGoal.length < 5 || cleanGoal.length > 600) {
return sendJson(res, 400, { error: "请用 5 到 600 个字符说明学习目标" });
}
if (cleanContact.length < 3 || cleanContact.length > 120) {
return sendJson(res, 400, { error: "请填写可用的微信号、邮箱或手机号" });
}
const reference = `QL-${randomUUID().split("-")[0].toUpperCase()}`;
const record = {
reference,
offer: "custom-learning-pack-v1",
amountCny: 29,
topic: cleanTopic,
goal: cleanGoal,
contact: cleanContact,
createdAt: new Date().toISOString()
};
await mkdir(dirname(leadFile), { recursive: true, mode: 0o700 });
await appendFile(leadFile, `${JSON.stringify(record)}\n`, { encoding: "utf8", mode: 0o600 });
await chmod(leadFile, 0o600);
sendJson(res, 201, { received: true, charged: false, reference });
} catch (error) {
if (error instanceof SyntaxError) return sendJson(res, 400, { error: "提交格式无效" });
console.error("Unable to store paid-pack interest", error);
sendJson(res, 503, { error: "暂时无法登记,请稍后再试" });
}
}
async function handleCommercialEvent(req, res) {
if (!commercialStorageReady) return sendJson(res, 503, { error: "商业数据存储尚未配置" });
let body = "";
for await (const chunk of req) {
body += chunk;
if (body.length > 1_000) return sendJson(res, 413, { error: "事件内容过长" });
}
const eventRate = withinRateLimit(commercialEventRequests, 2_000);
commercialEventRequests = eventRate.recent;
if (eventRate.limited) return sendJson(res, 204, {});
try {
const { event } = JSON.parse(body || "{}");
if (!commercialEvents.has(event)) return sendJson(res, 400, { error: "未知事件" });
await mkdir(dirname(commercialEventFile), { recursive: true, mode: 0o700 });
await appendFile(commercialEventFile, `${JSON.stringify({ event, createdAt: new Date().toISOString() })}\n`, { encoding: "utf8", mode: 0o600 });
await chmod(commercialEventFile, 0o600);
sendJson(res, 204, {});
} catch (error) {
if (!(error instanceof SyntaxError)) console.error("Unable to store commercial event", error);
sendJson(res, error instanceof SyntaxError ? 400 : 503, { error: "事件记录失败" });
}
}
const server = http.createServer(async (req, res) => {
if (req.method === "GET" && req.url === "/health") {
return commercialStorageReady
? sendJson(res, 200, { status: "ok" })
: sendJson(res, 503, { status: "not_ready" });
}
if (req.method === "POST" && req.url === "/api/analyze") return handleAnalyze(req, res);
if (req.method === "POST" && req.url === "/api/ask") return handleAsk(req, res);
if (req.method === "POST" && req.url === "/api/paid-pack-interest") return handlePaidPackInterest(req, res);
if (req.method === "POST" && req.url === "/api/commercial-event") return handleCommercialEvent(req, res);
if (req.method !== "GET") return sendJson(res, 405, { error: "Method not allowed" });
const requestPath = req.url === "/" ? "/index.html" : req.url.split("?")[0];
if (!publicFiles.has(requestPath)) {
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
return res.end("Not found");
}
const safePath = normalize(requestPath).replace(/^(\.\.(\/|\\|$))+/, "");
const filePath = join(root, safePath);
if (!filePath.startsWith(root)) return sendJson(res, 403, { error: "Forbidden" });
try {
const content = await readFile(filePath);
res.writeHead(200, { "Content-Type": mimeTypes[extname(filePath)] || "application/octet-stream", "Cache-Control": "no-cache" });
res.end(content);
} catch {
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Not found");
}
});
server.listen(port, host, () => {
console.log(`QuickLearn is running at http://${host}:${port}`);
});
function shutdown(signal) {
console.log(`${signal} received, closing HTTP server`);
server.close((error) => {
if (error) {
console.error("Unable to close HTTP server", error);
process.exit(1);
}
});
server.closeIdleConnections?.();
setTimeout(() => server.closeAllConnections?.(), 7_500).unref();
}
process.once("SIGTERM", () => shutdown("SIGTERM"));
process.once("SIGINT", () => shutdown("SIGINT"));