Compare commits

...

14 Commits

8
.idea/.gitignore vendored

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

@ -0,0 +1,6 @@
projectKey=EGA
serverUrl=http://localhost:9000
serverVersion=25.3.0.104237
dashboardUrl=http://localhost:9000/dashboard?id=EGA
ceTaskId=d2b0bc8f-f9f4-469a-8e42-440fcd7f4bf5
ceTaskUrl=http://localhost:9000/api/ce/task?id=d2b0bc8f-f9f4-469a-8e42-440fcd7f4bf5

@ -0,0 +1,3 @@
{
"Codegeex.RepoIndex": true
}

@ -191,7 +191,7 @@ A: 系统设计注重引导而非替代1AI提供建议而非直接答案
## 联系我们
### 开发团队
- **技术支持**267278466@qq.com
- **技术支持**
## 开源协议

@ -12,6 +12,7 @@ from typing import Dict, Any, Optional, List
from urllib.parse import urljoin
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from openai import OpenAI
# 确保正确的导入路径
current_dir = os.path.dirname(os.path.abspath(__file__))
@ -63,6 +64,7 @@ except Exception as e:
ANTHROPIC = "Anthropic"
GOOGLE = "Google"
LMSTUDIO = "LMStudio"
DEEPSEEK = "DeepSeek"
class LLMModel(BaseModel):
display_name: str
@ -160,6 +162,32 @@ class FastLLMClient:
return prompt
def _call_deepseek_api(self, prompt: str, model: LLMModel, max_tokens: int = 1000, **kwargs) -> str:
"""调用DeepSeek API通过openai包官方兼容方式"""
# 构建 system_message
messages = []
if kwargs.get('system_message'):
messages.append({"role": "system", "content": kwargs['system_message']})
messages.append({"role": "user", "content": prompt})
# 创建 OpenAI 客户端,指定 DeepSeek base_url
client = OpenAI(
api_key=getattr(model, "api_key", ""),
base_url="https://api.deepseek.com"
)
response = client.chat.completions.create(
model=model.model_name,
messages=messages,
temperature=kwargs.get('temperature', 0.7),
max_tokens=max_tokens,
stream=False
)
result = response.model_dump()
return result['choices'][0]['message']['content']
def _call_openai_api(self, prompt: str, model: LLMModel, max_tokens: int = 1000,
**kwargs) -> str:
"""调用OpenAI API - 优化版本"""
@ -313,16 +341,18 @@ class FastLLMClient:
elif model.provider == ModelProvider.GOOGLE:
response = self._call_google_api(context_prompt, model, max_tokens, **kwargs)
elif model.provider == ModelProvider.LMSTUDIO:
# LMStudio使用OpenAI兼容API
response = self._call_openai_api(context_prompt, model, max_tokens, **kwargs)
elif model.provider == ModelProvider.DEEPSEEK:
response = self._call_deepseek_api(context_prompt, model, max_tokens, **kwargs)
else:
raise ValueError(f"不支持的模型供应商: {model.provider}")
elapsed_time = time.time() - start_time
print(f"API调用耗时: {elapsed_time:.2f}")
return response.strip()
return response.strip()
except requests.exceptions.Timeout:
raise Exception("API调用超时")
except requests.exceptions.ConnectionError:

@ -13,6 +13,7 @@ class ModelProvider(str, Enum):
ANTHROPIC = "Anthropic"
DEEPSEEK = "DeepSeek"
GEMINI = "Gemini"
GOOGLE = "Google"
GROQ = "Groq"
OPENAI = "OpenAI"
OLLAMA = "Ollama"

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

@ -0,0 +1 @@
Revision:6239268765d383704e5cb48b62c9ead0898ce22d,CreatedAt:unknown

@ -0,0 +1,111 @@
{
"Global": {
"model_name": "PP-OCRv5_server_det"
},
"Hpi": {
"backend_configs": {
"paddle_infer": {
"trt_dynamic_shapes": {
"x": [
[
1,
3,
32,
32
],
[
1,
3,
736,
736
],
[
1,
3,
4000,
4000
]
]
}
},
"tensorrt": {
"dynamic_shapes": {
"x": [
[
1,
3,
32,
32
],
[
1,
3,
736,
736
],
[
1,
3,
4000,
4000
]
]
}
}
}
},
"PreProcess": {
"transform_ops": [
{
"DecodeImage": {
"channel_first": false,
"img_mode": "BGR"
}
},
{
"DetLabelEncode": null
},
{
"DetResizeForTest": {
"resize_long": 960
}
},
{
"NormalizeImage": {
"mean": [
0.485,
0.456,
0.406
],
"order": "hwc",
"scale": "1./255.",
"std": [
0.229,
0.224,
0.225
]
}
},
{
"ToCHWImage": null
},
{
"KeepKeys": {
"keep_keys": [
"image",
"shape",
"polys",
"ignore_tags"
]
}
}
]
},
"PostProcess": {
"name": "DBPostProcess",
"thresh": 0.3,
"box_thresh": 0.6,
"max_candidates": 1000,
"unclip_ratio": 1.5
}
}

@ -0,0 +1,53 @@
Global:
model_name: PP-OCRv5_server_det
Hpi:
backend_configs:
paddle_infer:
trt_dynamic_shapes: &id001
x:
- - 1
- 3
- 32
- 32
- - 1
- 3
- 736
- 736
- - 1
- 3
- 4000
- 4000
tensorrt:
dynamic_shapes: *id001
PreProcess:
transform_ops:
- DecodeImage:
channel_first: false
img_mode: BGR
- DetLabelEncode: null
- DetResizeForTest:
resize_long: 960
- NormalizeImage:
mean:
- 0.485
- 0.456
- 0.406
order: hwc
scale: 1./255.
std:
- 0.229
- 0.224
- 0.225
- ToCHWImage: null
- KeepKeys:
keep_keys:
- image
- shape
- polys
- ignore_tags
PostProcess:
name: DBPostProcess
thresh: 0.3
box_thresh: 0.6
max_candidates: 1000
unclip_ratio: 1.5

@ -0,0 +1 @@
Revision:08795e15fc7eb7ccc79a9b8d5241f59cfa61e5de,CreatedAt:unknown

@ -0,0 +1,169 @@
---
license: Apache License 2.0
library_name: PaddleOCR
language:
- English
pipeline_tag: image-to-text
tags:
- OCR
- PaddlePaddle
- PaddleOCR
- textline_recognition
---
# en_PP-OCRv5_mobile_rec
## Introduction
en_PP-OCRv5_mobile_rec is one of the PP-OCRv5_rec that are the latest generation text line recognition models developed by PaddleOCR team. It aims to efficiently and accurately support the recognition of English. The key accuracy metrics are as follow:
| Model | Accuracy (%) |
|-|-|
| en_PP-OCRv5_mobile_rec | 85.3|
**Note**: If any character (including punctuation) in a line was incorrect, the entire line was marked as wrong. This ensures higher accuracy in practical applications.
## Quick Start
### Installation
1. PaddlePaddle
Please refer to the following commands to install PaddlePaddle using pip:
```bash
# for CUDA11.8
python -m pip install paddlepaddle-gpu==3.0.0 -i https://www.paddlepaddle.org.cn/packages/stable/cu118/
# for CUDA12.6
python -m pip install paddlepaddle-gpu==3.0.0 -i https://www.paddlepaddle.org.cn/packages/stable/cu126/
# for CPU
python -m pip install paddlepaddle==3.0.0 -i https://www.paddlepaddle.org.cn/packages/stable/cpu/
```
For details about PaddlePaddle installation, please refer to the [PaddlePaddle official website](https://www.paddlepaddle.org.cn/en/install/quick).
2. PaddleOCR
Install the latest version of the PaddleOCR inference package from PyPI:
```bash
python -m pip install paddleocr
```
### Model Usage
You can quickly experience the functionality with a single command:
```bash
paddleocr text_recognition \
--model_name en_PP-OCRv5_mobile_rec \
-i https://cdn-uploads.huggingface.co/production/uploads/681c1ecd9539bdde5ae1733c/QmaPtftqwOgCtx0AIvU2z.png
```
You can also integrate the model inference of the text recognition module into your project. Before running the following code, please download the sample image to your local machine.
```python
from paddleocr import TextRecognition
model = TextRecognition(model_name="en_PP-OCRv5_mobile_rec")
output = model.predict(input="QmaPtftqwOgCtx0AIvU2z.png", batch_size=1)
for res in output:
res.print()
res.save_to_img(save_path="./output/")
res.save_to_json(save_path="./output/res.json")
```
After running, the obtained result is as follows:
```json
{'res': {'input_path': '/root/.paddlex/predict_input/QmaPtftqwOgCtx0AIvU2z.png', 'page_index': None, 'rec_text': 'the number of model parameters and FLOPs get larger, it', 'rec_score': 0.993655264377594}}
```
The visualized image is as follows:
![image/jpeg](https://cdn-uploads.huggingface.co/production/uploads/681c1ecd9539bdde5ae1733c/Xe-blNpCl-X-U1o3L4Rav.png)
For details about usage command and descriptions of parameters, please refer to the [Document](https://paddlepaddle.github.io/PaddleOCR/latest/en/version3.x/module_usage/text_recognition.html#iii-quick-start).
### Pipeline Usage
The ability of a single model is limited. But the pipeline consists of several models can provide more capacity to resolve difficult problems in real-world scenarios.
#### PP-OCRv5
The general OCR pipeline is used to solve text recognition tasks by extracting text information from images and outputting it in string format. And there are 5 modules in the pipeline:
* Document Image Orientation Classification Module (Optional)
* Text Image Unwarping Module (Optional)
* Text Line Orientation Classification Module (Optional)
* Text Detection Module
* Text Recognition Module
Run a single command to quickly experience the OCR pipeline:
```bash
paddleocr ocr -i https://cdn-uploads.huggingface.co/production/uploads/681c1ecd9539bdde5ae1733c/c3hSldnYVQXp48T5V0Ze4.png \
--text_recognition_model_name en_PP-OCRv5_mobile_rec \
--use_doc_orientation_classify False \
--use_doc_unwarping False \
--use_textline_orientation True \
--save_path ./output \
--device gpu:0
```
Results are printed to the terminal:
```json
{'res': {'input_path': '/root/.paddlex/predict_input/c3hSldnYVQXp48T5V0Ze4.png', 'page_index': None, 'model_settings': {'use_doc_preprocessor': True, 'use_textline_orientation': False}, 'doc_preprocessor_res': {'input_path': None, 'page_index': None, 'model_settings': {'use_doc_orientation_classify': False, 'use_doc_unwarping': False}, 'angle': -1}, 'dt_polys': array([[[252, 172],
...,
[254, 241]],
...,
[[665, 566],
...,
[663, 601]]], dtype=int16), 'text_det_params': {'limit_side_len': 64, 'limit_type': 'min', 'thresh': 0.3, 'max_side_limit': 4000, 'box_thresh': 0.6, 'unclip_ratio': 1.5}, 'text_type': 'general', 'textline_orientation_angles': array([-1, ..., -1]), 'text_rec_score_thresh': 0.0, 'return_word_box': False, 'rec_texts': ['The moon tells the sky', 'The sky tells the sea', 'The sea tells the tide', 'And the tide tells me', 'Lemn Sissay'], 'rec_scores': array([0.98405874, ..., 0.9837752 ]), 'rec_polys': array([[[252, 172],
...,
[254, 241]],
...,
[[665, 566],
...,
[663, 601]]], dtype=int16), 'rec_boxes': array([[252, ..., 241],
...,
[663, ..., 612]], dtype=int16)}}
```
If save_path is specified, the visualization results will be saved under `save_path`. The visualization output is shown below:
![image/jpeg](https://cdn-uploads.huggingface.co/production/uploads/681c1ecd9539bdde5ae1733c/DcAem61DifjkUQK9f-0iZ.png)
The command-line method is for quick experience. For project integration, also only a few codes are needed as well:
```python
from paddleocr import PaddleOCR
ocr = PaddleOCR(
text_recognition_model_name="en_PP-OCRv5_mobile_rec",
use_doc_orientation_classify=False, # Use use_doc_orientation_classify to enable/disable document orientation classification model
use_doc_unwarping=False, # Use use_doc_unwarping to enable/disable document unwarping module
use_textline_orientation=True, # Use use_textline_orientation to enable/disable textline orientation classification model
device="gpu:0", # Use device to specify GPU for model inference
)
result = ocr.predict("https://cdn-uploads.huggingface.co/production/uploads/681c1ecd9539bdde5ae1733c/6KQKOS42DKVEUnrticvhd.png")
for res in result:
res.print()
res.save_to_img("output")
res.save_to_json("output")
```
The default model used in pipeline is `PP-OCRv5_server_rec`, so it is needed that specifing to `en_PP-OCRv5_mobile_rec` by argument `text_recognition_model_name`. And you can also use the local model file by argument `text_recognition_model_dir`. For details about usage command and descriptions of parameters, please refer to the [Document](https://paddlepaddle.github.io/PaddleOCR/latest/en/version3.x/pipeline_usage/OCR.html#2-quick-start).
## Links
[PaddleOCR Repo](https://github.com/paddlepaddle/paddleocr)
[PaddleOCR Documentation](https://paddlepaddle.github.io/PaddleOCR/latest/en/index.html)

@ -0,0 +1,533 @@
{
"Global": {
"model_name": "en_PP-OCRv5_mobile_rec"
},
"Hpi": {
"backend_configs": {
"paddle_infer": {
"trt_dynamic_shapes": {
"x": [
[
1,
3,
48,
160
],
[
1,
3,
48,
320
],
[
8,
3,
48,
3200
]
]
}
},
"tensorrt": {
"dynamic_shapes": {
"x": [
[
1,
3,
48,
160
],
[
1,
3,
48,
320
],
[
8,
3,
48,
3200
]
]
}
}
}
},
"PreProcess": {
"transform_ops": [
{
"DecodeImage": {
"channel_first": false,
"img_mode": "BGR"
}
},
{
"MultiLabelEncode": {
"gtc_encode": "NRTRLabelEncode"
}
},
{
"RecResizeImg": {
"image_shape": [
3,
48,
320
]
}
},
{
"KeepKeys": {
"keep_keys": [
"image",
"label_ctc",
"label_gtc",
"length",
"valid_ratio"
]
}
}
]
},
"PostProcess": {
"name": "CTCLabelDecode",
"character_dict": [
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"!",
"\"",
"#",
"$",
"%",
"&",
"'",
"(",
")",
"*",
"+",
",",
"-",
".",
"/",
":",
";",
"<",
"=",
">",
"?",
"@",
"[",
"\\",
"]",
"_",
"`",
"{",
"|",
"}",
"^",
"~",
"©",
"®",
"℉",
"№",
"Ω",
"",
"™",
"∆",
"✓",
"✔",
"✗",
"✘",
"✕",
"☑",
"☒",
"●",
"▪",
"▫",
"◼",
"▶",
"◀",
"⬆",
"¤",
"¦",
"§",
"¨",
"ª",
"«",
"¬",
"¯",
"°",
"²",
"³",
"´",
"µ",
"¶",
"¸",
"¹",
"º",
"»",
"¼",
"½",
"¾",
"¿",
"×",
"",
"",
"",
"—",
"―",
"‖",
"‗",
"",
"",
"",
"",
"“",
"”",
"„",
"‟",
"†",
"‡",
"‣",
"",
"…",
"‧",
"‰",
"‴",
"",
"‶",
"‷",
"‸",
"",
"",
"※",
"‼",
"‽",
"‾",
"",
"₤",
"₡",
"₹",
"₽",
"₴",
"₿",
"¢",
"€",
"£",
"¥",
"",
"Ⅱ",
"Ⅲ",
"Ⅳ",
"",
"Ⅵ",
"Ⅶ",
"Ⅷ",
"Ⅸ",
"",
"Ⅺ",
"Ⅻ",
"",
"ⅱ",
"ⅲ",
"ⅳ",
"",
"ⅵ",
"ⅶ",
"ⅷ",
"ⅸ",
"",
"ⅺ",
"ⅻ",
"➀",
"➁",
"➂",
"➃",
"➄",
"➅",
"➆",
"➇",
"➈",
"➉",
"➊",
"➋",
"➌",
"➍",
"➎",
"➏",
"➐",
"➑",
"➒",
"➓",
"❶",
"❷",
"❸",
"❹",
"❺",
"❻",
"❼",
"❽",
"❾",
"❿",
"①",
"②",
"③",
"④",
"⑤",
"⑥",
"⑦",
"⑧",
"⑨",
"⑩",
"↑",
"→",
"↓",
"↕",
"←",
"↔",
"⇒",
"⇐",
"⇔",
"∀",
"∃",
"∄",
"∴",
"∵",
"∝",
"∞",
"∩",
"",
"∂",
"∫",
"∬",
"∭",
"∮",
"∯",
"∰",
"∑",
"∏",
"√",
"∛",
"∜",
"∱",
"∲",
"∳",
"",
"∷",
"",
"",
"",
"≈",
"≠",
"≡",
"≤",
"≥",
"⊂",
"⊃",
"⊥",
"⊾",
"⊿",
"□",
"∥",
"∋",
"ƒ",
"",
"″",
"À",
"Á",
"Â",
"Ã",
"Ä",
"Å",
"Æ",
"Ç",
"È",
"É",
"Ê",
"Ë",
"Ì",
"Í",
"Î",
"Ï",
"Ð",
"Ñ",
"Ò",
"Ó",
"Ô",
"Õ",
"Ö",
"Ø",
"Ù",
"Ú",
"Û",
"Ü",
"Ý",
"Þ",
"à",
"á",
"â",
"ã",
"ä",
"å",
"æ",
"ç",
"è",
"é",
"ê",
"ë",
"ì",
"í",
"î",
"ï",
"ð",
"ñ",
"ò",
"ó",
"ô",
"õ",
"ö",
"ø",
"ù",
"ú",
"û",
"ü",
"ý",
"þ",
"ÿ",
"Α",
"Β",
"Γ",
"Δ",
"Ε",
"Ζ",
"Η",
"Θ",
"Ι",
"Κ",
"Λ",
"Μ",
"Ν",
"Ξ",
"Ο",
"Π",
"Ρ",
"Σ",
"Τ",
"Υ",
"Φ",
"Χ",
"Ψ",
"Ω",
"α",
"β",
"γ",
"δ",
"ε",
"ζ",
"η",
"θ",
"ι",
"κ",
"λ",
"μ",
"ν",
"ξ",
"ο",
"π",
"ρ",
"σ",
"ς",
"τ",
"υ",
"φ",
"χ",
"ψ",
"ω",
"Å",
"ℏ",
"⌀",
"",
"⍵",
"𝑢",
"𝜓",
"",
"‥",
"︽",
"﹥",
"•",
"÷",
"",
"∙",
"⋅",
"·",
"±",
"∓",
"∟",
"∠",
"∡",
"∢",
"℧",
"☺"
]
}
}

@ -0,0 +1,479 @@
Global:
model_name: en_PP-OCRv5_mobile_rec
Hpi:
backend_configs:
paddle_infer:
trt_dynamic_shapes: &id001
x:
- - 1
- 3
- 48
- 160
- - 1
- 3
- 48
- 320
- - 8
- 3
- 48
- 3200
tensorrt:
dynamic_shapes: *id001
PreProcess:
transform_ops:
- DecodeImage:
channel_first: false
img_mode: BGR
- MultiLabelEncode:
gtc_encode: NRTRLabelEncode
- RecResizeImg:
image_shape:
- 3
- 48
- 320
- KeepKeys:
keep_keys:
- image
- label_ctc
- label_gtc
- length
- valid_ratio
PostProcess:
name: CTCLabelDecode
character_dict:
- '0'
- '1'
- '2'
- '3'
- '4'
- '5'
- '6'
- '7'
- '8'
- '9'
- A
- B
- C
- D
- E
- F
- G
- H
- I
- J
- K
- L
- M
- N
- O
- P
- Q
- R
- S
- T
- U
- V
- W
- X
- Y
- Z
- a
- b
- c
- d
- e
- f
- g
- h
- i
- j
- k
- l
- m
- n
- o
- p
- q
- r
- s
- t
- u
- v
- w
- x
- y
- z
- '!'
- '"'
- '#'
- $
- '%'
- '&'
- ''''
- (
- )
- '*'
- +
- ','
- '-'
- .
- /
- ':'
- ;
- <
- '='
- '>'
- '?'
- '@'
- '['
- \
- ']'
- _
- '`'
- '{'
- '|'
- '}'
- ^
- '~'
- ©
- ®
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ¤
- ¦
- §
- ¨
- ª
- «
- ¬
- ¯
- °
- ²
- ³
- ´
- µ
-
- ¸
- ¹
- º
- »
- ¼
- ½
- ¾
- ¿
- ×
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ¢
-
- £
- ¥
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ƒ
-
-
- À
- Á
- Â
- Ã
- Ä
- Å
- Æ
- Ç
- È
- É
- Ê
- Ë
- Ì
- Í
- Î
- Ï
- Ð
- Ñ
- Ò
- Ó
- Ô
- Õ
- Ö
- Ø
- Ù
- Ú
- Û
- Ü
- Ý
- Þ
- à
- á
- â
- ã
- ä
- å
- æ
- ç
- è
- é
- ê
- ë
- ì
- í
- î
- ï
- ð
- ñ
- ò
- ó
- ô
- õ
- ö
- ø
- ù
- ú
- û
- ü
- ý
- þ
- ÿ
- Α
- Β
- Γ
- Δ
- Ε
- Ζ
- Η
- Θ
- Ι
- Κ
- Λ
- Μ
- Ν
- Ξ
- Ο
- Π
- Ρ
- Σ
- Τ
- Υ
- Φ
- Χ
- Ψ
- Ω
- α
- β
- γ
- δ
- ε
- ζ
- η
- θ
- ι
- κ
- λ
- μ
- ν
- ξ
- ο
- π
- ρ
- σ
- ς
- τ
- υ
- φ
- χ
- ψ
- ω
-
-
-
-
-
- 𝑢
- 𝜓
-
-
-
-
-
- ÷
-
-
-
- ·
- ±
-
-
-
-
-
-
-

@ -9,6 +9,8 @@ import asyncio
import logging
from typing import Dict, List, Optional, Any
from config import get_config
import re
from typing import Dict, Optional, List, Union
# 配置调试日志
logging.basicConfig(level=logging.DEBUG)
@ -22,6 +24,47 @@ from models import ModelProvider, LLMModel
config = get_config()
def robust_json_parse(result: str) -> Dict:
"""增强的JSON解析函数处理常见格式问题"""
def fix_json(json_str: str) -> str:
"""修复常见JSON格式问题"""
# 1. 修复键名引号问题
json_str = re.sub(r"(\w+)\s*:", r'"\1":', json_str)
# 2. 修复字符串值引号问题
json_str = re.sub(r':\s*([\'"]?)([^\'",]+?)\1([,\]])', r': "\2"\3', json_str)
# 3. 移除尾随逗号
json_str = re.sub(r",\s*([}\]])", r"\1", json_str)
# 4. 修复数组中的字符串引号
json_str = re.sub(r'\[\s*([\'"]?)([^\'"]+?)\1\s*\]', r'["\2"]', json_str)
return json_str
try:
# 尝试直接解析
return json.loads(result)
except json.JSONDecodeError:
try:
# 尝试修复常见问题后解析
fixed = fix_json(result)
return json.loads(fixed)
except json.JSONDecodeError:
try:
# 尝试提取JSON部分
json_match = re.search(r'\{[\s\S]*\}', result)
if json_match:
return json.loads(fix_json(json_match.group()))
except:
pass
# 最终fallback
return {
"strengths": ["内容已提交"],
"issues": ["AI分析格式异常"],
"suggestions": ["建议重新分析"],
"next_steps": ["继续完善内容"],
"raw_response": result
}
class AIService:
"""AI服务类 - 统一上下文处理"""
@ -41,14 +84,14 @@ class AIService:
if user_settings:
return user_settings
return self.default_settings
def _create_llm_model(self, settings: Dict) -> LLMModel:
"""创建LLM模型对象"""
try:
provider = ModelProvider(settings['provider'])
except ValueError:
provider = ModelProvider.OLLAMA
logger.warning(f"未知的provider: {settings['provider']}已回退到OLLAMA")
model = LLMModel(
display_name=settings['model'],
model_name=settings['model'],
@ -157,7 +200,7 @@ Requirements:
raise Exception(f"生成题目失败: {str(e)}")
def analyze_content(self, content: str, stage: str, context: Dict,
user_settings: Optional[Dict] = None) -> Dict:
user_settings: Optional[Dict] = None) -> Dict:
"""分析写作内容"""
settings = self._get_ai_settings(user_settings)
model = self._create_llm_model(settings)
@ -167,7 +210,7 @@ Requirements:
stage_names = {
"brainstorm": "构思",
"outline": "提纲",
"outline": "提纲",
"writing": "正文"
}
@ -177,78 +220,126 @@ Requirements:
if ctx_info['subject'] == '英语':
prompt = f"""As an experienced American English writing instructor, please analyze this student's {stage_name} with the perspective of American academic writing standards.
Topic: {ctx_info['topic']}
Student's Content:
{content}
Topic: {ctx_info['topic']}
Student's Content:
{content}
Please analyze the content with American student writing style in mind, focusing on:
1. Content comprehension and meaning development
2. American writing conventions (thesis statements, topic sentences, transitions)
3. Voice, tone, and authentic expression
4. Critical thinking and argument development
5. Grammar and style appropriate for American academic writing
Please analyze the content with American student writing style in mind, focusing on:
1. Content comprehension and meaning development
2. American writing conventions (thesis statements, topic sentences, transitions)
3. Voice, tone, and authentic expression
4. Critical thinking and argument development
5. Grammar and style appropriate for American academic writing
Provide constructive feedback that helps the student write like an American student would, with natural flow and authentic voice.
Provide constructive feedback that helps the student write like an American student would, with natural flow and authentic voice.
请用JSON格式回复包含以下字段讲解文字用中文示范文字用英文
- strengths: 优点列表中文讲解
- issues: 问题列表中文讲解
- suggestions: 改进建议列表中文讲解英文示例要体现美国学生写作风格
- next_steps: 下一步建议列表中文讲解"""
请用完整JSON格式回复包含以下字段讲解文字用中文示范文字用英文
- strengths: 优点列表中文讲解
- issues: 问题列表中文讲解
- suggestions: 改进建议列表每个建议包含以下字段explanation: 改进理由中文讲解example: 示范文字英文示例要体现美国学生写作风格
- next_steps: 下一步建议列表中文讲解
请确保JSON格式一定要正确不要包含任何额外的文本或注释"""
else:
prompt = f"""作为专业的{ctx_info['subject']}写作指导老师,请分析学生的{stage_name}
{content}
{content}
请从以下方面给出建议
1. 优点和亮点
2. 存在的问题
3. 具体改进建议
4. 下一步建议
请从以下方面给出建议
1. 优点和亮点
2. 存在的问题
3. 具体改进建议
4. 下一步建议
请用JSON格式回复包含以下字段
- strengths: 优点列表
- issues: 问题列表
- suggestions: 改进建议列表
- next_steps: 下一步建议列表"""
请用JSON格式回复包含以下字段
- strengths: 优点列表
- issues: 问题列表
- suggestions: 改进建议列表每个建议包含explanation: 改进理由example: 示范示例
- next_steps: 下一步建议列表
请确保JSON格式一定要正确不要包含任何额外的文本或注释"""
try:
result = quick_generate(
prompt=prompt,
model=model,
max_tokens=600,
max_tokens=1500,
grade=ctx_info['grade'],
subject=ctx_info['subject'],
topic=ctx_info['topic'],
requirement=f"分析{stage_name}阶段的写作内容",
json_mode=True, # 启用JSON模式
temperature=0.3 # 降低温度以提高准确性
json_mode=True,
temperature=0.3
)
# 尝试解析JSON响应
try:
return json.loads(result)
except json.JSONDecodeError:
# 如果解析失败尝试提取JSON部分
import re
json_match = re.search(r'\{.*\}', result, re.DOTALL)
if json_match:
try:
return json.loads(json_match.group())
except json.JSONDecodeError:
pass
# 如果仍然失败,返回结构化的默认响应
return {
"strengths": ["内容已提交"],
"issues": ["AI分析格式异常"],
"suggestions": ["建议重新分析"],
"next_steps": ["继续完善内容"],
"raw_response": result
}
# 使用增强的JSON解析
print("===== 内容分析返回数据 =====")
print(result)
# 清理响应,移除可能的代码块标记
cleaned_result = result.strip()
if cleaned_result.startswith('```json'):
cleaned_result = cleaned_result[7:] # 移除开头的```json
if cleaned_result.endswith('```'):
cleaned_result = cleaned_result[:-3] # 移除结尾的```
cleaned_result = cleaned_result.strip()
analysis_data = robust_json_parse(cleaned_result)
# 确保数据结构正确
return self._validate_analysis_data(analysis_data, content)
except Exception as e:
raise Exception(f"分析内容失败: {str(e)}")
def _validate_analysis_data(self, analysis_data: Dict, content: str) -> Dict:
"""验证和补充内容分析数据"""
# 确保必要字段存在
required_fields = {
'strengths': [],
'issues': [],
'suggestions': [],
'next_steps': []
}
for field, default_value in required_fields.items():
if field not in analysis_data or analysis_data[field] is None:
analysis_data[field] = default_value
# 确保suggestions是列表格式且每个建议都有正确的结构
if not isinstance(analysis_data.get('suggestions'), list):
analysis_data['suggestions'] = []
# 转换suggestions格式如果AI返回了不同的格式
processed_suggestions = []
for suggestion in analysis_data['suggestions']:
if isinstance(suggestion, dict):
# 已经是对象格式,确保有必要的字段
processed_suggestion = {
'explanation': suggestion.get('explanation', ''),
'example': suggestion.get('example', '')
}
processed_suggestions.append(processed_suggestion)
elif isinstance(suggestion, str):
# 如果是字符串格式,转换为对象格式
processed_suggestions.append({
'explanation': suggestion,
'example': ''
})
analysis_data['suggestions'] = processed_suggestions
# 确保其他字段都是列表格式
for field in ['strengths', 'issues', 'next_steps']:
if not isinstance(analysis_data.get(field), list):
if isinstance(analysis_data.get(field), str):
# 如果是字符串,转换为单元素列表
analysis_data[field] = [analysis_data[field]]
else:
analysis_data[field] = []
return analysis_data
def evaluate_article(self, content: str, context: Dict,
user_settings: Optional[Dict] = None) -> Dict:
"""评估文章质量"""
@ -279,7 +370,8 @@ Consider how well the student demonstrates understanding of the topic and expres
- scores: 各维度得分对象 {{"content": 分数, "structure": 分数, "language": 分数, "innovation": 分数}}
- strengths: 优点列表中文讲解
- improvements: 改进建议列表中文讲解英文示例要体现美国学生写作风格
- comment: 总体评价中文"""
- comment: 总体评价中文
请确保JSON格式一定要正确不要包含任何额外的文本或注释"""
else:
prompt = f"""请评估这篇{ctx_info['article_type']}
@ -296,13 +388,14 @@ Consider how well the student demonstrates understanding of the topic and expres
- scores: 各维度得分对象 {{"content": 分数, "structure": 分数, "language": 分数, "innovation": 分数}}
- strengths: 优点列表
- improvements: 改进建议列表
- comment: 总体评价"""
- comment: 总体评价
请确保JSON格式一定要正确不要包含任何额外的文本或注释"""
try:
result = quick_generate(
prompt=prompt,
model=model,
max_tokens=800,
max_tokens=1000,
grade=ctx_info['grade'],
subject=ctx_info['subject'],
topic=ctx_info['topic'],
@ -340,6 +433,364 @@ Consider how well the student demonstrates understanding of the topic and expres
except Exception as e:
raise Exception(f"评估文章失败: {str(e)}")
def check_grammar(self, content: str, context: Dict,
user_settings: Optional[Dict] = None) -> Dict:
"""检查语法错误"""
settings = self._get_ai_settings(user_settings)
model = self._create_llm_model(settings)
# 提取上下文信息
ctx_info = self._extract_context_info(context)
# 根据学科制作不同的提示词
if ctx_info['subject'] == '英语':
prompt = f"""As an experienced American English teacher, please conduct a comprehensive grammar check for this {ctx_info['article_type']} written by a {ctx_info['grade']} student.
Topic: {ctx_info['topic']}
Student's Essay:
{content}
Please identify and correct grammatical errors with a focus on:
1. **Grammar Mistakes**: Subject-verb agreement, verb tenses, articles, prepositions
2. **Sentence Structure**: Run-on sentences, sentence fragments, parallel structure
3. **Punctuation**: Commas, periods, quotation marks, apostrophes
4. **Word Usage**: Wrong word choices, awkward phrasing, informal language
5. **Spelling**: Common spelling errors and typos
For each error found, please provide:
- The original text with error highlighted
- Explanation of the error (in Chinese for understanding)
- Corrected version (in proper English)
- Suggestion for improvement
请用完整JSON格式回复包含以下字段
- overall_assessment: 总体评价中文
- total_errors: 总错误数量
- error_categories: 错误分类统计 {{"grammar": 数量, "punctuation": 数量, "word_usage": 数量, "spelling": 数量}}
- errors: 错误列表每个错误包含
- original_text: 原始错误文本
- error_type: 错误类型
- explanation: 错误解释中文
- corrected_text: 修正后的文本
- suggestion: 改进建议中文
- score: 语法得分0-100
- recommendation: 学习建议中文
请确保JSON格式一定要正确不要包含任何额外的文本或注释"""
else:
prompt = f"""作为专业的语文老师,请对这篇{ctx_info['article_type']}进行语法检查:
题目{ctx_info['topic']}
学生作文
{content}
请检查以下方面的语法错误
1. **字词错误**错别字用词不当词语搭配错误
2. **句子错误**病句成分残缺搭配不当语序混乱
3. **标点错误**标点符号使用不当缺失或多余
4. **表达错误**表达不清逻辑混乱修辞不当
5. **格式错误**段落格式书写规范问题
对于每个发现的错误请提供
- 包含错误的原始文本
- 错误类型说明
- 错误解释
- 修改后的正确文本
- 改进建议
请用JSON格式回复包含以下字段
- overall_assessment: 总体评价
- total_errors: 总错误数量
- error_categories: 错误分类统计 {{"word": 数量, "sentence": 数量, "punctuation": 数量, "expression": 数量}}
- errors: 错误列表每个错误包含
- original_text: 原始错误文本
- error_type: 错误类型
- explanation: 错误解释
- corrected_text: 修正后的文本
- suggestion: 改进建议
- score: 语法得分0-100
- recommendation: 学习建议
请确保JSON格式一定要正确不要包含任何额外的文本或注释"""
try:
result = quick_generate(
prompt=prompt,
model=model,
max_tokens=1800,
grade=ctx_info['grade'],
subject=ctx_info['subject'],
topic=ctx_info['topic'],
requirement="语法检查",
json_mode=True,
temperature=0.1 # 低温度确保准确性
)
try:
grammar_data = json.loads(result)
except json.JSONDecodeError:
# 尝试提取可能的 JSON 子串并解析
import re
json_match = re.search(r'\{[\s\S]*\}', result)
if json_match:
try:
grammar_data = json.loads(json_match.group(0))
except json.JSONDecodeError:
logger.error("[语法检查] 提取到的JSON片段解析失败")
return {
"overall_assessment": "语法检查暂时不可用",
"total_errors": 0,
"error_categories": {},
"errors": [],
"score": 0,
"recommendation": "AI返回的数据无法解析请稍后重试",
"raw_response": result,
"error": "JSON解析失败提取片段后仍失败"
}
else:
logger.error("[语法检查] AI返回内容非JSON且未能提取JSON片段")
return {
"overall_assessment": "语法检查暂时不可用",
"total_errors": 0,
"error_categories": {},
"errors": [],
"score": 0,
"recommendation": "AI返回的数据格式不正确请稍后重试",
"raw_response": result,
"error": "返回非JSON"
}
# --------- END: 更严格的 JSON 解析 ----------
print("===== 语法检查返回数据 =====")
print(grammar_data)
print("==========================")
# 确保返回数据格式正确且包含必要的字段
grammar_data = self._validate_grammar_data(grammar_data, content)
return grammar_data
except Exception as e:
logger.error(f"语法检查失败: {str(e)}")
return {
"overall_assessment": "语法检查暂时不可用",
"total_errors": 0,
"error_categories": {},
"errors": [],
"score": 0,
"recommendation": "请稍后重试或检查网络连接",
"error": str(e)
}
def _validate_grammar_data(self, grammar_data: Dict, content: str) -> Dict:
"""验证和补充语法检查数据"""
# 确保必要字段存在
required_fields = {
'overall_assessment': '文章内容良好,但需要进一步改进语法准确性。',
'total_errors': 0,
'error_categories': {},
'errors': [],
'score': 100,
'recommendation': '建议继续练习,提高语法准确性。'
}
for field, default_value in required_fields.items():
if field not in grammar_data or grammar_data[field] is None:
grammar_data[field] = default_value
# 确保error_categories是字典格式
if not isinstance(grammar_data.get('error_categories'), dict):
grammar_data['error_categories'] = {}
# 确保errors是列表格式
if not isinstance(grammar_data.get('errors'), list):
grammar_data['errors'] = []
# 计算总错误数如果与error_categories不一致
if grammar_data['total_errors'] == 0 and grammar_data['errors']:
grammar_data['total_errors'] = len(grammar_data['errors'])
# 根据错误数量重新计算分数(如果分数不合理)
error_count = grammar_data['total_errors']
content_length = len(content.strip())
if content_length > 0:
# 每100字错误密度
error_density = error_count / max(content_length / 100, 1)
# 如果AI返回的分数不合理根据错误密度重新计算
if grammar_data['score'] > 90 and error_density > 2:
grammar_data['score'] = max(100 - error_density * 10, 0)
elif grammar_data['score'] < 10 and error_density < 1:
grammar_data['score'] = min(100 - error_density * 5, 100)
# 确保分数在合理范围内
grammar_data['score'] = max(0, min(100, grammar_data['score']))
return grammar_data
def vocabulary_upgrade(self, content: str, context: Dict,
user_settings: Optional[Dict] = None) -> Dict:
"""升级词汇,识别基础词汇并提供高阶替代"""
settings = self._get_ai_settings(user_settings)
model = self._create_llm_model(settings)
# 提取上下文信息
ctx_info = self._extract_context_info(context)
# 根据学科制作不同的提示词
if ctx_info['subject'] == '英语':
prompt = f"""As an experienced American English writing instructor, please analyze this {ctx_info['article_type']} and identify basic vocabulary that can be upgraded to more sophisticated alternatives.
Topic: {ctx_info['topic']}
Student's Content:
{content}
Please identify 5-8 basic words or phrases that could be replaced with more advanced vocabulary appropriate for {ctx_info['grade']} level American academic writing.
For each vocabulary item, provide:
1. The original basic word/phrase
2. 2-3 more sophisticated alternatives with explanations
3. Example sentences showing proper usage
4. Contextual guidance on when to use each alternative
Focus on vocabulary that would make the writing sound more like a native American student's work.
请用完整JSON格式回复包含以下字段讲解文字用中文示范文字用英文
- overall_assessment: 总体词汇水平评估中文
- total_suggestions: 总建议数量
- vocabulary_suggestions: 词汇建议列表每个建议包含
- original_word: 原始词汇
- alternatives: 替代词汇列表每个包含
- word: 高阶词汇
- meaning: 词汇含义解释中文
- usage_example: 使用示例英文
- explanation: 升级理由中文
- difficulty_level: 难度等级初级/中级/高级
- learning_tips: 学习建议列表中文
- next_steps: 后续学习步骤中文
请确保JSON格式一定要正确不要包含任何额外的文本或注释"""
else:
prompt = f"""作为专业的{ctx_info['subject']}写作指导老师,请分析这篇{ctx_info['article_type']}并识别可以升级的基础词汇。
题目{ctx_info['topic']}
学生作文
{content}
请识别5-8个可以升级的基础词汇或短语为每个词汇提供更高级的替代方案
对于每个词汇项目请提供
1. 原始基础词汇
2. 2-3个更高级的替代词汇及解释
3. 使用示例句子
4. 使用场景指导
请用JSON格式回复包含以下字段
- overall_assessment: 总体词汇水平评估
- total_suggestions: 总建议数量
- vocabulary_suggestions: 词汇建议列表每个建议包含
- original_word: 原始词汇
- alternatives: 替代词汇列表每个包含
- word: 高阶词汇
- meaning: 词汇含义解释
- usage_example: 使用示例
- explanation: 升级理由
- difficulty_level: 难度等级初级/中级/高级
- learning_tips: 学习建议列表
- next_steps: 后续学习步骤
请确保JSON格式一定要正确不要包含任何额外的文本或注释"""
try:
result = quick_generate(
prompt=prompt,
model=model,
max_tokens=2000,
grade=ctx_info['grade'],
subject=ctx_info['subject'],
topic=ctx_info['topic'],
requirement="词汇升级分析",
json_mode=True,
temperature=0.3
)
print("===== 词汇升级返回数据 =====")
print(result)
print("==========================")
try:
vocabulary_data = json.loads(result)
except json.JSONDecodeError:
# 尝试提取JSON部分
import re
json_match = re.search(r'\{.*\}', result, re.DOTALL)
if json_match:
try:
vocabulary_data = json.loads(json_match.group(0))
except json.JSONDecodeError:
logger.error("[词汇升级] 提取到的JSON片段解析失败")
return self._get_default_vocabulary_response(content)
else:
logger.error("[词汇升级] AI返回内容非JSON且未能提取JSON片段")
return self._get_default_vocabulary_response(content)
# 验证和补充词汇升级数据
vocabulary_data = self._validate_vocabulary_data(vocabulary_data, content)
return vocabulary_data
except Exception as e:
logger.error(f"词汇升级失败: {str(e)}")
return self._get_default_vocabulary_response(content, str(e))
def _get_default_vocabulary_response(self, content: str, error_msg: str = "") -> Dict:
"""获取默认的词汇升级响应"""
return {
"overall_assessment": "词汇升级功能暂时不可用" + (f"{error_msg}" if error_msg else ""),
"total_suggestions": 0,
"vocabulary_suggestions": [],
"learning_tips": ["请检查网络连接后重试", "建议先保存作品再尝试词汇升级"],
"next_steps": ["继续积累词汇量", "多阅读优秀范文"],
"error": error_msg or "未知错误"
}
def _validate_vocabulary_data(self, vocabulary_data: Dict, content: str) -> Dict:
"""验证和补充词汇升级数据"""
# 确保必要字段存在
required_fields = {
'overall_assessment': '词汇使用基本正确,有提升空间。',
'total_suggestions': 0,
'vocabulary_suggestions': [],
'learning_tips': ['多阅读优秀作品,积累词汇'],
'next_steps': ['定期复习升级的词汇']
}
for field, default_value in required_fields.items():
if field not in vocabulary_data or vocabulary_data[field] is None:
vocabulary_data[field] = default_value
# 确保vocabulary_suggestions是列表格式
if not isinstance(vocabulary_data.get('vocabulary_suggestions'), list):
vocabulary_data['vocabulary_suggestions'] = []
# 计算总建议数如果与vocabulary_suggestions不一致
if vocabulary_data['total_suggestions'] == 0 and vocabulary_data['vocabulary_suggestions']:
vocabulary_data['total_suggestions'] = len(vocabulary_data['vocabulary_suggestions'])
# 确保每个建议都有必要的字段
for suggestion in vocabulary_data['vocabulary_suggestions']:
if 'original_word' not in suggestion:
suggestion['original_word'] = '未知词汇'
if 'alternatives' not in suggestion or not isinstance(suggestion.get('alternatives'), list):
suggestion['alternatives'] = []
if 'explanation' not in suggestion:
suggestion['explanation'] = '可以升级为更高级的词汇'
if 'difficulty_level' not in suggestion:
suggestion['difficulty_level'] = '中级'
return vocabulary_data
def generate_suggestions(self, content: str, context: Dict,
suggestion_type: str = "improvement",
user_settings: Optional[Dict] = None) -> List[str]:
@ -374,7 +825,7 @@ Consider how well the student demonstrates understanding of the topic and expres
result = quick_generate(
prompt=prompt,
model=model,
max_tokens=400,
max_tokens=1000,
grade=ctx_info['grade'],
subject=ctx_info['subject'],
topic=ctx_info['topic'],
@ -533,7 +984,7 @@ Please provide:
result = quick_generate(
prompt=enhanced_prompt,
model=model,
max_tokens=1000,
max_tokens=1800,
grade=ctx_info['grade'],
subject=ctx_info['subject'],
topic=ctx_info['topic'],
@ -950,4 +1401,14 @@ def sync_test_connection(user_settings: Dict) -> bool:
def sync_health_check() -> Dict[str, Any]:
"""同步健康检查"""
return ai_service.health_check()
return ai_service.health_check()
def sync_check_grammar(content: str, context: Dict,
user_settings: Optional[Dict] = None) -> Dict:
"""同步语法检查"""
return ai_service.check_grammar(content, context, user_settings)
def sync_vocabulary_upgrade(content: str, context: Dict,
user_settings: Optional[Dict] = None) -> Dict:
"""同步词汇升级"""
return ai_service.vocabulary_upgrade(content, context, user_settings)

@ -8,12 +8,14 @@ import logging
import json
from datetime import datetime
from markupsafe import Markup
import sys
from config import get_config
from json_storage import init_default_data
from routes.main import main_bp
from routes.api import api_bp
from performance import PerformanceMiddleware
from routes.ocr import ocr_bp
from auth import auth_bp
# 获取配置
config = get_config()
@ -73,6 +75,8 @@ def create_app():
# 注册蓝图
app.register_blueprint(main_bp)
app.register_blueprint(api_bp)
app.register_blueprint(ocr_bp)
app.register_blueprint(auth_bp)
# 初始化性能监控
PerformanceMiddleware(app)
@ -81,17 +85,17 @@ def create_app():
@app.errorhandler(404)
def not_found(error):
"""404错误处理"""
return render_template('error.html',
error_code=404,
error_message='页面未找到'), 404
return render_template('error.html',
error_code=404,
error_message='页面未找到'), 404
@app.errorhandler(500)
def internal_error(error):
"""500错误处理"""
logger.error(f"内部服务器错误: {error}")
return render_template('error.html',
error_code=500,
error_message='内部服务器错误'), 500
error_code=500,
error_message='内部服务器错误'), 500
# 初始化JSON存储
try:
@ -103,16 +107,62 @@ def create_app():
logger.info("AI智能写作辅导软件启动成功")
return app
if __name__ == '__main__':
# 创建应用
app = create_app()
# 启动应用
print(f"启动AI智能写作辅导软件")
print(f"访问地址: http://{config.SERVER_HOST}:{config.SERVER_PORT}")
def is_desktop_mode():
"""检查是否在桌面模式下运行"""
return 'pywebview' in sys.modules or any('webview' in arg for arg in sys.argv)
def run_desktop_app():
"""运行桌面应用"""
try:
from desktop_app import DesktopApp
desktop_app = DesktopApp()
return desktop_app.start()
except ImportError as e:
print(f"桌面模式启动失败: {e}")
return False
import socket
def get_local_ip():
"""获取本机在局域网中的IP地址"""
try:
# 创建一个临时socket连接来获取本机IP
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
except:
return "无法获取IP地址"
app.run(
host=config.SERVER_HOST,
port=config.SERVER_PORT,
debug=config.DEBUG
)
if __name__ == '__main__':
# 检查是否以桌面模式运行
if is_desktop_mode():
print("🚀 启动桌面模式...")
success = run_desktop_app()
if not success:
print("💥 桌面模式启动失败回退到Web模式")
app = create_app()
config = get_config()
app.run(
host='0.0.0.0', # 修改为允许网络访问
port=config.SERVER_PORT,
debug=config.DEBUG,
use_reloader=False
)
else:
# 普通Web模式
app = create_app()
config = get_config()
local_ip = get_local_ip()
print(f"🌐 服务器启动信息:")
print(f"📍 本地访问: http://localhost:{config.SERVER_PORT}")
print(f"🌍 局域网访问: http://{local_ip}:{config.SERVER_PORT}")
print(f"🔧 调试模式: {'开启' if config.DEBUG else '关闭'}")
app.run(
host='0.0.0.0', # 关键修改:允许所有网络接口
port=config.SERVER_PORT,
debug=config.DEBUG,
use_reloader=False
)

@ -0,0 +1,255 @@
"""
认证蓝图 - 处理用户注册登录退出等功能
"""
from flask import Blueprint, request, jsonify, session, redirect, url_for, flash
from json_dao import UserDAO, with_user_dao
from auth_utils import validate_registration_data, hash_password, verify_password
import logging
logger = logging.getLogger(__name__)
# 创建认证蓝图
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
@auth_bp.route('/register', methods=['POST'])
def register():
"""用户注册"""
try:
data = request.get_json()
if not data:
return jsonify({
'success': False,
'message': '无效的请求数据'
}), 400
username = data.get('username', '').strip()
password = data.get('password', '')
confirm_password = data.get('confirm_password', '')
grade = data.get('grade', '初中')
subject = data.get('subject', '语文')
# 验证注册数据
is_valid, message = validate_registration_data(username, password, confirm_password)
if not is_valid:
return jsonify({
'success': False,
'message': message
}), 400
@with_user_dao
def _register_user(dao):
try:
user = dao.register_user(username, password, confirm_password, grade, subject)
return user, None # 成功时返回 (user, None)
except ValueError as e:
return None, str(e) # 错误时返回 (None, error_message)
user, error = _register_user()
if error:
return jsonify({
'success': False,
'message': error
}), 400
if user:
# 注册成功后自动登录
session['user_id'] = user['id']
session['username'] = user['username']
session['grade'] = user.get('grade', '初中')
session['subject'] = user.get('subject', '语文')
logger.info(f"用户注册成功: {username}")
return jsonify({
'success': True,
'message': '注册成功',
'data': {
'user_id': user['id'],
'username': user['username'],
'grade': user.get('grade'),
'subject': user.get('subject')
}
})
else:
return jsonify({
'success': False,
'message': '注册失败,请稍后重试'
}), 500
except Exception as e:
logger.error(f"注册过程出错: {str(e)}")
return jsonify({
'success': False,
'message': '注册失败,服务器错误'
}), 500
@auth_bp.route('/login', methods=['POST'])
def login():
"""用户登录"""
try:
data = request.get_json()
if not data:
return jsonify({
'success': False,
'message': '无效的请求数据'
}), 400
username = data.get('username', '').strip()
password = data.get('password', '')
if not username or not password:
return jsonify({
'success': False,
'message': '用户名和密码不能为空'
}), 400
@with_user_dao
def _authenticate_user(dao):
user = dao.authenticate_user(username, password)
return user
user = _authenticate_user()
if user:
# 登录成功设置session
session['user_id'] = user['id']
session['username'] = user['username']
session['grade'] = user.get('grade', '初中')
session['subject'] = user.get('subject', '语文')
logger.info(f"用户登录成功: {username}")
return jsonify({
'success': True,
'message': '登录成功',
'data': {
'user_id': user['id'],
'username': user['username'],
'grade': user.get('grade'),
'subject': user.get('subject')
}
})
else:
return jsonify({
'success': False,
'message': '用户名或密码错误'
}), 401
except Exception as e:
logger.error(f"登录过程出错: {str(e)}")
return jsonify({
'success': False,
'message': '登录失败,服务器错误'
}), 500
@auth_bp.route('/logout', methods=['POST'])
def logout():
"""用户退出"""
try:
# 清除session
session.clear()
return jsonify({
'success': True,
'message': '退出成功'
})
except Exception as e:
logger.error(f"退出过程出错: {str(e)}")
return jsonify({
'success': False,
'message': '退出失败'
}), 500
@auth_bp.route('/check', methods=['GET'])
def check_login():
"""检查登录状态"""
user_id = session.get('user_id')
if user_id:
@with_user_dao
def _get_user(dao):
return dao.get_user_by_id(user_id)
user = _get_user()
if user:
return jsonify({
'success': True,
'data': {
'user_id': user['id'],
'username': user['username'],
'grade': user.get('grade'),
'subject': user.get('subject'),
'is_logged_in': True
}
})
return jsonify({
'success': True,
'data': {
'is_logged_in': False
}
})
@auth_bp.route('/profile', methods=['PUT'])
def update_profile():
"""更新用户资料"""
try:
if 'user_id' not in session:
return jsonify({
'success': False,
'message': '请先登录'
}), 401
data = request.get_json()
if not data:
return jsonify({
'success': False,
'message': '无效的请求数据'
}), 400
user_id = session['user_id']
grade = data.get('grade')
subject = data.get('subject')
@with_user_dao
def _update_user(dao):
update_data = {}
if grade:
update_data['grade'] = grade
if subject:
update_data['subject'] = subject
if update_data:
user = dao.update_user(user_id, **update_data)
if user:
# 更新session中的信息
if grade:
session['grade'] = grade
if subject:
session['subject'] = subject
return user
return None
user = _update_user()
if user:
return jsonify({
'success': True,
'message': '资料更新成功',
'data': {
'grade': user.get('grade'),
'subject': user.get('subject')
}
})
else:
return jsonify({
'success': False,
'message': '资料更新失败'
}), 400
except Exception as e:
logger.error(f"更新资料过程出错: {str(e)}")
return jsonify({
'success': False,
'message': '资料更新失败,服务器错误'
}), 500

@ -71,4 +71,46 @@ def validate_password_strength(password: str) -> tuple[bool, str]:
# if not (has_letter and has_digit):
# return False, "密码必须包含字母和数字"
return True, "密码强度符合要求"
return True, "密码强度符合要求"
# 添加注册相关的验证函数
def validate_username(username: str) -> tuple[bool, str]:
"""验证用户名格式"""
if not username or len(username.strip()) < 3:
return False, "用户名至少需要3个字符"
if len(username) > 50:
return False, "用户名不能超过50个字符"
# 检查用户名格式(邮箱或普通用户名)
import re
if '@' in username:
# 邮箱格式验证
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(email_pattern, username):
return False, "邮箱格式不正确"
else:
# 普通用户名格式验证
username_pattern = r'^[a-zA-Z0-9_\u4e00-\u9fa5]+$'
if not re.match(username_pattern, username):
return False, "用户名只能包含字母、数字、下划线和中文字符"
return True, "用户名格式正确"
def validate_registration_data(username: str, password: str, confirm_password: str) -> tuple[bool, str]:
"""验证注册数据"""
# 验证用户名
is_valid, message = validate_username(username)
if not is_valid:
return False, message
# 验证密码
is_valid, message = validate_password_strength(password)
if not is_valid:
return False, message
# 验证密码确认
if password != confirm_password:
return False, "两次输入的密码不一致"
return True, "注册数据验证通过"

@ -0,0 +1,188 @@
#!/usr/bin/env python3
"""
桌面应用打包脚本
使用PyInstaller将应用打包为可执行文件
"""
import os
import sys
import shutil
import subprocess
from pathlib import Path
def check_dependencies():
"""检查打包依赖"""
required_packages = ['pywebview', 'flask', 'pyinstaller']
missing_packages = []
for package in required_packages:
try:
__import__(package)
except ImportError:
missing_packages.append(package)
if missing_packages:
print(f"❌ 缺少必要的包: {', '.join(missing_packages)}")
print("请运行: pip install " + " ".join(missing_packages))
return False
print("✓ 所有依赖包已安装")
return True
def create_spec_file():
"""创建PyInstaller spec文件"""
spec_content = """
# -*- mode: python ; coding: utf-8 -*-
import sys
from pathlib import Path
# 添加项目根目录到Python路径
project_root = Path(__file__).parent
sys.path.insert(0, str(project_root))
block_cipher = None
a = Analysis(
['desktop_app.py'],
pathex=[str(project_root)],
binaries=[],
datas=[
('templates/*', 'templates'),
('static/*', 'static'),
('config.env', '.'),
('requirements.txt', '.'),
],
hiddenimports=[
'flask',
'webview',
'json_dao',
'ai_service',
'ocr_service',
'auth_utils',
'performance',
'scoring_service',
'article_scoring_standards',
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='AI写作辅导软件',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False, # 设置为True可显示控制台窗口
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='static/favicon.ico' if os.path.exists('static/favicon.ico') else None,
)
"""
spec_path = Path('desktop_app.spec')
with open(spec_path, 'w', encoding='utf-8') as f:
f.write(spec_content)
return spec_path
def build_application():
"""打包应用"""
try:
print("🚀 开始打包应用...")
# 创建spec文件
spec_file = create_spec_file()
print("✓ Spec文件创建完成")
# 运行PyInstaller
cmd = [sys.executable, '-m', 'PyInstaller', '--clean', str(spec_file)]
print("📦 正在打包,这可能需要几分钟...")
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
if result.returncode == 0:
print("✅ 应用打包成功!")
# 显示输出目录
dist_dir = Path('dist')
if dist_dir.exists():
exe_files = list(dist_dir.glob('*.exe'))
if exe_files:
print(f"📁 可执行文件位置: {exe_files[0]}")
return True
else:
print(f"❌ 打包失败: {result.stderr}")
return False
except subprocess.CalledProcessError as e:
print(f"❌ 打包过程出错: {e}")
return False
except Exception as e:
print(f"❌ 打包失败: {e}")
return False
def cleanup():
"""清理临时文件"""
temp_dirs = ['build', 'dist', '__pycache__']
temp_files = ['desktop_app.spec']
for temp_dir in temp_dirs:
if Path(temp_dir).exists():
shutil.rmtree(temp_dir)
print(f"🧹 清理目录: {temp_dir}")
for temp_file in temp_files:
if Path(temp_file).exists():
Path(temp_file).unlink()
print(f"🧹 清理文件: {temp_file}")
def main():
"""主函数"""
print("=" * 60)
print("AI智能写作辅导软件 - 桌面应用打包工具")
print("=" * 60)
# 检查依赖
if not check_dependencies():
return
# 清理之前的构建
cleanup()
# 打包应用
if build_application():
print("\n🎉 打包完成!")
print("\n📋 使用说明:")
print("1. 在 'dist' 目录中找到可执行文件")
print("2. 可以直接运行或分发给其他用户")
print("3. 首次启动可能需要几秒钟初始化时间")
else:
print("\n💥 打包失败,请检查错误信息")
# 询问是否清理
if input("\n是否清理临时文件? (y/n): ").lower() == 'y':
cleanup()
if __name__ == '__main__':
main()

@ -16,9 +16,12 @@ class Config:
LLM_TIMEOUT = int(os.getenv('LLM_TIMEOUT', '30'))
# 服务器配置
SERVER_HOST = os.getenv('SERVER_HOST', '127.0.0.1')
SERVER_PORT = int(os.getenv('SERVER_PORT', '8080'))
DEBUG = os.getenv('DEBUG', 'True').lower() == 'true'
#SERVER_HOST = os.getenv('SERVER_HOST', '127.0.0.1')
#SERVER_PORT = int(os.getenv('SERVER_PORT', '8080'))
#DEBUG = os.getenv('DEBUG', 'True').lower() == 'true'
SERVER_HOST = '0.0.0.0' # 允许所有网络接口访问
SERVER_PORT = 5000
DEBUG = False # 生产环境建议关闭调试模式
# 数据库配置
DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///writing_assistant.db')

@ -0,0 +1,192 @@
{
"语文": {
"高中": [
"《慢下来,发现身边的美》 (2023 全国卷Ⅰ)",
"《论生逢其时》 (2021 北京卷)",
"《可为与有为》 (2021 全国卷Ⅲ)",
"《疫情中的距离与联系》 (2020 新高考卷)",
"《劳动的意义》 (2019 全国卷Ⅰ)",
"《时间的主人》 (2018 江苏卷)",
"《重读长辈这部书》 (2017 天津卷)",
"《我的高考》 (2016 全国卷Ⅱ)",
"《老规矩》 (2014 北京卷)",
"《忧与爱》 (2012 江苏卷)",
"《回到原点》 (2011 广东卷)",
"《仰望星空与脚踏实地》 (2010 北京卷)",
"《我说九零后》 (2009 天津卷)",
"《他们》 (2008 上海卷)",
"《怀想天空》 (2007 江苏卷)",
"《北京的符号》 (2006 北京卷)",
"《忘记与铭记》 (2005 全国卷)",
"《忙》 (2004 上海卷)",
"《感情亲疏和对事物的认知》 (2003 全国卷)",
"《心灵的选择》 (2002 全国卷)",
"《诚信》 (2001 全国卷)",
"《答案是丰富多彩的》 (2000 全国卷)",
"《假如记忆可以移植》 (1999 全国卷)",
"《坚韧——我追求的品格》 (1998 全国卷)",
"《乐于助人》 (1997 全国卷)",
"《漫画《给六指做整形手术》和《截错了》》 (1996 全国卷)",
"《鸟的评说》 (1995 全国卷)",
"《尝试》 (1994 全国卷)",
"《梧桐树下的对话》 (1993 全国卷)",
"《近墨者黑/近墨者未必黑》 (1991 全国卷)",
"《带刺的玫瑰花》 (1990 全国卷)",
"《习惯》 (1988 全国卷)",
"《理论对实践的指导意义》 (1987 全国卷)",
"《树木·森林·气候》 (1986 全国卷)",
"《给〈光明日报〉编辑部的信》 (1985 全国卷)",
"《对中学生作文的看法》 (1984 全国卷)",
"《挖井》 (1983 全国卷)",
"《先天下之忧而忧,后天下之乐而乐》 (1982 全国卷)",
"《毁树容易种树难》 (1981 全国卷)",
"《画蛋》 (1980 全国卷)",
"《陈伊玲的故事》 (1979 全国卷)",
"《速度问题是一个政治问题》 (1978 全国卷)",
"《我在这战斗的一年里》 (1977 北京卷)"
],
"初中": [
"《那一刻,我长大了》 (2023 北京卷)",
"《这也是一种荣誉》 (2022 浙江卷)",
"《藏在心底的温暖》 (2021 上海卷)",
"《我的青春里有___》 (2020 河南卷)",
"《成长的滋味》 (2019 广东卷)",
"《原来___》 (2018 江苏卷)",
"《不错过___》 (2017 湖南卷)",
"《陪伴》 (2016 安徽卷)",
"《这里也有乐趣》 (2015 上海卷)",
"《今天,我想说说心里话》 (2014 北京卷)",
"《心里美滋滋的》 (2013 上海卷)",
"《心里美滋滋的》 (2012 上海卷)",
"《悄悄地提醒》 (2011 上海卷)",
"《黑板上的记忆》 (2010 上海卷)",
"《在学海中游泳》 (2009 上海卷)",
"《我眼中的色彩》 (2008 上海卷)",
"《记住这一天》 (2007 上海卷)",
"《我们的名字叫___》 (2006 上海卷)",
"《充满活力的岁月》 (2005 上海卷)",
"《我们是初升的太阳》 (2004 上海卷)",
"《我想唱首歌》 (2003 上海卷)",
"《为自己竖起大拇指》 (2002 上海卷)",
"《有家真好》 (2001 上海卷)",
"《我也衔过一枚青橄榄》 (2000 上海卷)",
"《生活中的发现》 (1999 上海卷)",
"《我的欢乐》 (1998 上海卷)",
"《良师》 (1997 上海卷)",
"《变了》 (1996 上海卷)",
"《母爱》 (1995 上海卷)",
"《课后》 (1994 上海卷)",
"《我终于___》 (1993 上海卷)",
"《忘不了他(她)》 (1992 上海卷)",
"《___给了我___》 (1991 上海卷)",
"《在___面前》 (1990 上海卷)"
]
},
"英语": {
"高中": [
"The Power of Small Actions (2023 National)",
"Technology and Human Connection (2022 Beijing)",
"The Value of Cultural Diversity (2021 Shanghai)",
"Challenges and Opportunities of Online Learning (2020 New SAT)",
"My View on Artificial Intelligence (2023 Guangdong)",
"The Importance of Mental Health (2022 Zhejiang)",
"Sustainable Development and Our Responsibility (2021 Jiangsu)",
"The Impact of Social Media on Teenagers (2020 National)",
"The Meaning of Success (2019 TOEFL)",
"Cultural Differences in the Global Village (2018 IELTS)",
"The Role of Innovation in Modern Society (2017 National)",
"Environmental Protection: Everyone's Responsibility (2016 Beijing)",
"The Influence of Traditional Culture (2015 Shanghai)",
"My Understanding of Happiness (2014 Guangdong)",
"The Power of Dreams (2013 Zhejiang)",
"Challenges Faced by Today's Youth (2012 Jiangsu)",
"The Importance of Lifelong Learning (2011 National)",
"Technology's Impact on Communication (2010 TOEFL)",
"Globalization: Opportunities and Challenges (2009 IELTS)",
"The Value of Teamwork (2008 National)",
"My Ideal University Life (2007 Beijing)",
"The Impact of Internet on Education (2006 Shanghai)",
"The Importance of Cross-cultural Understanding (2005 Guangdong)",
"How to Balance Study and Leisure (2004 Zhejiang)",
"The Significance of Olympic Spirit (2003 Jiangsu)",
"My View on Examination System (2002 National)",
"The Role of Women in Modern Society (2001 TOEFL)",
"Environmental Issues in the 21st Century (2000 IELTS)",
"The Influence of Western Culture (1999 National)",
"My Favorite Book and Its Impact (1998 Beijing)",
"The Importance of English Learning (1997 Shanghai)",
"Changes in My Hometown (1996 Guangdong)",
"My Role Model (1995 Zhejiang)",
"The Value of Friendship (1994 Jiangsu)",
"My Dream Career (1993 National)",
"The Impact of Reform and Opening-up (1992 TOEFL)",
"Traditional Chinese Values (1991 IELTS)",
"The Importance of Sports (1990 National)",
"My Most Unforgettable Experience (1989 Beijing)",
"The Significance of Spring Festival (1988 Shanghai)",
"My Understanding of Patriotism (1987 Guangdong)",
"The Beauty of Chinese Language (1986 Zhejiang)",
"My Favorite Season (1985 Jiangsu)",
"The Importance of Family (1984 National)",
"My School Life (1983 TOEFL)",
"The Value of Hard Work (1982 IELTS)",
"Changes in Education System (1981 National)",
"My Hobbies and Interests (1980 Beijing)",
"Why I Want to Go to College (1979 National)",
"The Importance of Four Modernizations (1978 Shanghai)",
"My Hopes for the Future (1977 Guangdong)",
"The Meaning of New Life (1977 Beijing)"
],
"初中": [
"My Most Unforgettable Day (2023 Junior)",
"The Importance of Friendship (2022 Middle School)",
"How I Overcame a Fear (2021 Regional)",
"My Favorite Season (2020 Local)",
"A Person Who Inspires Me (2019 District)",
"My Dream School (2023 City Level)",
"The Book That Changed My View (2022 Provincial)",
"My Weekend Routine (2021 Municipal)",
"The Best Gift I Ever Received (2020 School Level)",
"My Favorite Festival (2019 Regional)",
"An Unforgettable Trip (2018 Junior)",
"My Hobbies and Interests (2017 Middle School)",
"The Importance of Healthy Lifestyle (2016 Regional)",
"My Family Members (2015 Local)",
"How I Learn English (2014 District)",
"My Favorite Teacher (2013 City Level)",
"A Memorable Birthday Party (2012 Provincial)",
"My Daily Life (2011 Municipal)",
"The Weather in My City (2010 School Level)",
"My Pet (2009 Regional)",
"My Best Friend (2008 Junior)",
"The Sports I Like (2007 Middle School)",
"My Favorite Food (2006 Regional)",
"My School (2005 Local)",
"How I Spend My Summer Vacation (2004 District)",
"My Neighborhood (2003 City Level)",
"The Movie I Like Best (2002 Provincial)",
"My Morning Routine (2001 Municipal)",
"The Changes in My Life (2000 School Level)",
"My Future Plan (1999 Regional)",
"Why I Like Learning English (1998 Junior)",
"My Favorite Subject (1997 Middle School)",
"A Happy Day (1996 Regional)",
"My Favorite Animal (1995 Local)",
"How I Help My Parents (1994 District)",
"My Favorite Color (1993 City Level)",
"The Person I Admire Most (1992 Provincial)",
"My Favorite Sport (1991 Municipal)",
"My Bedroom (1990 School Level)",
"My Daily Schedule (1989 Regional)",
"My First Day at School (1988 Junior)",
"My Favorite Season (1987 Middle School)",
"A Letter to My Friend (1986 Regional)",
"My Family (1985 Local)",
"My Favorite Holiday (1984 District)",
"How I Study English (1983 City Level)",
"My Dream (1982 Provincial)",
"My Hometown (1981 Municipal)",
"My Favorite Book (1980 School Level)"
]
}
}

@ -9,14 +9,14 @@
"grade": "初中",
"brainstorm_content": "",
"outline_content": "",
"writing_content": "",
"ai_feedback": "",
"final_score": 0,
"scores": "{\"brainstorm\": 0, \"outline\": 0, \"writing\": 0, \"highlight\": 0}",
"writing_content": "Generative AI are a hot topic. Some peoples thinks it make students lazy. Because they just use AI to do they homework. This is not completely true. Actually, if use correct, AI can helps student learning better. For example, when a student don't understand a concept, they can asks AI for explain. It give instant answer, more better than just search online. But, student must to think by themselves first. Rely on AI too much is bad. It can damaging they ability for independent thinking. So, the key is balance. We should use AI like a tool, not a replacement of our brain. In conclude, AI have both positive and negative affects. It is depend on how we uses it.",
"ai_feedback": "{\"writing_analysis\": {\"strengths\": [\"观点辩证学生能认识到AI的双面性既指出过度依赖的危害也肯定合理使用的价值\", \"结构完整:包含问题提出、正反论证和结论的基本框架\", \"立场明确:最终提出'关键在于平衡'的核心观点\", \"举例具体:用'不理解概念时询问AI'的案例支撑论点\"], \"issues\": [\"论点展开不足:缺乏分论点支撑,正反论证都停留在表面陈述\", \"学术规范缺失:没有明确的主题句和过渡词,段落间逻辑跳跃\", \"语言不地道:存在中式英语表达和语法错误\", \"论证深度不够:未涉及具体研究数据或教育理论支撑\", \"批判性思维薄弱未分析AI如何具体影响不同学习能力的学生\"], \"suggestions\": [{\"讲解\": \"建立清晰论点结构,使用主题句+支撑句模式\", \"示例\": \"While critics argue that generative AI fosters academic laziness, this perspective overlooks its potential as a cognitive tool when used intentionally. For instance, AI can serve as a 24/7 learning partner that provides customized explanations...\"}, {\"讲解\": \"增加学术过渡词和逻辑连接\", \"示例\": \"Conversely, unmonitored AI use may indeed undermine metacognitive skills. A study by Stanford University found that students who over-relied on AI for problem-solving showed decreased ability to...\"}, {\"讲解\": \"用具体案例替代泛泛而谈\", \"示例\": \"In Mr. Johnson's 8th-grade science class, students using AI for hypothesis refinement scored 23% higher on critical thinking assessments than those using traditional methods...\"}, {\"讲解\": \"强化批判性分析维度\", \"示例\": \"The central dilemma isn't whether to use AI, but how to design usage protocols that maximize its scaffolding function while minimizing dependency. Educators might consider...\"}, {\"讲解\": \"使用更地道的学术表达\", \"示例\": \"Rather than replacing human intellect, generative AI should function as a collaborative tool that amplifies our cognitive capabilities—much like calculators enhanced mathematical reasoning without eliminating the need to understand core principles.\"}], \"next_steps\": [\"学习美国中学议论文的经典五段式结构(引言-论点1-论点2-反论点-结论)\", \"收集关于AI教育影响的具体研究数据和权威来源\", \"练习使用学术过渡词however, furthermore, consequently等\", \"阅读《纽约时报》教育版相关文章,观察地道议论文表达\", \"尝试写作时先建立论证大纲,再展开具体段落\"]}}",
"final_score": 40,
"scores": "{\"brainstorm\": 0, \"outline\": 0, \"writing\": 2, \"highlight\": 2}",
"status": "writing",
"word_count": 0,
"word_count": 546,
"created_at": "2025-10-01T13:18:06.056827",
"updated_at": "2025-10-09T10:43:01.941600",
"updated_at": "2025-10-15T17:10:55.984866",
"completed_at": null
}
]

@ -6,10 +6,10 @@
"grade": "高中",
"subject": "英语",
"created_at": "2025-07-07T23:12:26.092924",
"ai_provider": "",
"ai_model": "",
"ai_api_key": "",
"ai_base_url": "",
"updated_at": "2025-10-09T10:43:25.409404"
"ai_provider": "DeepSeek",
"ai_model": "deepseek-chat",
"ai_api_key": "sk-ccdfd6d1973b45a084decf2654cf171a",
"ai_base_url": "https://api.deepseek.com",
"updated_at": "2025-10-15T17:05:01.345349"
}
]

@ -24,9 +24,6 @@ def init_db():
# 创建默认配置
create_default_config()
# 创建默认用户
create_default_user()
_db_initialized = True
print("数据库初始化成功")
@ -84,35 +81,7 @@ def create_default_config():
finally:
session.close()
def create_default_user():
"""创建默认用户"""
session = SessionLocal()
try:
# 检查是否已有用户
existing_user = session.query(User).filter_by(username='267278466@qq.com').first()
if existing_user:
return
# 创建默认用户
from auth_utils import hash_password
password_hash, _ = hash_password('default123') # 获取密码哈希字符串
default_user = User(
username='267278466@qq.com',
password_hash=password_hash,
grade='初中',
subject='语文'
)
session.add(default_user)
session.commit()
print("默认用户创建成功")
except Exception as e:
session.rollback()
print(f"创建默认用户失败: {e}")
raise
finally:
session.close()
# 移除create_default_user函数不再创建默认用户
def reset_db():
"""重置数据库"""

@ -0,0 +1,279 @@
#!/usr/bin/env python3
"""
AI智能写作辅导软件 - 桌面应用入口
使用pywebview将网页应用包装为桌面应用
"""
import webview
import threading
import os
import sys
import time
import logging
from pathlib import Path
# 添加项目根目录到Python路径
project_root = Path(__file__).parent
sys.path.insert(0, str(project_root))
# 导入配置文件
try:
from desktop_config import APP_NAME, WINDOW_CONFIG, SERVER_CONFIG, TEMP_DIR, LOG_DIR, DATA_DIR
except ImportError:
# 如果导入失败,使用默认配置
print("⚠️ 警告: 无法导入配置文件,使用默认配置")
APP_NAME = "EGA(默认)"
WINDOW_CONFIG = {'title': APP_NAME, 'width': 1800, 'height': 1200}
SERVER_CONFIG = {'host': '127.0.0.1', 'port': 8080}
TEMP_DIR = project_root / 'temp'
LOG_DIR = project_root / 'logs'
DATA_DIR = project_root / 'data'
from app import create_app
# 配置日志
LOG_DIR.mkdir(exist_ok=True) # 确保日志目录存在
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(LOG_DIR / 'desktop_app.log', encoding='utf-8'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
class DesktopApp:
"""桌面应用管理器"""
def __init__(self):
self.app = None
self.window = None
self.server_thread = None
self.is_running = False
# 使用统一的配置
self.config = {
'host': SERVER_CONFIG['host'],
'port': SERVER_CONFIG['port'],
'window_title': WINDOW_CONFIG['title'], # 使用统一的窗口标题
'window_width': WINDOW_CONFIG['width'],
'window_height': WINDOW_CONFIG['height'],
'min_width': WINDOW_CONFIG.get('min_width', 1000),
'min_height': WINDOW_CONFIG.get('min_height', 700),
'resizable': WINDOW_CONFIG.get('resizable', True),
'fullscreen': WINDOW_CONFIG.get('fullscreen', False),
'minimized': False
}
self.add_cleanup_handlers()
def clear_browser_cache(self):
"""清理浏览器缓存"""
try:
# 在应用启动前清理缓存
cache_dirs = [
TEMP_DIR / 'webview_cache',
Path.home() / '.cache' / 'pywebview'
]
for cache_dir in cache_dirs:
if cache_dir.exists():
import shutil
shutil.rmtree(cache_dir, ignore_errors=True)
print("已清理缓存目录:", cache_dir)
logger.info(f"已清理缓存目录: {cache_dir}")
except Exception as e:
print(" 清理缓存失败:", e)
logger.warning(f"清理缓存失败: {e}")
def start_flask_app(self):
"""启动Flask应用"""
try:
logger.info("正在启动Flask应用服务器...")
self.app = create_app()
# 禁用重载器以避免多线程问题
self.app.run(
host=self.config['host'],
port=self.config['port'],
debug=SERVER_CONFIG.get('debug', False),
use_reloader=False,
threaded=True
)
logger.info("Flask应用服务器已启动")
except Exception as e:
logger.error(f"启动Flask应用失败: {e}")
self.is_running = False
def start_server(self):
"""启动服务器线程"""
self.server_thread = threading.Thread(target=self.start_flask_app)
self.server_thread.daemon = True
self.server_thread.start()
# 等待服务器启动
max_wait_time = 30
wait_interval = 0.5
waited_time = 0
while waited_time < max_wait_time:
try:
import requests
response = requests.get(
f"http://{self.config['host']}:{self.config['port']}/",
timeout=1
)
if response.status_code == 200:
logger.info("服务器启动成功")
self.is_running = True
return True
except Exception as e:
logger.debug(f"等待服务器启动... ({waited_time:.1f}s) - {e}")
time.sleep(wait_interval)
waited_time += wait_interval
logger.error("服务器启动超时")
return False
def create_window(self):
"""创建应用窗口"""
try:
# 设置DPI感知
if hasattr(webview, 'dpi'):
webview.dpi = 96 # 或根据屏幕调整
url = f"http://{self.config['host']}:{self.config['port']}"
# 创建窗口,使用统一的配置
self.window = webview.create_window(
title=self.config['window_title'],
url=url,
width=self.config['window_width'],
height=self.config['window_height'],
min_size=(self.config['min_width'], self.config['min_height']),
resizable=self.config['resizable'],
fullscreen=self.config['fullscreen'],
minimized=self.config['minimized']
)
# 设置窗口事件处理
self.window.events.loaded += self.on_window_loaded
self.window.events.closing += self.on_window_closing
self.window.events.closed += self.on_window_closed
logger.info("应用窗口创建成功")
return True
except Exception as e:
logger.error(f"创建应用窗口失败: {e}")
return False
def on_window_loaded(self):
"""窗口加载完成事件"""
logger.info("应用窗口加载完成")
def on_window_closing(self):
"""窗口关闭事件"""
logger.info("应用窗口正在关闭...")
def on_window_closed(self):
"""窗口关闭完成事件"""
logger.info("应用窗口已关闭")
self.is_running = False
# 强制退出应用
os._exit(0)
def start(self):
"""启动桌面应用"""
try:
logger.info("正在启动AI智能写作辅导软件桌面版...")
# 清理缓存
self.clear_browser_cache()
# 启动服务器
if not self.start_server():
logger.error("服务器启动失败,应用无法启动")
return False
# 创建窗口
if not self.create_window():
logger.error("窗口创建失败,应用无法启动")
return False
# 启动GUI事件循环
logger.info("启动GUI事件循环")
webview.start(gui='qt', debug=False)
return True
except Exception as e:
logger.error(f"启动桌面应用失败: {e}")
return False
def add_cleanup_handlers(self):
"""添加清理处理器"""
import atexit
atexit.register(self.cleanup)
def cleanup(self):
"""应用退出时的清理工作"""
try:
if hasattr(self, 'window') and self.window:
self.window.destroy()
# 清理临时文件
self.clear_temp_files()
logger.info("应用清理完成")
except Exception as e:
logger.error(f"清理过程中出错: {e}")
def clear_temp_files(self):
"""清理临时文件"""
try:
temp_files = [
TEMP_DIR / 'cache',
TEMP_DIR / 'sessions'
]
for temp_dir in temp_files:
if temp_dir.exists():
import shutil
shutil.rmtree(temp_dir, ignore_errors=True)
except Exception as e:
logger.warning(f"清理临时文件失败: {e}")
def main():
"""主函数"""
print("=" * 60)
print("AI智能写作辅导软件 - 桌面版") # 统一标题
print("=" * 60)
# 检查依赖
try:
import webview
print("✓ pywebview 已安装")
except ImportError:
print("❌ 未安装 pywebview请运行: pip install pywebview")
return
try:
from app import create_app
print("✓ Flask应用导入成功")
except ImportError as e:
print(f"❌ 导入Flask应用失败: {e}")
return
# 创建并启动应用
desktop_app = DesktopApp()
success = desktop_app.start()
if success:
print("🎉 应用启动成功!")
else:
print("💥 应用启动失败,请检查日志文件")
if __name__ == '__main__':
main()

@ -0,0 +1,61 @@
"""
桌面应用配置文件
"""
import os
from pathlib import Path
# 应用基本信息
APP_NAME = "EGA"
APP_VERSION = "2.0.0"
APP_DESCRIPTION = "AI智能写作辅导软件 - 桌面版"
# 窗口配置
WINDOW_CONFIG = {
'title': APP_NAME, # 统一使用APP_NAME
'width': 1800,
'height': 1200,
'min_width': 1000,
'min_height': 700,
'resizable': True,
'fullscreen': False,
'frameless': False,
'easy_drag': True,
'on_top': False,
'confirm_close': True,
'hidpi': True,
'high_dpi': True,
'text_scale': 1.2 # 文本缩放比例
}
# 服务器配置
SERVER_CONFIG = {
'host': '127.0.0.1',
'port': 8080,
'debug': False
}
# 路径配置
BASE_DIR = Path(__file__).parent
DATA_DIR = BASE_DIR / 'data'
LOG_DIR = BASE_DIR / 'logs'
TEMP_DIR = BASE_DIR / 'temp'
# 创建必要目录
for directory in [DATA_DIR, LOG_DIR, TEMP_DIR]:
directory.mkdir(exist_ok=True)
# 特殊功能配置
FEATURE_FLAGS = {
'enable_tray_icon': True,
'enable_system_notifications': True,
'enable_auto_update': False,
'enable_hot_reload': False
}
# 主题配置
THEME_CONFIG = {
'dark_mode': False,
'accent_color': '#2196F3',
'background_color': '#FFFFFF',
'font_family': 'Microsoft YaHei, Arial, sans-serif'
}

@ -0,0 +1,38 @@
# diagnostic.py
import sys
import socket
import psutil
from pathlib import Path
def run_diagnostics():
print("=== 应用诊断信息 ===")
# 检查端口占用
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.bind(('127.0.0.1', 8080))
print("✓ 端口8080可用")
sock.close()
except OSError:
print("❌ 端口8080被占用")
# 检查必要目录
required_dirs = ['data', 'logs', 'temp']
for dir_name in required_dirs:
dir_path = Path(__file__).parent / dir_name
if dir_path.exists():
print(f"✓ 目录 {dir_name} 存在")
else:
print(f"❌ 目录 {dir_name} 缺失")
# 检查Python包
required_packages = ['webview', 'flask', 'pillow']
for package in required_packages:
try:
__import__(package)
print(f"✓ 包 {package} 已安装")
except ImportError:
print(f"❌ 包 {package} 未安装")
if __name__ == '__main__':
run_diagnostics()

@ -19,6 +19,11 @@ class UserDAO:
def create_user(self, username: str, password: str, grade: str, subject: str = '语文') -> Dict:
"""创建用户"""
# 验证用户名是否已存在
existing_user = UserStorage.get_user_by_username(username)
if existing_user:
raise ValueError("用户名已存在")
# 验证密码强度
is_valid, message = validate_password_strength(password)
if not is_valid:
@ -34,6 +39,14 @@ class UserDAO:
subject=subject
)
def register_user(self, username: str, password: str, confirm_password: str,
grade: str = '初中', subject: str = '语文') -> Dict:
"""用户注册"""
if password != confirm_password:
raise ValueError("密码确认不匹配")
return self.create_user(username, password, grade, subject)
def get_user_by_id(self, user_id: int) -> Optional[Dict]:
"""根据ID获取用户"""
return UserStorage.get_user_by_id(user_id)
@ -72,6 +85,20 @@ class ProjectDAO:
def __exit__(self, exc_type, exc_val, exc_tb):
pass
def update_project(self, project_id: int, **kwargs) -> Optional[Dict]:
"""更新项目信息"""
return ProjectStorage.update_project(project_id, **kwargs)
def save_vocabulary_upgrade(self, project_id: int, vocabulary_data: Dict[str, Any]) -> Optional[Dict]:
"""保存词汇升级结果"""
vocabulary_json = json.dumps(vocabulary_data, ensure_ascii=False)
return ProjectStorage.update_project(project_id, vocabulary_upgrade=vocabulary_json)
def save_grammar_check(self, project_id: int, grammar_data: Dict[str, Any]) -> Optional[Dict]:
"""保存语法检查结果"""
grammar_json = json.dumps(grammar_data, ensure_ascii=False)
return ProjectStorage.update_project(project_id, grammar_check=grammar_json)
def create_project(self, user_id: int, title: str, topic: str, article_type: str, subject: str, grade: str = '初中') -> Dict:
"""创建写作项目"""

@ -300,19 +300,7 @@ class ConfigStorage(JSONStorage):
def init_default_data():
"""初始化默认数据"""
# 创建默认用户
try:
from auth_utils import hash_password
password_hash, _ = hash_password('default123')
UserStorage.create_user(
username='267278466@qq.com',
password_hash=password_hash,
grade='初中',
subject='语文'
)
print("默认用户创建成功")
except ValueError:
print("默认用户已存在")
# 不再创建默认用户,用户需要自行注册
# 创建默认配置
default_configs = {

@ -0,0 +1,200 @@
"""
OCR服务模块 - 处理图片文字识别功能
"""
import os
import sys
import logging
import tempfile
from typing import Dict, List, Optional, Tuple
import json
logger = logging.getLogger(__name__)
class OCRService:
"""OCR服务类封装PaddleOCR功能"""
def __init__(self):
self.ocr_engine = None
self.is_initialized = False
self.initialize_ocr()
def initialize_ocr(self) -> bool:
"""初始化OCR引擎"""
print("初始化OCR引擎")
try:
# 设置PaddleOCR环境变量 - 使用相对路径
project_root = os.path.dirname(os.path.abspath(__file__))
paddle_models_dir = os.path.join(project_root, 'PaddleModels')
os.environ['PADDLE_HOME'] = paddle_models_dir
os.environ['HOME'] = paddle_models_dir
os.environ['PADDLE_MODELS_DIR'] = paddle_models_dir
os.environ['PADDLELEX_HOME'] = paddle_models_dir
os.environ['USERPROFILE'] = paddle_models_dir
os.environ['HOMEPATH'] = paddle_models_dir
# 确保目录存在
os.makedirs(paddle_models_dir, exist_ok=True)
print(f"PADDLE_HOME: {os.environ.get('PADDLE_HOME')}")
print(f"HOME: {os.environ.get('HOME')}")
# 重新导入模块以确保环境变量生效
if 'paddleocr' in sys.modules:
import importlib
importlib.reload(sys.modules['paddleocr'])
from paddleocr import PaddleOCR
# 初始化OCR引擎
self.ocr_engine = PaddleOCR(
lang='en',
use_doc_orientation_classify=False,
use_doc_unwarping=False,
use_textline_orientation=False,
)
self.is_initialized = True
logger.info("✓ PaddleOCR初始化成功")
print("✓ PaddleOCR初始化成功")
return True
except Exception as e:
logger.error(f"❌ OCR初始化失败: {e}")
print(f"❌ OCR初始化失败: {e}")
self.is_initialized = False
return False
def recognize_text_from_image(self, image_path: str) -> Dict:
"""
从图片中识别文字
Args:
image_path: 图片文件路径
Returns:
dict: 识别结果
"""
print("从图片识别文字")
if not self.is_initialized:
print("OCR服务未初始化")
return {
"success": False,
"error": "OCR服务正在初始化请稍后重试",
"text": ""
}
try:
if not os.path.exists(image_path):
return {
"success": False,
"error": f"图片文件不存在: {image_path}",
"text": ""
}
# 执行OCR识别
print(f"识别图片: {image_path}")
result = self.ocr_engine.ocr(img=image_path)
# 处理识别结果
recognized_text = self._process_ocr_result(result)
return {
"success": True,
"text": recognized_text,
#"raw_result": result,
"error": ""
}
except Exception as e:
logger.error(f"OCR识别失败: {e}")
print(f"OCR识别失败: {e}")
return {
"success": False,
"error": f"识别失败: {str(e)}",
"text": ""
}
def _process_ocr_result(self, result) -> str:
"""处理OCR识别结果提取文字内容并拼接完整文段"""
if not result:
return ""
# 从结果中提取识别出的文字列表
rec_texts = result[0].get('rec_texts', []) if isinstance(result, list) and result else []
if not rec_texts:
return ""
# 简单拼接所有识别出的文字
full_text = " ".join(rec_texts)
# 尝试根据常见标点符号进行简单的句子分割
# 这里可以根据实际需求添加更复杂的文本处理逻辑
sentences = []
current_sentence = ""
for word in rec_texts:
current_sentence += word + " "
# 如果单词包含句号、问号、感叹号,认为是一个句子的结束
if '.' in word or '?' in word or '!' in word:
sentences.append(current_sentence.strip())
current_sentence = ""
# 添加最后一个句子(如果有)
if current_sentence:
sentences.append(current_sentence.strip())
# 如果有分割出的句子,使用更自然的格式
if len(sentences) > 1:
formatted_text = ". ".join(sentences)
# 确保每个句子以句号结束
if not formatted_text.endswith('.'):
formatted_text += '.'
return formatted_text
else:
# 如果没有明显句子边界,返回简单拼接的结果
return full_text
def recognize_text_from_bytes(self, image_data: bytes) -> Dict:
"""
从字节数据中识别文字
Args:
image_data: 图片字节数据
Returns:
dict: 识别结果
"""
print("从字节数据识别文字")
try:
# 创建临时文件
with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as temp_file:
temp_file.write(image_data)
temp_file_path = temp_file.name
# 使用文件路径进行识别
print(f"临时文件路径: {temp_file_path}")
result = self.recognize_text_from_image(temp_file_path)
# 删除临时文件
try:
os.unlink(temp_file_path)
except:
pass
return result
except Exception as e:
logger.error(f"从字节数据识别失败: {e}")
return {
"success": False,
"error": f"处理图片数据失败: {str(e)}",
"text": ""
}
# 创建全局OCR服务实例
ocr_service = OCRService()

@ -7,7 +7,7 @@ from datetime import datetime
import json
from json_dao import UserDAO, ProjectDAO, with_user_dao, with_project_dao, dict_to_user, dict_to_project, dicts_to_projects
from ai_service import sync_generate_topic, sync_analyze_content, sync_evaluate_article, sync_health_check, sync_test_connection, sync_generate_suggestions, sync_generate_stage_suggestions
from ai_service import sync_generate_topic, sync_analyze_content, sync_evaluate_article, sync_health_check, sync_test_connection, sync_generate_suggestions, sync_generate_stage_suggestions, sync_check_grammar,sync_vocabulary_upgrade
from scoring_service import ScoringService
from ai_service import AIService
@ -92,10 +92,9 @@ def health_check():
@api_bp.route('/users/current', methods=['GET'])
def get_current_user():
"""获取当前用户信息"""
# 自动设置为已登录状态
# 检查用户是否登录
if 'user_id' not in session:
session['user_id'] = 1
session['username'] = '267278466@qq.com'
return error_response("用户未登录", 401)
user_id = session.get('user_id')
@ -118,10 +117,9 @@ def get_current_user():
@api_bp.route('/users/settings', methods=['PUT'])
def update_user_settings():
"""更新用户设置"""
# 自动设置为已登录状态
# 检查用户是否登录
if 'user_id' not in session:
session['user_id'] = 1
session['username'] = '267278466@qq.com'
return error_response("用户未登录", 401)
user_id = session.get('user_id')
@ -161,10 +159,9 @@ def update_user_settings():
@api_bp.route('/users/ai-settings', methods=['PUT'])
def update_user_ai_settings():
"""更新用户AI设置"""
# 自动设置为已登录状态
# 检查用户是否登录
if 'user_id' not in session:
session['user_id'] = 1
session['username'] = '267278466@qq.com'
return error_response("用户未登录", 401)
user_id = session.get('user_id')
@ -206,10 +203,9 @@ def update_user_ai_settings():
@api_bp.route('/users/ai-settings', methods=['GET'])
def get_user_ai_settings_api():
"""获取用户AI设置"""
# 自动设置为已登录状态
# 检查用户是否登录
if 'user_id' not in session:
session['user_id'] = 1
session['username'] = '267278466@qq.com'
return error_response("用户未登录", 401)
user_id = session.get('user_id')
@ -224,10 +220,9 @@ def get_user_ai_settings_api():
@api_bp.route('/projects', methods=['POST'])
def create_project():
"""创建写作项目"""
# 自动设置为已登录状态
# 检查用户是否登录
if 'user_id' not in session:
session['user_id'] = 1
session['username'] = '267278466@qq.com'
return error_response("用户未登录", 401)
user_id = session.get('user_id')
@ -322,10 +317,9 @@ def create_project():
@api_bp.route('/projects', methods=['GET'])
def get_projects():
"""获取项目列表"""
# 自动设置为已登录状态
# 检查用户是否登录
if 'user_id' not in session:
session['user_id'] = 1
session['username'] = '267278466@qq.com'
return error_response("用户未登录", 401)
user_id = session.get('user_id')
@ -347,10 +341,9 @@ def get_projects():
@api_bp.route('/projects/<int:project_id>', methods=['GET'])
def get_project(project_id):
"""获取项目详情"""
# 自动设置为已登录状态
# 检查用户是否登录
if 'user_id' not in session:
session['user_id'] = 1
session['username'] = '267278466@qq.com'
return error_response("用户未登录", 401)
user_id = session.get('user_id')
@ -375,10 +368,9 @@ def get_project(project_id):
@api_bp.route('/projects/<int:project_id>', methods=['PUT'])
def update_project(project_id):
"""更新项目"""
# 自动设置为已登录状态
# 检查用户是否登录
if 'user_id' not in session:
session['user_id'] = 1
session['username'] = '267278466@qq.com'
return error_response("用户未登录", 401)
user_id = session.get('user_id')
@ -430,10 +422,9 @@ def update_project(project_id):
@api_bp.route('/projects/<int:project_id>', methods=['DELETE'])
def delete_project(project_id):
"""删除项目"""
# 自动设置为已登录状态
# 检查用户是否登录
if 'user_id' not in session:
session['user_id'] = 1
session['username'] = '267278466@qq.com'
return error_response("用户未登录", 401)
user_id = session.get('user_id')
@ -496,10 +487,9 @@ def generate_topic():
@api_bp.route('/ai/analyze', methods=['POST'])
def analyze_content():
"""分析写作内容"""
# 自动设置为已登录状态
# 检查用户是否登录
if 'user_id' not in session:
session['user_id'] = 1
session['username'] = '267278466@qq.com'
return error_response("用户未登录", 401)
user_id = session.get('user_id')
@ -591,10 +581,9 @@ def analyze_content():
@api_bp.route('/ai/evaluate', methods=['POST'])
def evaluate_article():
"""评估文章质量"""
# 自动设置为已登录状态
# 检查用户是否登录
if 'user_id' not in session:
session['user_id'] = 1
session['username'] = '267278466@qq.com'
return error_response("用户未登录", 401)
user_id = session.get('user_id')
@ -686,10 +675,9 @@ def evaluate_article():
@api_bp.route('/ai/suggestions', methods=['POST'])
def generate_suggestions():
"""生成写作建议"""
# 自动设置为已登录状态
# 检查用户是否登录
if 'user_id' not in session:
session['user_id'] = 1
session['username'] = '267278466@qq.com'
return error_response("用户未登录", 401)
user_id = session.get('user_id')
@ -770,6 +758,196 @@ def generate_suggestions():
traceback.print_exc()
return error_response(f"生成建议失败: {str(e)}", 500)
@api_bp.route('/ai/vocabulary_upgrade', methods=['POST'])
def vocabulary_upgrade():
"""词汇升级"""
# 检查用户是否登录
if 'user_id' not in session:
return error_response("用户未登录", 401)
user_id = session.get('user_id')
try:
data = request.get_json()
content = data.get('content')
project_id = data.get('project_id')
if not content:
return error_response("内容不能为空")
# 获取用户信息
@with_user_dao
def _get_user_data(dao):
user = dao.get_user_by_id(user_id)
return user # JSON DAO直接返回字典
user_data = _get_user_data()
if not user_data:
return error_response("用户不存在", 404)
# 获取项目信息
project_data = None
if project_id:
@with_project_dao
def _get_project_data(dao):
proj = dao.get_project_by_id(project_id)
return proj if proj and proj.get('user_id') == user_id else None
project_data = _get_project_data()
# 构建AI上下文 - 优先使用项目的年级和学科信息
if project_data:
# 使用项目的年级和学科信息
context = {
'grade': project_data.get('grade', user_data.get('grade', '')),
'subject': project_data.get('subject', user_data.get('subject', '')),
'content': content,
'topic': project_data.get('topic', ''),
'article_type': project_data.get('article_type', ''),
'title': project_data.get('title', '')
}
else:
# 没有项目信息时使用用户设置
context = {
'grade': user_data.get('grade', ''),
'subject': user_data.get('subject', ''),
'content': content,
'topic': data.get('topic', ''),
'article_type': data.get('article_type', '')
}
# 获取用户AI设置
user_ai_settings = get_user_ai_settings(user_id)
# 词汇升级
vocabulary_result = sync_vocabulary_upgrade(
content=content,
context=context,
user_settings=user_ai_settings
)
# 保存词汇升级结果到项目
if project_id:
@with_project_dao
def _save_vocabulary_result(dao):
project = dao.get_project_by_id(project_id)
if project and project.get('user_id') == user_id:
# 更新项目的词汇升级信息
vocab_data_str = project.get('vocabulary_upgrade', '{}')
if not vocab_data_str or vocab_data_str.strip() == '':
vocab_data_str = '{}'
vocab_data = json.loads(vocab_data_str)
vocab_data['latest_upgrade'] = vocabulary_result
vocab_data['last_upgraded_at'] = datetime.utcnow().isoformat()
# 使用DAO更新项目 - 需要先添加这个字段到项目存储
dao.save_vocabulary_upgrade(project_id, vocab_data)
return True
return False
_save_vocabulary_result()
return success_response(vocabulary_result)
except Exception as e:
import traceback
traceback.print_exc()
return error_response(f"词汇升级失败: {str(e)}", 500)
@api_bp.route('/ai/check_grammar', methods=['POST'])
def check_grammar():
"""检查语法错误"""
# 检查用户是否登录
if 'user_id' not in session:
return error_response("用户未登录", 401)
user_id = session.get('user_id')
try:
data = request.get_json()
content = data.get('content')
project_id = data.get('project_id')
if not content:
return error_response("内容不能为空")
# 获取用户信息
@with_user_dao
def _get_user_data(dao):
user = dao.get_user_by_id(user_id)
return user # JSON DAO直接返回字典
user_data = _get_user_data()
if not user_data:
return error_response("用户不存在", 404)
# 获取项目信息
project_data = None
if project_id:
@with_project_dao
def _get_project_data(dao):
proj = dao.get_project_by_id(project_id)
return proj if proj and proj.get('user_id') == user_id else None
project_data = _get_project_data()
# 构建AI上下文 - 优先使用项目的年级和学科信息
if project_data:
# 使用项目的年级和学科信息
context = {
'grade': project_data.get('grade', user_data.get('grade', '')),
'subject': project_data.get('subject', user_data.get('subject', '')),
'content': content,
'topic': project_data.get('topic', ''),
'article_type': project_data.get('article_type', ''),
'title': project_data.get('title', '')
}
else:
# 没有项目信息时使用用户设置
context = {
'grade': user_data.get('grade', ''),
'subject': user_data.get('subject', ''),
'content': content,
'topic': data.get('topic', ''),
'article_type': data.get('article_type', '')
}
# 获取用户AI设置
user_ai_settings = get_user_ai_settings(user_id)
# 检查语法
grammar_result = sync_check_grammar(
content=content,
context=context,
user_settings=user_ai_settings
)
# 保存语法检查结果到项目
if project_id:
@with_project_dao
def _save_grammar_result(dao):
project = dao.get_project_by_id(project_id)
if project and project.get('user_id') == user_id:
# 更新项目的语法检查信息
grammar_data_str = project.get('grammar_check', '{}')
if not grammar_data_str or grammar_data_str.strip() == '':
grammar_data_str = '{}'
grammar_data = json.loads(grammar_data_str)
grammar_data['latest_check'] = grammar_result
grammar_data['last_checked_at'] = datetime.utcnow().isoformat()
# 使用DAO更新项目
dao.save_grammar_check(project_id, grammar_data)
return True
return False
_save_grammar_result()
return success_response(grammar_result)
except Exception as e:
import traceback
traceback.print_exc()
return error_response(f"语法检查失败: {str(e)}", 500)
# AI配置相关API
@api_bp.route('/ai/models/ollama', methods=['GET'])
def get_ollama_models():
@ -866,10 +1044,9 @@ def test_ai_connection():
@api_bp.route('/ai/score_article', methods=['POST'])
def score_article():
"""使用新的评分标准对文章进行评分"""
# 自动设置为已登录状态
# 检查用户是否登录
if 'user_id' not in session:
session['user_id'] = 1
session['username'] = '267278466@qq.com'
return error_response("用户未登录", 401)
user_id = session.get('user_id')

@ -13,42 +13,55 @@ main_bp = Blueprint('main', __name__)
@main_bp.route('/')
def index():
"""首页 - 重定向到项目页面"""
# 自动设置为已登录状态
# 移除自动登录代码
if 'user_id' not in session:
session['user_id'] = 1
session['username'] = '267278466@qq.com'
return redirect(url_for('main.auth', type='login')) # 重定向到登录页面
return redirect(url_for('main.projects'))
@main_bp.route('/login')
def login():
"""登录页面 - 直接重定向到项目页面"""
# 自动设置为已登录状态
# 移除自动登录代码
if 'user_id' not in session:
session['user_id'] = 1
session['username'] = '267278466@qq.com'
return render_template('auth.html', is_login=True) # 显示登录页面
return redirect(url_for('main.projects'))
@main_bp.route('/setup')
def setup():
"""项目设置页面"""
# 自动设置为已登录状态
# 移除自动登录代码,添加登录检查
if 'user_id' not in session:
session['user_id'] = 1
session['username'] = '267278466@qq.com'
return redirect(url_for('main.auth', type='login'))
return render_template('setup.html')
# 登录页面已删除
# 登录页面
@main_bp.route('/auth/<type>')
def auth(type):
"""认证页面(登录/注册)"""
is_login = (type == 'login')
return render_template('auth.html', is_login=is_login)
# 修改现有的路由,添加登录检查
def login_required(f):
"""登录装饰器"""
from functools import wraps
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
return redirect(url_for('main.auth', type='login'))
return f(*args, **kwargs)
return decorated_function
@main_bp.route('/projects')
def projects():
"""项目列表页面"""
# 自动设置为已登录状态
# 移除自动登录代码,添加登录检查
if 'user_id' not in session:
session['user_id'] = 1
session['username'] = '267278466@qq.com'
return redirect(url_for('main.auth', type='login'))
user_id = session.get('user_id')
@ -84,10 +97,9 @@ def projects():
@main_bp.route('/writing/<int:project_id>')
def writing(project_id):
"""写作页面"""
# 自动设置为已登录状态
# 移除自动登录代码,添加登录检查
if 'user_id' not in session:
session['user_id'] = 1
session['username'] = '267278466@qq.com'
return redirect(url_for('main.auth', type='login'))
user_id = session.get('user_id')
@ -128,10 +140,9 @@ def writing(project_id):
@main_bp.route('/review/<int:project_id>')
def review(project_id):
"""评阅页面"""
# 自动设置为已登录状态
# 移除自动登录代码,添加登录检查
if 'user_id' not in session:
session['user_id'] = 1
session['username'] = '267278466@qq.com'
return redirect(url_for('main.auth', type='login'))
user_id = session.get('user_id')
@ -154,5 +165,3 @@ def review(project_id):
except Exception as e:
flash(f'获取项目信息失败: {str(e)}', 'error')
return redirect(url_for('main.projects'))
# 登出功能已删除

@ -0,0 +1,122 @@
"""
OCR相关路由
"""
import os
import logging
from flask import Blueprint, request, jsonify, current_app
from werkzeug.utils import secure_filename
import base64
from ocr_service import ocr_service
logger = logging.getLogger(__name__)
# 创建OCR蓝图
ocr_bp = Blueprint('ocr', __name__)
def allowed_file(filename):
"""检查文件类型是否允许"""
allowed_extensions = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff'}
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in allowed_extensions
@ocr_bp.route('/api/ocr/recognize', methods=['POST'])
def recognize_text():
"""
图片文字识别接口
支持文件上传和base64数据
"""
print("收到OCR识别请求")
try:
# 检查是否有文件上传
if 'image' in request.files:
file = request.files['image']
if file.filename == '':
return jsonify({
"success": False,
"error": "未选择文件",
"text": ""
}), 400
print("处理上传的文件")
if file and allowed_file(file.filename):
# 读取文件数据
image_data = file.read()
result = ocr_service.recognize_text_from_bytes(image_data)
print("OCR识别完成,from file upload")
return jsonify(result)
else:
print("不支持的文件格式")
return jsonify({
"success": False,
"error": "不支持的文件格式",
"text": ""
}), 400
# 检查是否有base64数据
elif 'image_data' in request.json:
image_data_base64 = request.json['image_data']
try:
# 解码base64数据
if ',' in image_data_base64:
# 处理data URL格式
image_data_base64 = image_data_base64.split(',')[1]
image_data = base64.b64decode(image_data_base64)
result = ocr_service.recognize_text_from_bytes(image_data)
print("OCR识别完成,from base64 data")
return jsonify(result)
except Exception as e:
logger.error(f"Base64解码失败: {e}")
print("Base64解码失败")
return jsonify({
"success": False,
"error": "图片数据格式错误",
"text": ""
}), 400
else:
print("未提供图片文件或base64数据")
return jsonify({
"success": False,
"error": "请提供图片文件或base64数据",
"text": ""
}), 400
except Exception as e:
logger.error(f"OCR识别接口错误: {e}")
print("OCR识别接口发生错误")
return jsonify({
"success": False,
"error": f"服务器错误: {str(e)}",
"text": ""
}), 500
@ocr_bp.route('/api/ocr/status', methods=['GET'])
def get_ocr_status():
"""获取OCR服务状态"""
return jsonify({
"success": True,
"initialized": ocr_service.is_initialized,
"status": "ready" if ocr_service.is_initialized else "not_initialized"
})
@ocr_bp.route('/api/ocr/initialize', methods=['POST'])
def initialize_ocr():
"""手动初始化OCR服务"""
print("收到OCR初始化请求")
try:
success = ocr_service.initialize_ocr()
if success:
print("OCR服务初始化成功")
else:
print("OCR服务初始化失败")
return jsonify({
"success": success,
"message": "OCR初始化成功" if success else "OCR初始化失败"
})
except Exception as e:
return jsonify({
"success": False,
"error": f"初始化失败: {str(e)}"
}), 500

@ -607,4 +607,116 @@
max-width: 100%;
padding: 0;
}
}
}
/* 超小屏幕设备优化 - 字体调整 */
@media (max-width: 575.98px) {
body {
font-size: 14px !important;
line-height: 1.5 !important;
}
.hero-title {
font-size: 1.8rem !important;
}
.hero-subtitle {
font-size: 0.9rem !important;
}
.section-title h2 {
font-size: 1.5rem !important;
}
/* 写作界面字体优化 */
.writing-textarea {
font-size: 14px !important;
line-height: 1.6 !important;
}
.navbar-brand {
font-size: 1.1rem !important;
}
.btn {
font-size: 0.85rem !important;
}
.form-control,
.form-select {
font-size: 14px !important;
}
}
/* 超小屏幕字体优化 */
@media (max-width: 375px) {
body {
font-size: 13px !important;
}
.writing-textarea {
font-size: 13px !important;
}
.btn {
font-size: 0.8rem !important;
padding: 0.5rem 1rem !important;
}
.stage-btn {
font-size: 0.75rem !important;
padding: 6px 10px !important;
}
}
/* 触摸友好的按钮和链接 */
@media (max-width: 768px) {
.btn,
.stage-btn,
.nav-link,
.dropdown-item {
min-height: 44px !important;
min-width: 44px !important;
padding: 12px 16px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
/* 表单元素触摸优化 */
.form-control,
.form-select {
min-height: 44px !important;
font-size: 16px !important; /* 防止iOS缩放 */
padding: 12px !important;
}
}
/* 手机端间距系统 */
@media (max-width: 768px) {
.mobile-spacing {
/* 使用更紧凑的间距系统 */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 12px;
--spacing-lg: 16px;
--spacing-xl: 20px;
}
.container,
.writing-container,
.project-header,
.writing-stages,
.writing-content {
gap: var(--spacing-md) !important;
margin-bottom: var(--spacing-md) !important;
}
.card,
.ai-assistant,
.stage-content {
margin-bottom: var(--spacing-sm) !important;
}
.btn,
.form-control {
margin-bottom: var(--spacing-sm) !important;
}
}

@ -2,10 +2,10 @@
/* 全局样式优化 */
:root {
--primary-color: #667eea;
--secondary-color: #764ba2;
--success-color: #28a745;
--warning-color: #ffc107;
--primary-color: #2c3149;
--secondary-color: #443950;
--success-color: #45d166;
--warning-color: #d6a100;
--danger-color: #dc3545;
--info-color: #17a2b8;
--light-color: #f8f9fa;
@ -123,7 +123,7 @@ body {
}
.btn-primary:hover {
background: linear-gradient(135deg, #5a6fd8, #6a4190);
background: linear-gradient(135deg, #3d3d3d, #3f3f3f);
transform: translateY(-1px);
}
@ -244,7 +244,7 @@ body {
/* 写作页面样式 */
.writing-container {
max-width: 1400px;
max-width: auto;
margin: 0 auto;
padding: 20px;
}
@ -269,7 +269,7 @@ body {
}
.topic-label {
color: #6c757d;
color: #8a8a8a;
font-size: 0.9rem;
font-weight: 600;
margin-bottom: 0.5rem;
@ -788,7 +788,7 @@ body {
}
.breadcrumb-item.active {
color: #6c757d;
color: #58626c;
}
/* 页脚样式优化 */
@ -1043,4 +1043,13 @@ body {
.navbar-brand:hover {
transform: scale(1.02);
}
}
.logo-icon {
display: inline-block;
width: 20px;
height: 20px;
background-image: url('/static/logo.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}

@ -0,0 +1,108 @@
{
"vocabulary": [
{
"word": "perseverance",
"meaning": "坚持不懈,毅力",
"example": "Through perseverance, she overcame all obstacles and achieved her dream.",
"category": "品质"
},
{
"word": "eloquent",
"meaning": "雄辩的,有说服力的",
"example": "His eloquent speech moved the entire audience to tears.",
"category": "表达"
},
{
"word": "resilient",
"meaning": "有弹性的,适应力强的",
"example": "Children are remarkably resilient and can adapt to new environments quickly.",
"category": "品质"
},
{
"word": "conscientious",
"meaning": "认真的,自觉的",
"example": "She is a conscientious worker who always completes her tasks with great care and attention to detail.",
"category": "品质"
},
{
"word": "amicable",
"meaning": "友善的,心平气和的",
"example": "They reached an amicable agreement after a friendly discussion.",
"category": "品质"
},
{
"word": "meticulous",
"meaning": "一丝不苟的,极其谨慎的",
"example": "His meticulous approach to research ensured the accuracy of all his findings.",
"category": "品质"
},
{
"word": "superior",
"meaning": "优质的,上等的",
"example": "Our products are of superior quality compared to others in the market. [1,2](@ref)",
"category": "质量"
},
{
"word": "durable",
"meaning": "耐用的,持久的",
"example": "Customers like the goods durable in use because they last for a long time. [5](@ref)",
"category": "质量"
}
],
"expressions": [
{
"phrase": "A blessing in disguise",
"meaning": "因祸得福",
"example": "Losing that job was a blessing in disguise because it led me to a better opportunity.",
"category": "谚语"
},
{
"phrase": "Food for thought",
"meaning": "发人深省的东西",
"example": "The professor's lecture provided plenty of food for thought about climate change.",
"category": "习语"
},
{
"phrase": "Have a good one",
"meaning": "祝你好,再见(地道告别语)",
"example": "As I left the store, the cashier smiled and said, 'Have a good one!'",
"category": "日常表达"
},
{
"phrase": "I'm good",
"meaning": "不用了,我很好(委婉拒绝或表示满足)",
"example": "'Would you like more coffee?' 'No, I'm good. Thanks.'",
"category": "日常表达"
},
{
"phrase": "A touch of",
"meaning": "一点儿,略有",
"example": "I have a touch of flu and need some rest this weekend.",
"category": "习语"
},
{
"phrase": "Off the hook",
"meaning": "摆脱麻烦,脱身",
"example": "After he paid all the fines, he was finally off the hook.",
"category": "习语"
},
{
"phrase": "Hands down",
"meaning": "毫无疑问,绝对",
"example": "This is hands down the best movie I've seen this year.",
"category": "习语"
}
],
"writing_tips": [
"使用具体的例子来支持你的观点",
"尝试使用比喻和类比让文章更生动",
"注意段落之间的过渡要自然流畅",
"使用多样化的句式结构避免单调",
"开头要吸引读者,结尾要给人留下深刻印象",
"在邮件开头使用礼貌用语,如'I hope this e-mail finds you well'",
"使用状语前置来提升表达层次,如'In God we trust'",
"在正式请求中使用委婉表达,如'It would be great if you could...'",
"学会使用模糊语气词如-ish, or so, something等使表达更自然",
"使用'appreciate it'代替'thank you'表达更深刻的感激之情"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

@ -0,0 +1,294 @@
{% extends "base.html" %}
{% block title %}{% if is_login %}登录{% else %}注册{% endif %} - AI智能写作辅导软件{% endblock %}
{% block content %}
<div class="auth-container d-flex justify-content-center align-items-center">
<div class="row justify-content-center">
<div class="col-11 col-sm-11 col-md-11 col-lg-11 col-xl-11">
<div class="auth-card">
<div class="auth-header text-center mb-4">
<h2>
<img src="/static/logo.png" class="me-2" style="width: 3em; height: 3em;filter: invert(100%);" alt="logo">
EssayGraderAssistant
</h2>
<p class="text-muted">
{% if is_login %}
欢迎回来,请登录您的账户
{% else %}
创建新账户开始AI写作之旅
{% endif %}
</p>
</div>
<form id="authForm">
<div class="mb-3">
<label for="username" class="form-label">
<i class="fas fa-user me-1"></i>
用户名/邮箱
</label>
<input type="text" class="form-control" id="username" name="username"
placeholder="请输入用户名或邮箱地址" required>
<div class="invalid-feedback" id="usernameError"></div>
</div>
<div class="mb-3">
<label for="password" class="form-label">
<i class="fas fa-lock me-1"></i>
密码
</label>
<input type="password" class="form-control" id="password" name="password"
placeholder="请输入密码" required>
<div class="invalid-feedback" id="passwordError"></div>
</div>
{% if not is_login %}
<div class="mb-3">
<label for="confirmPassword" class="form-label">
<i class="fas fa-lock me-1"></i>
确认密码
</label>
<input type="password" class="form-control" id="confirmPassword" name="confirm_password"
placeholder="请再次输入密码" required>
<div class="invalid-feedback" id="confirmPasswordError"></div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="grade" class="form-label">
<i class="fas fa-graduation-cap me-1"></i>
年级
</label>
<select class="form-select" id="grade" name="grade">
<option value="小学">小学</option>
<option value="初中" selected>初中</option>
<option value="高中">高中</option>
<option value="大学">大学</option>
</select>
</div>
<div class="col-md-6">
<label for="subject" class="form-label">
<i class="fas fa-book me-1"></i>
学科
</label>
<select class="form-select" id="subject" name="subject">
<option value="语文" selected>语文</option>
<option value="英语">英语</option>
</select>
</div>
</div>
{% endif %}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-lg" id="submitBtn">
<i class="fas fa-sign-in-alt me-2"></i>
{% if is_login %}登录{% else %}注册{% endif %}
</button>
</div>
</form>
<div class="auth-footer text-center mt-4">
{% if is_login %}
<p class="text-muted">
还没有账户?
<a href="{{ url_for('main.auth', type='register') }}" class="text-primary">立即注册</a>
</p>
{% else %}
<p class="text-muted">
已有账户?
<a href="{{ url_for('main.auth', type='login') }}" class="text-primary">立即登录</a>
</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block head %}
<style>
.auth-container {
min-height: 80vh;
display: flex;
align-items: center;
padding: 2rem 0;
}
.auth-card {
background: white;
border-radius: 15px;
padding: 2.5rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
border: 1px solid #e9ecef;
}
.auth-header h2 {
color: #333;
font-weight: 700;
margin-bottom: 0.5rem;
}
.auth-footer a {
text-decoration: none;
font-weight: 500;
}
.auth-footer a:hover {
text-decoration: underline;
}
.form-label {
font-weight: 500;
color: #495057;
margin-bottom: 0.5rem;
}
.form-control, .form-select {
border-radius: 8px;
padding: 0.75rem 1rem;
border: 2px solid #e9ecef;
transition: all 0.3s ease;
}
.form-control:focus, .form-select:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 8px;
padding: 0.75rem;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.invalid-feedback {
display: block;
margin-top: 0.25rem;
font-size: 0.875rem;
}
@media (max-width: 768px) {
.auth-card {
padding: 2rem;
margin: 1rem;
}
.auth-container {
padding: 1rem 0;
}
}
</style>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const authForm = document.getElementById('authForm');
const submitBtn = document.getElementById('submitBtn');
const isLogin = {{ 'true' if is_login else 'false' }};
authForm.addEventListener('submit', async function(e) {
e.preventDefault();
// 清除之前的错误提示
clearErrors();
// 禁用提交按钮
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>处理中...';
try {
const formData = new FormData(authForm);
const data = Object.fromEntries(formData);
const url = isLogin ? '/auth/login' : '/auth/register';
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
// 显示成功消息
Utils.showAlert(result.message, 'success');
// 延迟跳转,让用户看到成功消息
setTimeout(() => {
window.location.href = '{{ url_for("main.projects") }}';
}, 1500);
} else {
// 显示错误消息
Utils.showAlert(result.message, 'error');
// 显示字段错误
if (result.errors) {
displayFieldErrors(result.errors);
}
}
} catch (error) {
console.error('认证错误:', error);
Utils.showAlert('网络错误,请稍后重试', 'error');
} finally {
// 恢复提交按钮
submitBtn.disabled = false;
submitBtn.innerHTML = isLogin ?
'<i class="fas fa-sign-in-alt me-2"></i>登录' :
'<i class="fas fa-user-plus me-2"></i>注册';
}
});
function clearErrors() {
const errorElements = document.querySelectorAll('.invalid-feedback');
errorElements.forEach(element => {
element.textContent = '';
element.style.display = 'none';
});
const inputs = document.querySelectorAll('.form-control, .form-select');
inputs.forEach(input => {
input.classList.remove('is-invalid');
});
}
function displayFieldErrors(errors) {
for (const [field, message] of Object.entries(errors)) {
const input = document.getElementById(field);
const errorElement = document.getElementById(field + 'Error');
if (input && errorElement) {
input.classList.add('is-invalid');
errorElement.textContent = message;
errorElement.style.display = 'block';
}
}
}
// 实时验证
authForm.addEventListener('input', function(e) {
const target = e.target;
if (target.classList.contains('is-invalid')) {
target.classList.remove('is-invalid');
const errorElement = document.getElementById(target.id + 'Error');
if (errorElement) {
errorElement.textContent = '';
errorElement.style.display = 'none';
}
}
});
});
</script>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save