Initial commit: 知识库管理器应用

- 实现基于 PySide6 的 GUI 界面
- 集成 FastAPI 知识库服务器 API
- 支持查看、编辑、提交 Markdown 文件
- 包含完整的 pytest-qt 测试套件
- 添加功能列表文档
This commit is contained in:
2026-01-30 12:03:12 +08:00
commit 0ab3d4f9c1
10 changed files with 695 additions and 0 deletions

80
FEATURES.md Normal file
View File

@@ -0,0 +1,80 @@
# 功能列表
## 核心功能
### 1. 服务器管理
- ✅ 服务器地址配置
- ✅ 连接测试
- ✅ 状态显示
- ✅ 默认地址预设http://43.134.1.17:8800/
### 2. 文件管理
- ✅ 获取服务器文件列表
- ✅ 自动过滤只显示 .md 文件
- ✅ 刷新文件列表
- ✅ 文件列表点击加载
### 3. 文件编辑
- ✅ 查看 Markdown 文件内容
- ✅ 在线编辑文件内容
- ✅ 文件名显示和编辑
- ✅ 自动添加 .md 扩展名
### 4. 文件操作
- ✅ 保存修改到服务器POST 请求)
- ✅ 提交新文件到服务器
- ✅ 文件内容验证
- ✅ 操作成功/失败提示
### 5. 用户界面
- ✅ 直观的分割窗口布局
- ✅ 左侧文件列表
- ✅ 右侧编辑区域
- ✅ 顶部服务器配置栏
- ✅ 底部操作按钮
- ✅ 状态栏显示
- ✅ 错误提示对话框
- ✅ 成功提示对话框
## 技术特性
### API 集成
- ✅ GET /list - 获取文件列表
- ✅ GET /{filename}/get - 下载文件内容
- ✅ POST /{filename}/post - 上传文件内容
- ✅ 请求超时处理10秒
- ✅ 错误处理和异常捕获
### 测试覆盖
- ✅ 窗口初始化测试
- ✅ UI 组件测试
- ✅ 服务器配置测试
- ✅ 文件操作测试
- ✅ 边界条件测试
- ✅ Mock 服务器响应测试
### 用户体验
- ✅ 自动依赖检查
- ✅ 一键启动脚本
- ✅ 一键测试脚本
- ✅ 中文本地化
- ✅ 友好的错误提示
- ✅ 实时状态更新
## 依赖项
- PySide6 6.6.0 - Qt6 GUI 框架
- requests 2.31.0 - HTTP 客户端
- pytest 7.4.3 - 测试框架
- pytest-qt 4.2.0 - Qt 应用测试
## 项目文件
- main_window.py - 主应用程序
- api_client.py - API 客户端
- test_app.py - 测试套件
- requirements.txt - 依赖列表
- run.bat - 启动脚本
- run_tests.bat - 测试脚本
- README.md - 使用文档
- FEATURES.md - 功能列表(本文件)

96
README.md Normal file
View File

@@ -0,0 +1,96 @@
# 知识库管理器
基于 PySide6 的知识库管理应用程序,用于连接 FastAPI 知识库服务器,查看、编辑和提交 Markdown 格式的知识库文件。
## 功能特性
- 服务器地址配置和连接测试
- 获取服务器上的文件列表(仅显示 .md 文件)
- 查看和编辑 Markdown 文件内容
- 保存修改到服务器
- 提交新的知识库文件
- 直观的用户界面,支持分割窗口
## 安装依赖
```bash
pip install -r requirements.txt
```
## 运行应用程序
### Windows 用户
双击 `run.bat` 文件启动应用程序。
### 命令行运行
```bash
python main_window.py
```
## 运行测试
### Windows 用户
双击 `run_tests.bat` 文件运行测试。
### 命令行运行
```bash
pytest test_app.py -v
```
## API 端点
应用程序使用以下 API 端点:
- `GET /list` - 获取文件列表
- `GET /{filename}/get` - 下载文件内容
- `POST /{filename}/post` - 上传文件内容
## 使用说明
1. **配置服务器**
- 在"服务器地址"输入框中输入服务器地址(例如:`http://43.134.1.17:8800/`
- 点击"配置"按钮连接服务器
2. **查看文件列表**
- 配置成功后,点击"刷新列表"按钮
- 文件列表将显示所有 .md 文件
3. **编辑文件**
- 在左侧文件列表中点击要编辑的文件
- 文件内容将显示在右侧编辑器中
- 编辑完成后点击"保存到服务器"
4. **提交新文件**
- 在"文件名"输入框中输入新文件名(自动添加 .md 扩展名)
- 在编辑器中输入文件内容
- 点击"提交新文件"按钮
## 文件结构
```
.
├── main_window.py # 主应用程序窗口
├── api_client.py # API 客户端模块
├── test_app.py # pytest-qt 测试用例
├── requirements.txt # Python 依赖
├── run.bat # Windows 启动脚本
├── run_tests.bat # Windows 测试脚本
└── README.md # 本文件
```
## 技术栈
- **UI 框架**: PySide6 (Qt6 的官方 Python 绑定)
- **HTTP 客户端**: requests
- **测试框架**: pytest, pytest-qt
## 注意事项
- 确保服务器地址正确且可访问
- 文件名会自动添加 .md 扩展名
- 网络操作有超时设置10秒
- 所有文件操作都有错误提示

43
api_client.py Normal file
View File

@@ -0,0 +1,43 @@
import requests
from typing import List, Optional
class KnowledgeBaseAPI:
def __init__(self, base_url: str):
self.base_url = base_url.rstrip('/')
def get_file_list(self) -> List[str]:
"""获取服务器上的文件列表"""
try:
response = requests.get(f"{self.base_url}/list", timeout=10)
response.raise_for_status()
return response.json()
except requests.RequestException as e:
raise Exception(f"获取文件列表失败: {str(e)}")
def get_file(self, filename: str) -> str:
"""获取文件内容"""
try:
response = requests.get(f"{self.base_url}/{filename}/get", timeout=10)
response.raise_for_status()
return response.text
except requests.RequestException as e:
raise Exception(f"获取文件 {filename} 失败: {str(e)}")
def post_file(self, filename: str, content: str) -> bool:
"""上传文件内容"""
try:
files = {'file': (filename, content, 'text/markdown')}
response = requests.post(f"{self.base_url}/{filename}/post", files=files, timeout=10)
response.raise_for_status()
return True
except requests.RequestException as e:
raise Exception(f"上传文件 {filename} 失败: {str(e)}")
def test_connection(self) -> bool:
"""测试服务器连接"""
try:
response = requests.get(f"{self.base_url}/list", timeout=5)
return response.status_code == 200
except requests.RequestException:
return False

2
fastapi的内容.txt Normal file
View File

@@ -0,0 +1,2 @@
{"service":"知识库API服务","version":"1.0.0","endpoints":[{"method":"GET","path":"/list","description":"获取文件列表"},{"method":"GET","path":"/{filename}/get","description":"下载文件"},{"method":"POST","path":"/{filename}/post","description":"上传文件"}],"knowledge_base_path":"/root/clawd/knowledge_base","port":8800,"usage_examples":{"list_files":"curl -X GET http://[服务器IP]:8800/list","download_file":"curl -X GET http://[服务器IP]:8800/example.txt/get -o example.txt","upload_file":"curl -X POST -F 'file=@local_file.txt' http://[服务器IP]:8800/remote_name.txt/post"}}

203
main_window.py Normal file
View File

@@ -0,0 +1,203 @@
import sys
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QListWidget, QTextEdit,
QMessageBox, QSplitter, QFileDialog
)
from PySide6.QtCore import Qt
from api_client import KnowledgeBaseAPI
class KnowledgeBaseApp(QMainWindow):
def __init__(self):
super().__init__()
self.api: Optional[KnowledgeBaseAPI] = None
self.current_filename: Optional[str] = None
self.init_ui()
def init_ui(self):
self.setWindowTitle('知识库管理器')
self.setGeometry(100, 100, 1200, 800)
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
# 服务器地址配置区域
server_layout = QHBoxLayout()
server_label = QLabel('服务器地址:')
self.server_input = QLineEdit('http://43.134.1.17:8800/')
self.server_input.setMinimumWidth(300)
self.config_button = QPushButton('配置')
self.config_button.clicked.connect(self.configure_server)
self.refresh_button = QPushButton('刷新列表')
self.refresh_button.clicked.connect(self.refresh_file_list)
server_layout.addWidget(server_label)
server_layout.addWidget(self.server_input)
server_layout.addWidget(self.config_button)
server_layout.addWidget(self.refresh_button)
server_layout.addStretch()
main_layout.addLayout(server_layout)
# 主内容区域(使用分割器)
splitter = QSplitter(Qt.Horizontal)
# 左侧:文件列表
left_widget = QWidget()
left_layout = QVBoxLayout(left_widget)
list_label = QLabel('文件列表 (.md):')
self.file_list = QListWidget()
self.file_list.itemClicked.connect(self.load_file)
left_layout.addWidget(list_label)
left_layout.addWidget(self.file_list)
# 右侧:编辑区域
right_widget = QWidget()
right_layout = QVBoxLayout(right_widget)
filename_layout = QHBoxLayout()
filename_label = QLabel('文件名:')
self.filename_input = QLineEdit()
self.filename_input.setPlaceholderText('输入文件名例如example.md')
filename_layout.addWidget(filename_label)
filename_layout.addWidget(self.filename_input)
self.text_editor = QTextEdit()
self.text_editor.setPlaceholderText('在此编辑 Markdown 内容...')
button_layout = QHBoxLayout()
self.save_button = QPushButton('保存到服务器')
self.save_button.clicked.connect(self.save_file)
self.post_button = QPushButton('提交新文件')
self.post_button.clicked.connect(self.post_new_file)
button_layout.addWidget(self.save_button)
button_layout.addWidget(self.post_button)
right_layout.addLayout(filename_layout)
right_layout.addWidget(self.text_editor)
right_layout.addLayout(button_layout)
splitter.addWidget(left_widget)
splitter.addWidget(right_widget)
splitter.setSizes([300, 900])
main_layout.addWidget(splitter)
# 状态栏
self.status_label = QLabel('就绪')
self.statusBar().addWidget(self.status_label)
def configure_server(self):
server_url = self.server_input.text().strip()
if not server_url:
QMessageBox.warning(self, '警告', '请输入服务器地址')
return
self.api = KnowledgeBaseAPI(server_url)
if self.api.test_connection():
self.status_label.setText(f'已连接到服务器: {server_url}')
QMessageBox.information(self, '成功', '服务器配置成功!')
self.refresh_file_list()
else:
QMessageBox.critical(self, '错误', '无法连接到服务器,请检查地址是否正确')
self.status_label.setText('连接失败')
def refresh_file_list(self):
if not self.api:
QMessageBox.warning(self, '警告', '请先配置服务器')
return
try:
files = self.api.get_file_list()
self.file_list.clear()
# 只显示 .md 文件
md_files = [f for f in files if f.endswith('.md')]
self.file_list.addItems(md_files)
self.status_label.setText(f'已加载 {len(md_files)} 个文件')
except Exception as e:
QMessageBox.critical(self, '错误', str(e))
self.status_label.setText('加载文件列表失败')
def load_file(self, item):
filename = item.text()
self.current_filename = filename
self.filename_input.setText(filename)
try:
content = self.api.get_file(filename)
self.text_editor.setPlainText(content)
self.status_label.setText(f'已加载文件: {filename}')
except Exception as e:
QMessageBox.critical(self, '错误', str(e))
self.status_label.setText('加载文件失败')
def save_file(self):
if not self.api:
QMessageBox.warning(self, '警告', '请先配置服务器')
return
if not self.current_filename:
QMessageBox.warning(self, '警告', '请先选择一个文件')
return
content = self.text_editor.toPlainText()
try:
self.api.post_file(self.current_filename, content)
QMessageBox.information(self, '成功', f'文件 {self.current_filename} 保存成功!')
self.status_label.setText(f'已保存文件: {self.current_filename}')
except Exception as e:
QMessageBox.critical(self, '错误', str(e))
self.status_label.setText('保存文件失败')
def post_new_file(self):
if not self.api:
QMessageBox.warning(self, '警告', '请先配置服务器')
return
filename = self.filename_input.text().strip()
if not filename:
QMessageBox.warning(self, '警告', '请输入文件名')
return
if not filename.endswith('.md'):
filename += '.md'
self.filename_input.setText(filename)
content = self.text_editor.toPlainText()
if not content:
QMessageBox.warning(self, '警告', '请输入文件内容')
return
try:
self.api.post_file(filename, content)
QMessageBox.information(self, '成功', f'文件 {filename} 提交成功!')
self.current_filename = filename
self.status_label.setText(f'已提交文件: {filename}')
self.refresh_file_list()
except Exception as e:
QMessageBox.critical(self, '错误', str(e))
self.status_label.setText('提交文件失败')
def main():
app = QApplication(sys.argv)
window = KnowledgeBaseApp()
window.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()

4
requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
PySide6==6.6.0
requests==2.31.0
pytest-qt==4.2.0
pytest==7.4.3

43
run.bat Normal file
View File

@@ -0,0 +1,43 @@
@echo off
chcp 65001 > nul
echo ================================================
echo 知识库管理器 - 启动脚本
echo ================================================
echo.
REM 检查 Python 是否安装
python --version >nul 2>&1
if %errorlevel% neq 0 (
echo [错误] 未找到 Python请先安装 Python 3.8 或更高版本
pause
exit /b 1
)
echo [1/3] 检查依赖...
pip show PySide6 >nul 2>&1
if %errorlevel% neq 0 (
echo [提示] 未安装依赖,正在安装...
pip install -r requirements.txt
if %errorlevel% neq 0 (
echo [错误] 依赖安装失败
pause
exit /b 1
)
) else (
echo [成功] 依赖已安装
)
echo.
echo [2/3] 启动应用程序...
python main_window.py
if %errorlevel% neq 0 (
echo.
echo [错误] 应用程序启动失败
pause
exit /b 1
)
echo.
echo [成功] 应用程序已关闭
pause

43
run_tests.bat Normal file
View File

@@ -0,0 +1,43 @@
@echo off
chcp 65001 > nul
echo ================================================
echo 知识库管理器 - 测试脚本
echo ================================================
echo.
REM 检查 Python 是否安装
python --version >nul 2>&1
if %errorlevel% neq 0 (
echo [错误] 未找到 Python请先安装 Python 3.8 或更高版本
pause
exit /b 1
)
echo [1/3] 检查依赖...
pip show pytest-qt >nul 2>&1
if %errorlevel% neq 0 (
echo [提示] 未安装依赖,正在安装...
pip install -r requirements.txt
if %errorlevel% neq 0 (
echo [错误] 依赖安装失败
pause
exit /b 1
)
) else (
echo [成功] 依赖已安装
)
echo.
echo [2/3] 运行测试...
pytest test_app.py -v
if %errorlevel% neq 0 (
echo.
echo [警告] 部分测试失败
) else (
echo.
echo [成功] 所有测试通过
)
echo.
pause

180
test_app.py Normal file
View File

@@ -0,0 +1,180 @@
import pytest
from PySide6.QtWidgets import QApplication, QMessageBox
from PySide6.QtCore import Qt
from unittest.mock import Mock, patch
from main_window import KnowledgeBaseApp
@pytest.fixture(scope="module")
def app():
"""创建 QApplication 实例"""
if not QApplication.instance():
app = QApplication([])
else:
app = QApplication.instance()
yield app
@pytest.fixture
def main_window(app, qtbot):
"""创建主窗口实例"""
window = KnowledgeBaseApp()
qtbot.addWidget(window)
window.show()
return window
def test_window_initialization(main_window, qtbot):
"""测试窗口初始化"""
assert main_window.windowTitle() == '知识库管理器'
assert main_window.server_input.text() == 'http://43.134.1.17:8800/'
assert main_window.config_button.text() == '配置'
assert main_window.refresh_button.text() == '刷新列表'
assert main_window.save_button.text() == '保存到服务器'
assert main_window.post_button.text() == '提交新文件'
def test_server_input_exists(main_window, qtbot):
"""测试服务器地址输入框"""
assert main_window.server_input is not None
assert main_window.server_input.placeholderText() == ''
qtbot.keyClicks(main_window.server_input, 'http://test.com')
assert 'http://test.com' in main_window.server_input.text()
def test_filename_input_exists(main_window, qtbot):
"""测试文件名输入框"""
assert main_window.filename_input is not None
assert main_window.filename_input.placeholderText() == '输入文件名例如example.md'
qtbot.keyClicks(main_window.filename_input, 'test.md')
assert main_window.filename_input.text() == 'test.md'
def test_text_editor_exists(main_window, qtbot):
"""测试文本编辑器"""
assert main_window.text_editor is not None
test_content = '# Test Markdown\n\nThis is a test.'
main_window.text_editor.setPlainText(test_content)
assert main_window.text_editor.toPlainText() == test_content
def test_file_list_exists(main_window):
"""测试文件列表"""
assert main_window.file_list is not None
assert main_window.file_list.count() == 0
def test_buttons_clickable(main_window, qtbot):
"""测试按钮可点击"""
assert main_window.config_button.isEnabled()
assert main_window.refresh_button.isEnabled()
assert main_window.save_button.isEnabled()
assert main_window.post_button.isEnabled()
def test_configure_server_without_url(main_window, qtbot):
"""测试配置服务器无URL"""
main_window.server_input.clear()
with patch.object(QMessageBox, 'warning') as mock_warning:
main_window.configure_server()
mock_warning.assert_called_once()
assert main_window.api is None
def test_configure_server_invalid_url(main_window, qtbot):
"""测试配置服务器无效URL"""
main_window.server_input.setText('http://invalid-url-that-does-not-exist.com')
with patch.object(QMessageBox, 'critical') as mock_critical:
main_window.configure_server()
mock_critical.assert_called_once()
def test_refresh_file_list_without_api(main_window, qtbot):
"""测试刷新文件列表(未配置服务器)"""
main_window.api = None
with patch.object(QMessageBox, 'warning') as mock_warning:
main_window.refresh_file_list()
mock_warning.assert_called_once()
def test_save_file_without_api(main_window, qtbot):
"""测试保存文件(未配置服务器)"""
main_window.api = None
with patch.object(QMessageBox, 'warning') as mock_warning:
main_window.save_file()
mock_warning.assert_called_once()
def test_save_file_without_selection(main_window, qtbot):
"""测试保存文件(未选择文件)"""
main_window.api = Mock()
main_window.current_filename = None
with patch.object(QMessageBox, 'warning') as mock_warning:
main_window.save_file()
mock_warning.assert_called_once()
def test_post_new_file_without_api(main_window, qtbot):
"""测试提交新文件(未配置服务器)"""
main_window.api = None
with patch.object(QMessageBox, 'warning') as mock_warning:
main_window.post_new_file()
mock_warning.assert_called_once()
def test_post_new_file_without_filename(main_window, qtbot):
"""测试提交新文件(无文件名)"""
main_window.api = Mock()
main_window.filename_input.clear()
with patch.object(QMessageBox, 'warning') as mock_warning:
main_window.post_new_file()
mock_warning.assert_called_once()
def test_post_new_file_without_content(main_window, qtbot):
"""测试提交新文件(无内容)"""
main_window.api = Mock()
main_window.filename_input.setText('test.md')
main_window.text_editor.clear()
with patch.object(QMessageBox, 'warning') as mock_warning:
main_window.post_new_file()
mock_warning.assert_called_once()
def test_post_new_file_auto_add_extension(main_window, qtbot):
"""测试提交新文件(自动添加.md扩展名"""
main_window.api = Mock()
main_window.filename_input.setText('test')
main_window.text_editor.setPlainText('# Test')
with patch.object(QMessageBox, 'information') as mock_info:
with patch.object(main_window, 'refresh_file_list'):
main_window.post_new_file()
assert main_window.filename_input.text() == 'test.md'
def test_ui_layout_elements(main_window):
"""测试UI布局元素"""
assert main_window.server_input.parent() is not None
assert main_window.file_list.parent() is not None
assert main_window.text_editor.parent() is not None
assert main_window.status_label.parent() is not None
def test_status_label_updates(main_window, qtbot):
"""测试状态标签更新"""
initial_text = main_window.status_label.text()
main_window.status_label.setText('测试状态')
assert main_window.status_label.text() == '测试状态'
if __name__ == '__main__':
pytest.main([__file__, '-v'])

1
地址.txt Normal file
View File

@@ -0,0 +1 @@
http://43.134.1.17:8800/