前提环境

  • Vercel账号

  • GitHub仓库

  • 域名

  • 大家也可以根据代码修改自己想要的效果

✒️申请星火Spark-Lite

打开讯飞星火大模型API-大模型API-大模型接口-科大讯飞

下滑到如下,选择Spark-Lite,点击立即调用

会提示创建新应用,信息随便填,之后侧边栏点击Spark Lite,选择领取无限量~

因为我很早之前已经领取过了,具体选择怎么领取,应该也不难~

接着右侧会出现你的APPIDAPISecretAPIKey,我们就可以进行下一步了

✒️创建Spark仓库用于代理服务器

拉取仓库到本地,创建一个api文件夹和一个spark-proxy.js文件

spark-proxy.js文件内容为

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
const crypto = require('crypto'); // 可能不再需要,取决于新鉴权方式
// --- 移除这行: const fetch = require('node-fetch'); ---

// --- 从环境变量读取敏感信息 ---
const APPID = process.env.SPARK_APPID; // 可能仍需要
const API_SECRET = process.env.SPARK_API_SECRET; // 用于新鉴权
const API_KEY = process.env.SPARK_API_KEY; // 用于新鉴权

// --- Spark API 地址 (兼容 OpenAI 格式) ---
const SPARK_API_URL = "https://spark-api-open.xf-yun.com/v1/chat/completions";
// --- 确认 Lite 版或其他模型在新 API 中的标识符 ---
const MODEL_NAME = "lite"; // 请根据官方文档确认正确的模型名称

// --- 移除旧的 getAuthorizationUrl 函数 ---
// function getAuthorizationUrl() { ... } // 删除此函数

// --- Vercel Serverless Function 主体 ---
module.exports = async (req, res) => {
// --- 使用动态 import() 导入 node-fetch ---
const fetch = (await import('node-fetch')).default;

// --- 设置 CORS 响应头 ---
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); // 允许 Authorization 头

// --- 处理 OPTIONS 预检请求 ---
if (req.method === 'OPTIONS') {
return res.status(200).end();
}

// --- 处理 POST 请求 ---
if (req.method === 'POST') {
// 检查必要的环境变量是否设置 (根据新鉴权方式调整)
if (!API_KEY || !API_SECRET || !APPID) { // APPID 可能仍需要,取决于具体实现
console.error("Server Error: Spark environment variables not configured.");
return res.status(500).json({ error: "服务器内部错误:API凭证未配置" });
}

try {
const { content, title } = req.body; // 从请求体获取内容和标题

if (!content) {
return res.status(400).json({ error: "请求体缺少 'content' 字段" });
}

// --- 构造符合 OpenAI 格式的请求体 ---
const requestData = {
model: MODEL_NAME, // 使用正确的模型名称
messages: [
{ role: "system", content: "你是一个有用的助手,请根据用户提供的文章标题和内容生成一段简洁的摘要。" },
{ role: "user", content: `文章标题:${title || '无标题'}\n文章内容:${content}` }
],
temperature: 0.5,
max_tokens: 200
// 根据需要添加其他参数,如 stream: false
};

// --- 构造请求头 (!!! 重要:根据讯飞官方文档修改鉴权方式 !!!) ---
const headers = {
'Content-Type': 'application/json',
// --- 这里需要添加正确的 Authorization Header ---
// 示例 1 (假设是 Bearer Token, token 如何生成需查文档):
// 'Authorization': `Bearer ${generateSparkToken(API_KEY, API_SECRET)}`,
// 示例 2 (假设直接用 Key/Secret, 具体 Header 名称需查文档):
// 'Authorization': `Key ${API_KEY}`,
// 'X-Spark-Secret': API_SECRET, // Header 名称仅为示例
// --- 请务必参考讯飞官方文档获取准确的鉴权 Header ---
'Authorization': `Bearer ${API_KEY}:${API_SECRET}` // 这是一个常见的但不一定正确的示例,请验证!
};


// --- 发送请求到 Spark API ---
const sparkResponse = await fetch(SPARK_API_URL, { // 直接使用 API URL,不再需要 authUrl
method: 'POST',
headers: headers, // 使用新的 Headers
body: JSON.stringify(requestData) // 使用新的 Request Body
});

// 检查 fetch 是否成功
if (!sparkResponse) {
console.error("Proxy Error: Failed to fetch Spark API.");
return res.status(500).json({ error: '代理服务器未能连接到 Spark API' });
}

const sparkData = await sparkResponse.json();

// --- 处理 Spark API 的响应 (OpenAI 兼容格式) ---
if (sparkResponse.ok && sparkData.choices && sparkData.choices.length > 0 && sparkData.choices[0].message) {
const assistantMessage = sparkData.choices[0].message;
if (assistantMessage.role === 'assistant' && assistantMessage.content) {
const summary = assistantMessage.content.trim();
return res.status(200).json({ summary: summary });
} else {
console.error("Spark response parsing error (unexpected message role or content):", sparkData);
return res.status(500).json({ error: "未能从 Spark 获取有效摘要内容" });
}
} else if (sparkData.error) { // OpenAI 兼容 API 通常用 error 字段报告错误
console.error("Spark API Error:", sparkData.error.message);
return res.status(sparkResponse.status || 500).json({ error: `Spark API 错误: ${sparkData.error.message} (Code: ${sparkData.error.code || 'N/A'})` });
} else if (!sparkResponse.ok) { // 处理其他 HTTP 错误
console.error("Spark request failed:", sparkResponse.status, sparkData);
// 尝试从响应体获取更详细的错误信息
let errorMessage = `获取摘要失败,状态码: ${sparkResponse.status}`;
if (sparkData && typeof sparkData === 'object') {
errorMessage += ` - ${JSON.stringify(sparkData)}`;
} else if (typeof sparkData === 'string') {
errorMessage += ` - ${sparkData}`;
}
return res.status(sparkResponse.status).json({ error: errorMessage });
}
else {
console.error("Spark request failed or unexpected format:", sparkResponse.status, sparkData);
return res.status(sparkResponse.status || 500).json({ error: `获取摘要失败,状态码: ${sparkResponse.status}, 响应格式未知` });
}

} catch (error) {
console.error("Proxy Error:", error);
// 检查是否是 JSON 解析错误
if (error instanceof SyntaxError) {
console.error("Failed to parse Spark API response as JSON.");
// 可以尝试获取原始文本响应
// const rawResponse = await sparkResponse.text(); // 需要在 try 块内重新获取或传递
// console.error("Raw response:", rawResponse);
return res.status(500).json({ error: '代理服务器错误:无法解析 Spark API 响应' });
}
return res.status(500).json({ error: '代理服务器内部错误', details: error.message });
}
} else {
// 如果不是 POST 或 OPTIONS 请求
res.setHeader('Allow', ['POST', 'OPTIONS']);
return res.status(405).end(`Method ${req.method} Not Allowed`);
}
};

// --- 如果需要生成 Token,可能需要类似这样的辅助函数 (具体实现需查文档) ---
// function generateSparkToken(apiKey, apiSecret) {
// // ... 根据讯飞文档实现 Token 生成逻辑 ...
// return "generated_token_string";
// }

安装依赖: 如果你的项目还没有 package.json ,在根目录运行 npm init -y ,然后安装 node-fetc

1
npm install node-fetch

接着你就可以push到你的仓库里了

✒️打开Vercel

新建projects,关联自己刚刚的Spark仓库

  • 项目导入成功后,进入该项目在 Vercel 上的仪表盘 (Dashboard)。
  • 在项目仪表盘中找到 “Settings”(设置)选项卡。
  • 在 “Settings” 下找到 “Environment Variables”(环境变量)部分。
  • 在这里添加你的 SPARK_APPID , SPARK_API_KEY , 和 SPARK_API_SECRET 。
  • 重新部署

注意,一定要填对,否则调用就会报错401

成功后,Vercel 会提供一个类似 https://your-project-name.vercel.app 的域名。你的代理函数可以通过 https://your-project-name.vercel.app/api/spark-proxy 访问。

当然你需要在你的域名控制台解析一下,让其可以国内访问~

✒️打开你的博客源代码

1.✒️在source/js创建一个gpt.js

将里面的proxyApiUrl替换为你部署的 Vercel URL

比如我的是https://abc.vercel.app/

那么proxyApiUrlhttps://abc.vercel.app/api/spark-proxy.js

⚠️:首先查看主题是否内置了pjax

如果有,则gpt.js内容为:

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
console.log("\n %c Post-Abstract-AI (Spark Lite) 开源博客文章摘要AI生成工具 %c https://github.com/zhheo/Post-Abstract-AI \n", "color: #fadfa3; background: #030307; padding:5px 0;", "background: #fadfa3; padding:5px 0;")
var sparkLiteIsRunning = false;

function insertAIDiv(selector) {
removeExistingAIDiv();
const targetElement = document.querySelector(selector);
if (!targetElement) return;

const aiDiv = document.createElement('div');
aiDiv.className = 'post-SparkLite';

const aiTitleDiv = document.createElement('div');
aiTitleDiv.className = 'sparkLite-title';
aiDiv.appendChild(aiTitleDiv);

const aiIcon = document.createElement('i');
aiIcon.className = 'sparkLite-title-icon';
aiTitleDiv.appendChild(aiIcon);

aiIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48px" height="48px" viewBox="0 0 48 48">
<title>机器人</title>
<g id="机器人" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M34.717885,5.03561087 C36.12744,5.27055371 37.079755,6.60373651 36.84481,8.0132786 L35.7944,14.3153359 L38.375,14.3153359 C43.138415,14.3153359 47,18.1768855 47,22.9402569 L47,34.4401516 C47,39.203523 43.138415,43.0650727 38.375,43.0650727 L9.625,43.0650727 C4.861585,43.0650727 1,39.203523 1,34.4401516 L1,22.9402569 C1,18.1768855 4.861585,14.3153359 9.625,14.3153359 L12.2056,14.3153359 L11.15519,8.0132786 C10.920245,6.60373651 11.87256,5.27055371 13.282115,5.03561087 C14.69167,4.80066802 16.024865,5.7529743 16.25981,7.16251639 L17.40981,14.0624532 C17.423955,14.1470924 17.43373,14.2315017 17.43948,14.3153359 L30.56052,14.3153359 C30.56627,14.2313867 30.576045,14.1470924 30.59019,14.0624532 L31.74019,7.16251639 C31.975135,5.7529743 33.30833,4.80066802 34.717885,5.03561087 Z M38.375,19.4902885 L9.625,19.4902885 C7.719565,19.4902885 6.175,21.0348394 6.175,22.9402569 L6.175,34.4401516 C6.175,36.3455692 7.719565,37.89012 9.625,37.89012 L38.375,37.89012 C40.280435,37.89012 41.825,36.3455692 41.825,34.4401516 L41.825,22.9402569 C41.825,21.0348394 40.280435,19.4902885 38.375,19.4902885 Z M14.8575,23.802749 C16.28649,23.802749 17.445,24.9612484 17.445,26.3902253 L17.445,28.6902043 C17.445,30.1191812 16.28649,31.2776806 14.8575,31.2776806 C13.42851,31.2776806 12.27,30.1191812 12.27,28.6902043 L12.27,26.3902253 C12.27,24.9612484 13.42851,23.802749 14.8575,23.802749 Z M33.1425,23.802749 C34.57149,23.802749 35.73,24.9612484 35.73,26.3902253 L35.73,28.6902043 C35.73,30.1191812 34.57149,31.2776806 33.1425,31.2776806 C31.71351,31.2776806 30.555,30.1191812 30.555,28.6902043 L30.555,26.3902253 C30.555,24.9612484 31.71351,23.802749 33.1425,23.802749 Z" id="形状结合" fill="#444444" fill-rule="nonzero"></path>
</g>
</svg>`;

const aiTitleTextDiv = document.createElement('div');
aiTitleTextDiv.className = 'sparkLite-title-text';
aiTitleTextDiv.textContent = 'AI摘要';
aiTitleDiv.appendChild(aiTitleTextDiv);

const aiTagDiv = document.createElement('div');
aiTagDiv.className = 'sparkLite-tag';
aiTagDiv.id = 'sparkLite-tag';
aiTagDiv.textContent = 'Spark Lite';

// 添加刷新按钮
const refreshBtn = document.createElement('span');
refreshBtn.className = 'sparkLite-refresh-btn';
refreshBtn.innerHTML = '⟳';
refreshBtn.title = '重新生成摘要';
refreshBtn.addEventListener('click', function () {
runSparkLite();
});

aiTagDiv.appendChild(refreshBtn);
aiTitleDiv.appendChild(aiTagDiv);

const aiExplanationDiv = document.createElement('div');
aiExplanationDiv.className = 'sparkLite-explanation';
aiExplanationDiv.innerHTML = '生成中...' + '<span class="blinking-cursor"></span>';
aiDiv.appendChild(aiExplanationDiv);

targetElement.insertBefore(aiDiv, targetElement.firstChild);
}

function removeExistingAIDiv() {
const existingAIDiv = document.querySelector(".post-SparkLite");
if (existingAIDiv) {
existingAIDiv.parentElement.removeChild(existingAIDiv);
}
}

function getTitleAndContent() {
try {
const title = document.getElementsByClassName('post-title')[0].innerText;
const container = document.querySelector(sparkLite_postSelector);
if (!container) {
console.warn('Spark Lite:找不到文章容器...');
return '';
}
const paragraphs = container.getElementsByTagName('p');
const headings = container.querySelectorAll('h1, h2, h3, h4, h5');
let content = '';

for (let h of headings) {
content += h.innerText + ' ';
}

for (let p of paragraphs) {
const filteredText = p.innerText.replace(/https?:\/\/[^\s]+/g, '');
content += filteredText;
}

const combinedText = title + ' ' + content;
let wordLimit = 1000;
if (typeof sparkLite_wordLimit !== "undefined") {
wordLimit = sparkLite_wordLimit;
}
return combinedText.slice(0, wordLimit);
} catch (e) {
console.error('Spark Lite 错误:...', e);
return '';
}
}

// 添加IndexedDB初始化函数
const initDB = () => {
return new Promise((resolve, reject) => {
const request = indexedDB.open('SparkLiteDB', 1);

request.onupgradeneeded = (e) => {
const db = e.target.result;
if (!db.objectStoreNames.contains('summaries')) {
db.createObjectStore('summaries', { keyPath: 'url' });
}
};

request.onsuccess = (e) => resolve(e.target.result);
request.onerror = (e) => reject(e.target.error);
});
};

async function fetchSparkLiteSummary(content) {
const title = document.title;
const url = window.location.href;

// 先尝试从IndexedDB读取
try {
const db = await initDB();
const tx = db.transaction('summaries', 'readonly');
const store = tx.objectStore('summaries');
const request = store.get(url);

const cachedData = await new Promise((resolve) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => resolve(null);
});

if (cachedData?.summary) {
// 检查缓存是否过期(7天有效期)
const isExpired = Date.now() - cachedData.timestamp > 7 * 24 * 60 * 60 * 1000;
if (!isExpired) {
return cachedData.summary;
}
}
} catch (e) {
console.log('读取IndexedDB缓存失败', e);
}

const proxyApiUrl = "";
const requestDataToProxy = { content: content, title: title };
const timeout = 30000;

try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);

const response = await fetch(proxyApiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestDataToProxy),
signal: controller.signal
});

clearTimeout(timeoutId);
const data = await response.json();

// 成功获取摘要后存入IndexedDB
if (response.ok) {
try {
const db = await initDB();
const tx = db.transaction('summaries', 'readwrite');
tx.objectStore('summaries').put({
url: url,
summary: data.summary,
timestamp: Date.now()
});
} catch (e) {
console.log('IndexedDB写入失败', e);
}
return data.summary;
} else {
console.error(`代理或 API 错误: ${data.error || response.statusText}`);
return `获取摘要失败: ${data.error || `HTTP 状态码: ${response.status}`}`;
}
} catch (error) {
if (error.name === 'AbortError') {
console.error('Spark Lite 请求超时 (通过代理)');
return '获取文章摘要超时,请稍后刷新重试。';
} else {
console.error('Spark Lite 请求失败 (通过代理):', error);
if (error instanceof SyntaxError) {
return '获取文章摘要失败:代理服务器响应格式错误。';
}
return '获取文章摘要失败,请检查网络连接或代理服务器状态。';
}
}
}

function aiShowAnimation(text) {
const element = document.querySelector(".sparkLite-explanation");
if (!element) {
console.warn('Spark Lite:找不到元素...');
return;
}

if (typeof sparkLite_typingAnimate !== "undefined" && !sparkLite_typingAnimate) {
element.innerHTML = text;
return;
}

const typingDelay = 25;
const punctuationDelayMultiplier = 6;

element.style.display = "block";
element.innerHTML = "生成中..." + '<span class="blinking-cursor"></span>';

let animationRunning = true;
let currentIndex = 0;
let initialAnimation = true;
let lastUpdateTime = performance.now();

const animate = () => {
if (currentIndex < text.length && animationRunning) {
const currentTime = performance.now();
const timeDiff = currentTime - lastUpdateTime;

const letter = text.slice(currentIndex, currentIndex + 1);
const isPunctuation = /[,。!、?,.!?]/.test(letter);
const delay = isPunctuation ? typingDelay * punctuationDelayMultiplier : typingDelay;

if (timeDiff >= delay) {
element.innerText = text.slice(0, currentIndex + 1);
lastUpdateTime = currentTime;
currentIndex++;

if (currentIndex < text.length) {
element.innerHTML = text.slice(0, currentIndex) + '<span class="blinking-cursor"></span>';
} else {
element.innerHTML = text;
element.style.display = "block";
observer.disconnect();
}
}
requestAnimationFrame(animate);
}
}

const observer = new IntersectionObserver((entries) => {
let isVisible = entries[0].isIntersecting;
animationRunning = isVisible;
if (animationRunning && initialAnimation) {
setTimeout(() => {
requestAnimationFrame(animate);
}, 200);
}
}, { threshold: 0 });
let post_ai = document.querySelector('.post-SparkLite');
observer.observe(post_ai);
}

function runSparkLite() {
insertAIDiv(sparkLite_postSelector);
const content = getTitleAndContent();
// console.log(content);

if (content) {
fetchSparkLiteSummary(content).then(summary => {
aiShowAnimation(summary);
});
} else {
const aiExplanationDiv = document.querySelector(".sparkLite-explanation");
if (aiExplanationDiv) {
aiExplanationDiv.textContent = '未能获取到文章内容,无法生成摘要。';
}
}
}

function checkURLAndRun() {
if (sparkLiteIsRunning) return false;

if (typeof sparkLite_postURL === "undefined") {
return true;
}

try {
const wildcardToRegExp = (s) => new RegExp('^' + s.split(/\*+/).map(regExpEscape).join('.*') + '$');
const regExpEscape = (s) => s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
const urlPattern = wildcardToRegExp(sparkLite_postURL);
const currentURL = window.location.href;

if (urlPattern.test(currentURL)) {
return true;
} else {
removeExistingAIDiv();
return false;
}
} catch (error) {
console.error("Spark Lite:我没有看懂你编写的自定义链接规则...", error);
return false;
}
}

function initializeSparkLite() {
const targetElement = document.querySelector(sparkLite_postSelector);
if (!targetElement) {
removeExistingAIDiv();
return;
}

if (checkURLAndRun()) {
runSparkLite();
} else {
runSparkLite();
}
}

document.removeEventListener("DOMContentLoaded", initializeSparkLite);
document.addEventListener("DOMContentLoaded", initializeSparkLite);

document.addEventListener("pjax:complete", function () {
if (document.querySelector(sparkLite_postSelector)) {
initializeSparkLite();
}
});

document.removeEventListener("pjax:send", removeExistingAIDiv);
document.addEventListener("pjax:send", removeExistingAIDiv);

如果没有,gtp.js内容为:

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
console.log("\n %c Post-Abstract-AI (Spark Lite) 开源博客文章摘要AI生成工具 %c https://github.com/zhheo/Post-Abstract-AI \n", "color: #fadfa3; background: #030307; padding:5px 0;", "background: #fadfa3; padding:5px 0;")

const initDB = () => {
return new Promise((resolve, reject) => {
const request = indexedDB.open('SparkLiteDB', 1);

request.onupgradeneeded = (e) => {
const db = e.target.result;
if (!db.objectStoreNames.contains('summaries')) {
const store = db.createObjectStore('summaries', { keyPath: 'url' });
store.createIndex('timestamp', 'timestamp', { unique: false });
}
};

request.onsuccess = (e) => resolve(e.target.result);
request.onerror = (e) => reject(e.target.error);
});
};

var sparkLiteIsRunning = false; // 重命名

// --- insertAIDiv 函数 ---
function insertAIDiv(selector) {
// 首先移除现有的 "post-SparkLite" 类元素(如果有的话)
removeExistingAIDiv(); // 需要同步修改 removeExistingAIDiv 函数选择器

// 获取目标元素
const targetElement = document.querySelector(selector);

// 如果没有找到目标元素,不执行任何操作
if (!targetElement) {
return;
}

// 创建要插入的HTML元素
const aiDiv = document.createElement('div');
aiDiv.className = 'post-SparkLite'; // 修改类名

const aiTitleDiv = document.createElement('div');
aiTitleDiv.className = 'sparkLite-title'; // 修改类名
aiDiv.appendChild(aiTitleDiv);

const aiIcon = document.createElement('i');
aiIcon.className = 'sparkLite-title-icon'; // 修改类名
aiTitleDiv.appendChild(aiIcon);

// 插入 SVG 图标 (保持不变或替换)
aiIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48px" height="48px" viewBox="0 0 48 48">
<title>机器人</title>
<g id="机器人" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M34.717885,5.03561087 C36.12744,5.27055371 37.079755,6.60373651 36.84481,8.0132786 L35.7944,14.3153359 L38.375,14.3153359 C43.138415,14.3153359 47,18.1768855 47,22.9402569 L47,34.4401516 C47,39.203523 43.138415,43.0650727 38.375,43.0650727 L9.625,43.0650727 C4.861585,43.0650727 1,39.203523 1,34.4401516 L1,22.9402569 C1,18.1768855 4.861585,14.3153359 9.625,14.3153359 L12.2056,14.3153359 L11.15519,8.0132786 C10.920245,6.60373651 11.87256,5.27055371 13.282115,5.03561087 C14.69167,4.80066802 16.024865,5.7529743 16.25981,7.16251639 L17.40981,14.0624532 C17.423955,14.1470924 17.43373,14.2315017 17.43948,14.3153359 L30.56052,14.3153359 C30.56627,14.2313867 30.576045,14.1470924 30.59019,14.0624532 L31.74019,7.16251639 C31.975135,5.7529743 33.30833,4.80066802 34.717885,5.03561087 Z M38.375,19.4902885 L9.625,19.4902885 C7.719565,19.4902885 6.175,21.0348394 6.175,22.9402569 L6.175,34.4401516 C6.175,36.3455692 7.719565,37.89012 9.625,37.89012 L38.375,37.89012 C40.280435,37.89012 41.825,36.3455692 41.825,34.4401516 L41.825,22.9402569 C41.825,21.0348394 40.280435,19.4902885 38.375,19.4902885 Z M14.8575,23.802749 C16.28649,23.802749 17.445,24.9612484 17.445,26.3902253 L17.445,28.6902043 C17.445,30.1191812 16.28649,31.2776806 14.8575,31.2776806 C13.42851,31.2776806 12.27,30.1191812 12.27,28.6902043 L12.27,26.3902253 C12.27,24.9612484 13.42851,23.802749 14.8575,23.802749 Z M33.1425,23.802749 C34.57149,23.802749 35.73,24.9612484 35.73,26.3902253 L35.73,28.6902043 C35.73,30.1191812 34.57149,31.2776806 33.1425,31.2776806 C31.71351,31.2776806 30.555,30.1191812 30.555,28.6902043 L30.555,26.3902253 C30.555,24.9612484 31.71351,23.802749 33.1425,23.802749 Z" id="形状结合" fill="#444444" fill-rule="nonzero"></path>
</g>
</svg>`;

const aiTitleTextDiv = document.createElement('div');
aiTitleTextDiv.className = 'sparkLite-title-text'; // 修改类名
aiTitleTextDiv.textContent = 'AI摘要';
aiTitleDiv.appendChild(aiTitleTextDiv);

const aiTagDiv = document.createElement('div');
aiTagDiv.className = 'sparkLite-tag'; // 修改类名
aiTagDiv.id = 'sparkLite-tag'; // 修改 ID
aiTagDiv.textContent = 'Spark Lite'; // 修改显示文本
aiTitleDiv.appendChild(aiTagDiv);

const aiExplanationDiv = document.createElement('div');
aiExplanationDiv.className = 'sparkLite-explanation'; // 修改类名
aiExplanationDiv.innerHTML = '生成中...' + '<span class="blinking-cursor"></span>';
aiDiv.appendChild(aiExplanationDiv);

// 将创建的元素插入到目标元素的顶部
targetElement.insertBefore(aiDiv, targetElement.firstChild);
}

// --- removeExistingAIDiv 函数 ---
function removeExistingAIDiv() {
// 查找具有 "post-SparkLite" 类的元素
const existingAIDiv = document.querySelector(".post-SparkLite"); // 修改选择器

// 如果找到了这个元素,就从其父元素中删除它
if (existingAIDiv) {
existingAIDiv.parentElement.removeChild(existingAIDiv);
}
}


// --- 主要逻辑对象 ---
var sparkLite = { // 重命名对象
// --- getTitleAndContent 函数 (保持不变) ---
getTitleAndContent: function () {
try {
const title = document.title;
const container = document.querySelector(sparkLite_postSelector); // 假设您也重命名了配置变量
if (!container) {
console.warn('Spark Lite:找不到文章容器...');
return '';
}
const paragraphs = container.getElementsByTagName('p');
const headings = container.querySelectorAll('h1, h2, h3, h4, h5');
let content = '';

for (let h of headings) {
content += h.innerText + ' ';
}

for (let p of paragraphs) {
// 移除包含'http'的链接
const filteredText = p.innerText.replace(/https?:\/\/[^\s]+/g, '');
content += filteredText;
}

const combinedText = title + ' ' + content;
let wordLimit = 1000;
if (typeof sparkLite_wordLimit !== "undefined") { // 假设您也重命名了配置变量
wordLimit = sparkLite_wordLimit;
}
const truncatedText = combinedText.slice(0, wordLimit);
return truncatedText;
} catch (e) {
console.error('Spark Lite 错误:...', e);
return '';
}
},

// --- fetchSparkLiteSummary 函数 (核心修改) ---
fetchSparkLiteSummary: async function (content) {
const title = document.title;
const url = window.location.href;

// 先尝试从IndexedDB读取
try {
const db = await initDB();
const tx = db.transaction('summaries', 'readonly');
const store = tx.objectStore('summaries');
const request = store.get(url);

const cachedData = await new Promise((resolve) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => resolve(null);
});

if (cachedData?.summary) {
// 检查缓存是否过期(7天有效期)
const isExpired = Date.now() - cachedData.timestamp > 7 * 24 * 60 * 60 * 1000;
if (!isExpired) {
return cachedData.summary;
}
}
} catch (e) {
console.log('读取IndexedDB缓存失败', e);
}

const proxyApiUrl = "";
const requestDataToProxy = { content: content, title: title };
const timeout = 30000;

try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);

const response = await fetch(proxyApiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestDataToProxy),
signal: controller.signal
});

clearTimeout(timeoutId);
const data = await response.json();

if (response.ok) {
// 成功获取摘要后存入IndexedDB
try {
const db = await initDB();
const tx = db.transaction('summaries', 'readwrite');
tx.objectStore('summaries').put({
url: url,
summary: data.summary,
timestamp: Date.now()
});
} catch (e) {
console.log('IndexedDB写入失败', e);
}
return data.summary;
} else {
console.error(`代理或 API 错误: ${data.error || response.statusText}`);
return `获取摘要失败: ${data.error || `HTTP 状态码: ${response.status}`}`;
}
} catch (error) {
if (error.name === 'AbortError') {
console.error('Spark Lite 请求超时 (通过代理)');
return '获取文章摘要超时,请稍后刷新重试。';
} else {
console.error('Spark Lite 请求失败 (通过代理):', error);
if (error instanceof SyntaxError) {
return '获取文章摘要失败:代理服务器响应格式错误。';
}
return '获取文章摘要失败,请检查网络连接或代理服务器状态。';
}
}
},

// --- aiShowAnimation 函数 ---
// 可以修改 console.error 和 element.innerHTML 中的 "TianliGPT" 为 "Spark Lite"
aiShowAnimation: function (text) {
const element = document.querySelector(".sparkLite-explanation"); // 修改选择器
if (!element) {
return;
}

if (sparkLiteIsRunning) { // 修改变量名
return;
}

// 检查用户是否已定义 sparkLite_typingAnimate
if (typeof sparkLite_typingAnimate !== "undefined" && !sparkLite_typingAnimate) { // 修改变量名
element.innerHTML = text;
return;
}

sparkLiteIsRunning = true; // 修改变量名
const typingDelay = 25;
const waitingTime = 1000;
const punctuationDelayMultiplier = 6;

element.style.display = "block";
element.innerHTML = "生成中..." + '<span class="blinking-cursor"></span>';

let animationRunning = true;
let currentIndex = 0;
let initialAnimation = true;
let lastUpdateTime = performance.now();

const animate = () => {
if (currentIndex < text.length && animationRunning) {
const currentTime = performance.now();
const timeDiff = currentTime - lastUpdateTime;

const letter = text.slice(currentIndex, currentIndex + 1);
const isPunctuation = /[,。!、?,.!?]/.test(letter);
const delay = isPunctuation ? typingDelay * punctuationDelayMultiplier : typingDelay;

if (timeDiff >= delay) {
element.innerText = text.slice(0, currentIndex + 1);
lastUpdateTime = currentTime;
currentIndex++;

if (currentIndex < text.length) {
element.innerHTML =
text.slice(0, currentIndex) +
'<span class="blinking-cursor"></span>';
} else {
element.innerHTML = text;
element.style.display = "block";
sparkLiteIsRunning = false; // 修改变量名
observer.disconnect();// 暂停监听
}
}
requestAnimationFrame(animate);
}
}

// 使用IntersectionObserver对象优化ai离开视口后暂停的业务逻辑,提高性能
const observer = new IntersectionObserver((entries) => {
let isVisible = entries[0].isIntersecting;
animationRunning = isVisible; // 标志变量更新
if (animationRunning && initialAnimation) {
setTimeout(() => {
requestAnimationFrame(animate);
}, 200);
}
}, { threshold: 0 });
let post_ai = document.querySelector('.post-SparkLite'); // 修改选择器
observer.observe(post_ai);//启动新监听
},
}

// --- runSparkLite 函数 (保持不变) ---
function runSparkLite() { // 重命名函数
// 确保在运行前移除可能存在的旧div,防止重复添加
removeExistingAIDiv();
// 插入新的占位符
insertAIDiv(sparkLite_postSelector);
const content = sparkLite.getTitleAndContent(); // 调用重命名后的对象和方法
if (content) {
// console.log('Spark Lite 本次提交的内容为:' + content); // 修改日志文本
} else {
// 如果没有获取到内容,可能需要移除占位符或显示错误
const aiExplanationDiv = document.querySelector(".sparkLite-explanation");
if (aiExplanationDiv) {
aiExplanationDiv.textContent = '未能获取到文章内容,无法生成摘要。';
}
return; // 提前退出,不进行 fetch
}
sparkLite.fetchSparkLiteSummary(content).then(summary => { // 调用重命名后的方法
sparkLite.aiShowAnimation(summary); // 调用重命名后的方法
});
}

// --- checkURLAndRun 函数 (稍微调整,主要负责URL检查) ---
function checkURLAndRun() {
// 检查 AI 是否已在运行,防止重复启动动画等
if (sparkLiteIsRunning) {
return false; // 返回 false 表示不应继续执行
}
// 检查 AI 容器是否已存在 (如果存在,理论上不应再次运行完整流程,除非是内容更新)
// 为简化逻辑,我们允许它继续,runSparkLite内部会处理移除和重新插入
// if (document.querySelector(".post-SparkLite")) {
// return false;
// }

// URL 检查逻辑
if (typeof sparkLite_postURL === "undefined") {
console.log("Spark Lite: No URL restriction.");
return true; // 返回 true 表示检查通过,可以运行
}

try {
const wildcardToRegExp = (s) => {
return new RegExp('^' + s.split(/\*+/).map(regExpEscape).join('.*') + '$');
};
const regExpEscape = (s) => {
return s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
};
const urlPattern = wildcardToRegExp(sparkLite_postURL);
const currentURL = window.location.href;

if (urlPattern.test(currentURL)) {
console.log("Spark Lite: URL matches.");
return true; // URL匹配,检查通过
} else {
console.log("Spark Lite:因为不符合自定义的链接规则,我决定不执行摘要功能。");
removeExistingAIDiv(); // 如果URL不匹配了,移除可能存在的旧AI框
return false; // URL不匹配,检查不通过
}
} catch (error) {
console.error("Spark Lite:我没有看懂你编写的自定义链接规则...", error);
return false; // 出错,检查不通过
}
}

// --- 新增:统一的初始化入口函数 ---
function initializeSparkLite() {
// 1. 检查文章容器是否存在
const targetElement = document.querySelector(sparkLite_postSelector);
if (!targetElement) {
// console.log("Spark Lite: Target post selector not found.");
removeExistingAIDiv(); // 确保目标容器不在时,AI框也被移除
return;
}

// 2. 执行URL和运行状态检查
if (checkURLAndRun()) {
// 3. 如果检查通过,执行核心逻辑
// console.log("Spark Lite: Initialization checks passed, running...");
runSparkLite();
} else {
// console.log("Spark Lite: Initialization checks failed (URL mismatch or already running).");
}
}


// --- Event Listeners (使用新的初始化函数) ---

// 确保在移除旧监听器(如果可能)后添加新的
// (在简单脚本场景下通常不需要移除,但这是良好实践)

// --- 增强路由变化监听 ---

// 保存原始的 pushState 和 replaceState 方法
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;

// 包装 pushState
history.pushState = function () {
// 调用原始方法
const result = originalPushState.apply(this, arguments);
// 创建并触发自定义事件,表明 URL 可能已更改
window.dispatchEvent(new Event('pushstate'));
// 触发我们的初始化函数
// 使用 setTimeout 确保在 DOM 更新后执行
setTimeout(initializeSparkLite, 100);
return result;
};

// 包装 replaceState
history.replaceState = function () {
// 调用原始方法
const result = originalReplaceState.apply(this, arguments);
// 创建并触发自定义事件,表明 URL 可能已更改
window.dispatchEvent(new Event('replacestate'));
// 触发我们的初始化函数
// 使用 setTimeout 确保在 DOM 更新后执行
setTimeout(initializeSparkLite, 100);
return result;
};

// 监听 popstate 事件 (浏览器前进/后退按钮)
window.addEventListener('popstate', () => {
// 触发我们的初始化函数
// 使用 setTimeout 确保在 DOM 更新后执行
setTimeout(initializeSparkLite, 100);
});

// --- (确保之前的事件监听器仍然存在) ---
// 初始加载
document.removeEventListener("DOMContentLoaded", initializeSparkLite); // 避免重复添加
document.addEventListener("DOMContentLoaded", initializeSparkLite);

2.在source/css创建一个gpt.css

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


/* AI */

.post-SparkLite {
background: var(--efu-secondbg);
border-radius: 12px;
padding: 12px;
line-height: 1.3;
border: var(--style-border-always);
margin: 16px 0;
}

@media screen and (max-width: 768px) {
.post-SparkLite {
margin-top: 22px;
}
}

.sparkLite-title {
display: flex;
color: var(--efu-lighttext);
border-radius: 8px;
align-items: center;
padding: 0 12px;
cursor: default;
user-select: none;
}

.sparkLite-title-text {
font-weight: bold;
margin-left: 8px;
line-height: 1;
}

.sparkLite-explanation {
margin-top: 12px;
padding: 8px 12px;
background: var(--efu-card-bg);
border-radius: 8px;
border: var(--style-border-always);
font-size: 15px;
line-height: 1.4;
display: flex;
}

.sparkLite-tag {
font-size: 12px;
background-color: var(--efu-lighttext);
color: var(--efu-card-bg);
font-weight: bold;
border-radius: 4px;
margin-left: auto;
line-height: 1;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: 0.3s;
}

.sparkLite-tag:hover {
background: var(--efu-fontcolor);
color: var(--efu-card-bg);
}

.sparkLite-title-icon {
width: 20px;
height: 20px;
}

.sparkLite-title-icon svg {
width: 20px;
height: 20px;
fill: var(--efu-main);
}

.sparkLite-title-icon svg path {
fill: var(--efu-main);
}

.tianliGPT-title {
display: flex;
color: var(--efu-lighttext);
border-radius: 8px;
align-items: center;
padding: 0 12px;
cursor: default;
user-select: none;
}

.tianliGPT-title-text {
font-weight: bold;
margin-left: 8px;
line-height: 1;
}

.tianliGPT-explanation {
margin-top: 12px;
padding: 8px 12px;
background: var(--efu-card-bg);
border-radius: 8px;
border: var(--style-border-always);
font-size: 15px;
line-height: 1.4;
display: flex;
}

.tianliGPT-suggestions {
display: flex;
flex-wrap: wrap;
}

.tianliGPT-suggestions .tianliGPT-suggestions-item {
margin-top: 12px;
padding: 8px 12px;
background: var(--efu-card-bg);
border-radius: 8px 8px 8px 0;
border: var(--style-border-always);
font-size: 15px;
line-height: 1.4;
display: flex;
width: fit-content;
margin-right: 12px;
cursor: pointer;
transition: 0.3s;
}

.tianliGPT-suggestions .tianliGPT-suggestions-item:hover {
background: var(--efu-main);
color: var(--efu-white);
}

.blinking-cursor {
background-color: var(--efu-main);
width: 10px;
height: 16px;
display: inline-block;
vertical-align: middle;
animation: blinking-cursor 0.5s infinite;
-webkit-animation: blinking-cursor 0.5s infinite;
margin-left: 4px;
}

@keyframes blinking-cursor {
0% {
opacity: 1;
}
40% {
opacity: 1;
}
50% {
opacity: 0;
}
90% {
opacity: 0;
}
100% {
opacity: 1;
}
}

.tianliGPT-tag {
font-size: 12px;
background-color: var(--efu-lighttext);
color: var(--efu-card-bg);
font-weight: bold;
border-radius: 4px;
margin-left: auto;
line-height: 1;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: 0.3s;
}

.tianliGPT-tag:hover {
background: var(--efu-fontcolor);
color: var(--efu-card-bg);
}

ins.adsbygoogle {
margin: 16px 0;
background: var(--efu-card-bg);
border-radius: 12px;
overflow: hidden;
border: var(--style-border-always);
}

#tianliGPT-Toggle {
font-size: 12px;
background: var(--efu-lighttext);
color: var(--efu-card-bg);
padding: 4px;
border-radius: 4px;
margin-left: 6px;
transform: scale(0.8);
cursor: pointer;
transition: 0.3s;
font-weight: bold;
}

#tianliGPT-Toggle:hover {
background: var(--efu-fontcolor);
color: var(--efu-card-bg);
}

.tianliGPT-title-icon {
width: 20px;
height: 20px;
}

.tianliGPT-title-icon svg {
width: 20px;
height: 20px;
fill: var(--efu-main);
}

.tianliGPT-title-icon svg path {
fill: var(--efu-main);
}

3.打开博客页面,复制文章内容的选择器

注意,一定要是内容,不能包含底部信息什么的

在source/js下新建一个入口js文件custom.js

1
2
3
4
// --- 其他配置 (根据需要调整) ---
var sparkLite_postSelector = "#post > article"; // 文章内容容器的选择器,例如 #article-container, .post-content
var sparkLite_wordLimit = 1000; // 提交给 API 的最大字数限制
var sparkLite_typingAnimate = true; // 是否启用打字机效果

其中s的parkLite_postSelector就是复制的选择器

4.引入这三个文件

每个主题引入js,css方法不一样

比如solitude主题就是在_config.solitude.yml文件里的extends

1
2
3
4
5
6
7
8
9
extends:
# Insert in head
# 插入到 head
head:
- <script src="/js/custom.js"></script>
- <script src="/js/gpt.js"></script>
- <link rel="stylesheet" href="/css/gpt.css">

# --------------------------- end ---------------------------

最后,对其进行hexo一键三连,就会发现: