Skip to content

Commit 799dc75

Browse files
committed
允许跨域
1 parent 25a0eab commit 799dc75

File tree

4 files changed

+348
-2
lines changed

4 files changed

+348
-2
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ LinkSumm是一款使用AI大模型驱动的智能摘要提取器,您可以输
1111
* [x] 支持流式传输
1212
* [x] 支持限制IP请求频率
1313
* [x] 支持限制总结的字符串长度
14-
* [x] 支持请求SPA页面
14+
* [x] 支持请求SPA应用
15+
* [ ] 根据URL内容继续对话
1516
* [ ] 内容缓存
1617
* [ ] PWA支持
1718
* [ ] 开放API
1819
* [ ] 支持代理获取内容
1920

21+
2022
## 安装
2123

2224
> 目前仅支持Docker安装,请确保您已经安装Docker和Docker Compose

app/config/config.default.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
},
88
"app":{
99
"req_limit":100,
10-
"word_limit":3000,
10+
"word_limit":10000,
1111
"template_name":"index.html"
1212
},
1313
"site":{

app/main.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from app.routers.routers import router
77
from app.middleware.req_limit import req_limit # 直接导入函数,而不是整个模块
88
import json
9+
from fastapi.middleware.cors import CORSMiddleware
910

1011
# 声明全局变量
1112
config = None
@@ -37,6 +38,13 @@ async def lifespan(app: FastAPI):
3738

3839
# 注册中间件
3940
app.middleware("http")(req_limit)
41+
app.add_middleware(
42+
CORSMiddleware,
43+
allow_credentials=False,
44+
allow_origins=["*"],
45+
allow_methods=["*"],
46+
allow_headers=["*"],
47+
)
4048

4149
# 将路由添加到应用中
4250
app.include_router(router)

app/templates/custom.html

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
<!DOCTYPE html>
2+
<html lang="zh-CN">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6+
<title>LinkSumm - 输入URL让AI为您总结</title>
7+
<meta name="description" content="LinkSumm可以帮助您将网页内容进行总结,提取出重要信息。">
8+
<meta name="keywords" content="LinkSumm, AI总结, AI提取, 网页总结, 内容提取">
9+
<link rel="stylesheet" href="/static/element-ui/index.css">
10+
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🔗</text></svg>">
11+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
12+
<link rel="stylesheet" href="/static/css/style.css">
13+
<script defer src="https://tj.rss.ink/script.js" data-website-id="fc376c34-e38d-4d45-bf03-f369a2298782"></script>
14+
</head>
15+
<body>
16+
<div id="app">
17+
<div class="header">
18+
<h1><span class="icon">🔗</span> LinkSumm</h1>
19+
<p>输入一个URL地址,让AI为您总结内容</p>
20+
</div>
21+
22+
<div class="input-section">
23+
<!-- 模式选择 -->
24+
<div class="select-mode">
25+
<div>
26+
<el-radio-group v-model="mode">
27+
<el-radio-button label="快速请求"></el-radio-button>
28+
<el-tooltip class="item" effect="light" content="支持SPA单页应用,但速度较慢" placement="right">
29+
<el-radio-button label="深度请求"></el-radio-button>
30+
</el-tooltip>
31+
32+
</el-radio-group>
33+
</div>
34+
</div>
35+
<!-- 模式选择END -->
36+
<el-alert
37+
v-if="errorMessage"
38+
:title="errorMessage"
39+
type="error"
40+
show-icon
41+
:closable="false"
42+
style="margin-bottom: 20px;">
43+
</el-alert>
44+
45+
<div class="input-row">
46+
<el-input
47+
placeholder="输入URL地址 (例如: https://example.com/article)"
48+
v-model.trim="urlInput"
49+
prefix-icon="el-icon-link"
50+
size="medium"
51+
clearable
52+
@keyup.enter.native="startSummarization"
53+
:disabled="isLoading">
54+
55+
56+
</el-input>
57+
58+
<!-- 第二行内容 -->
59+
<div class="row-2">
60+
61+
<el-select
62+
v-model="selectedModel"
63+
placeholder="选择模型"
64+
:disabled="isLoading"
65+
size="medium">
66+
<el-option v-if="models.length === 0" label="加载模型中..." value="" disabled></el-option>
67+
<el-option label="自动选择" value="auto"></el-option>
68+
<el-option
69+
v-for="model in models"
70+
:key="model.model"
71+
:label="model.name"
72+
:value="model.model">
73+
</el-option>
74+
</el-select>
75+
76+
<el-button
77+
type="primary"
78+
@click="startSummarization"
79+
:loading="isLoading"
80+
:disabled="!urlInput || isLoading"
81+
icon="el-icon-s-promotion"
82+
size="medium">
83+
开始总结
84+
</el-button>
85+
</div>
86+
<!-- 第二行内容END -->
87+
</div>
88+
</div>
89+
90+
<div v-if="outputContent != ''" class="result-section">
91+
<h2>标题: ${ originalTitle }</h2>
92+
<h3>总结:</h3>
93+
<div id="summary-output" v-html="outputContent"></div>
94+
</div>
95+
96+
<div class="footer">
97+
<p>&copy;2025 <a rel="nofollow" target="_blank" href="https://github.com/helloxz/linksumm">LinkSumm</a>. All Rights Reserved.</p>
98+
</div>
99+
</div>
100+
101+
<script src="/static/js/vue.min.js"></script>
102+
<script src="/static/element-ui/index.js"></script>
103+
<script src="/static/js/axios.min.js"></script>
104+
<script src="/static/js/marked.min.js"></script>
105+
106+
<script>
107+
new Vue({
108+
delimiters: ['${', '}'], // 修改为任意你喜欢的符号组合,避免和Jinja2冲突
109+
el: '#app',
110+
data() {
111+
return {
112+
mode: '快速请求', // 默认模式
113+
urlInput: '',
114+
selectedModel: 'auto', // Default to auto or the first available model
115+
models: [],
116+
isLoading: false,
117+
loadingStatus: '', // To show fetching/summarizing status
118+
originalTitle: '测试',
119+
originalContent: '', // Store original fetched content if needed
120+
summaryContent: '', // Raw summary stream
121+
errorMessage: '',
122+
outputContent:""// 输出内容
123+
}
124+
},
125+
computed: {
126+
renderedSummary() {
127+
if (this.summaryContent) {
128+
// Configure marked - enable GitHub Flavored Markdown
129+
marked.setOptions({
130+
gfm: true,
131+
breaks: false, // Use GFM line breaks
132+
pedantic: false,
133+
smartLists: true,
134+
smartypants: false
135+
});
136+
return marked.parse(this.summaryContent);
137+
}
138+
return '';
139+
}
140+
},
141+
methods: {
142+
handleUrlParam() {
143+
// 1. 获取地址栏上的?url=参数值
144+
const queryString = window.location.search;
145+
const urlParams = new URLSearchParams(queryString);
146+
const urlValue = urlParams.get('url');
147+
148+
// 如果没有url参数,直接返回
149+
if (!urlValue) return;
150+
151+
// 2. 验证URL是否合法
152+
try {
153+
// 使用URL构造函数验证URL合法性
154+
new URL(urlValue);
155+
} catch (e) {
156+
// URL不合法,直接返回
157+
return;
158+
}
159+
160+
// 3. 将合法URL赋值给this.urlInput
161+
this.urlInput = urlValue;
162+
163+
// 4. 调用startSummarization函数
164+
this.startSummarization();
165+
},
166+
async fetchModels() {
167+
try {
168+
const response = await axios.get('/api/get/models');
169+
if (response.data && response.data.code === 200) {
170+
this.models = response.data.data;
171+
} else {
172+
this.errorMessage = 'Failed to load AI models.';
173+
console.error("Error fetching models:", response.data);
174+
}
175+
} catch (error) {
176+
this.errorMessage = 'Error fetching AI models list.';
177+
console.error('Error fetching models:', error);
178+
}
179+
},
180+
181+
async startSummarization() {
182+
if (!this.urlInput || this.isLoading) {
183+
return;
184+
}
185+
// Basic URL validation (consider a more robust regex if needed)
186+
if (!this.urlInput.startsWith('http://') && !this.urlInput.startsWith('https://')) {
187+
this.errorMessage = '请输入以 http:// 或 https:// 开头的有效URL';
188+
return;
189+
}
190+
191+
this.isLoading = true;
192+
this.errorMessage = '';
193+
this.summaryContent = '';
194+
this.originalTitle = '';
195+
this.originalContent = '';
196+
this.outputContent = '';
197+
198+
199+
try {
200+
const formData = new URLSearchParams();
201+
formData.append('url', this.urlInput);
202+
// 判断mode的值
203+
if (this.mode === '深度请求') {
204+
formData.append('mode', 'deep');
205+
} else {
206+
formData.append('mode', 'fast');
207+
}
208+
// 1. Fetch Content
209+
const contentResponse = await axios.post('/api/get/content', formData);
210+
211+
if (contentResponse.data && contentResponse.data.code === 200) {
212+
this.originalTitle = contentResponse.data.data.title;
213+
this.originalContent = contentResponse.data.data.content;
214+
215+
if (!this.originalContent || this.originalContent === "No content could be extracted" || this.originalContent === "Error extracting content") {
216+
this.errorMessage = `无法从URL中提取可读内容 (${this.originalContent})`;
217+
this.isLoading = false;
218+
return;
219+
}
220+
221+
// 2. Fetch Summary (using fetch for streaming)
222+
await this.fetchSummaryStream(this.originalContent);
223+
224+
} else {
225+
throw new Error(contentResponse.data.msg || 'Failed to fetch content.');
226+
}
227+
228+
} catch (error) {
229+
this.errorMessage = `错误: ${error.message || '发生未知错误'}`;
230+
this.isLoading = false;
231+
// 清空标题和内容
232+
this.originalTitle = '';
233+
this.originalContent = '';
234+
this.summaryContent = '';
235+
this.outputContent = '';
236+
}
237+
},
238+
239+
async fetchSummaryStream(contentToSummarize) {
240+
const payload = {
241+
model: this.selectedModel,
242+
input: contentToSummarize
243+
};
244+
245+
try {
246+
const response = await fetch('/api/summ', {
247+
method: 'POST',
248+
headers: {
249+
'Content-Type': 'application/json',
250+
},
251+
body: JSON.stringify(payload),
252+
});
253+
254+
if (!response.ok) {
255+
let errorMsg = `网络响应错误 (状态: ${response.status})`;
256+
try {
257+
const errData = await response.json();
258+
errorMsg = errData.msg || errData.detail || errorMsg;
259+
} catch(e) { /* Ignore if response is not JSON */ }
260+
throw new Error(errorMsg);
261+
}
262+
263+
// Check if the response is JSON (likely an error before streaming started)
264+
const contentType = response.headers.get('Content-Type');
265+
if (contentType && contentType.includes('application/json')) {
266+
const jsonData = await response.json();
267+
if (jsonData.code !== 200) {
268+
throw new Error(jsonData.msg || 'Received an error response from the summary API.');
269+
}
270+
console.warn("Received unexpected JSON response from streaming endpoint:", jsonData);
271+
this.isLoading = false;
272+
return;
273+
}
274+
275+
// Process the stream
276+
const reader = response.body.getReader();
277+
const decoder = new TextDecoder('utf-8');
278+
let buffer = '';
279+
this.summaryContent = '';
280+
281+
while (true) {
282+
const { done, value } = await reader.read();
283+
if (done) {
284+
break;
285+
}
286+
287+
buffer += decoder.decode(value, { stream: true });
288+
const lines = buffer.split('\n');
289+
buffer = lines.pop() || '';
290+
291+
for (const line of lines) {
292+
if (line.trim() === '' || !line.startsWith('data:')) continue;
293+
294+
if (line.includes('[DONE]')) {
295+
await new Promise(resolve => setTimeout(resolve, 50));
296+
break;
297+
}
298+
299+
try {
300+
const jsonStr = line.substring(5).trim();
301+
const data = JSON.parse(jsonStr);
302+
if (data.value !== undefined) {
303+
this.summaryContent += data.value;
304+
this.outputContent = marked.parse(this.summaryContent);
305+
}
306+
} catch (e) {
307+
console.error('Failed to parse stream data line:', line, e);
308+
}
309+
}
310+
if (lines.some(line => line.includes('[DONE]'))) {
311+
let url = this.urlInput;
312+
this.outputContent = marked.parse(this.summaryContent + `\n\n> 原文来自:[${url}](${url})`);
313+
// 清空输入URL
314+
this.urlInput = '';
315+
break;
316+
}
317+
}
318+
319+
this.isLoading = false;
320+
321+
} catch (error) {
322+
console.error('Failed to fetch or process summary stream:', error);
323+
this.errorMessage = `总结过程中出错: ${error.message}`;
324+
this.isLoading = false;
325+
this.summaryContent = '';
326+
}
327+
}
328+
},
329+
mounted() {
330+
this.fetchModels();
331+
this.handleUrlParam();
332+
}
333+
});
334+
</script>
335+
</body>
336+
</html>

0 commit comments

Comments
 (0)