f1e0f6b5ef
- Add complete server infrastructure with Gitea, API, PostgreSQL - Implement Obsidian-compatible markdown mindmap converter - Add Flutter service for server synchronization - Configure GitHub Actions for automated build and deploy - Add production docker-compose with health checks - Include VPS setup and deployment scripts - Add comprehensive deployment documentation
530 lines
18 KiB
Dart
530 lines
18 KiB
Dart
import 'dart:convert';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
import '../models/task_model.dart';
|
|
|
|
class DeepSeekService {
|
|
final String? apiKey;
|
|
final String baseUrl = 'https://api.deepseek.com';
|
|
|
|
// 🎯 РЕЖИМ MOCK - установите true для работы без API запросов
|
|
// Когда пополните баланс DeepSeek, установите false
|
|
final bool useMockMode = true;
|
|
|
|
DeepSeekService({this.apiKey});
|
|
|
|
// AI чат для mind maps
|
|
Future<Map<String, dynamic>> chatCompletion({
|
|
required List<Map<String, dynamic>> messages,
|
|
}) async {
|
|
if (useMockMode || apiKey == null || apiKey!.isEmpty) {
|
|
return _mockChatResponse(messages);
|
|
}
|
|
|
|
try {
|
|
final response = await http.post(
|
|
Uri.parse('$baseUrl/v1/chat/completions'),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer $apiKey',
|
|
},
|
|
body: jsonEncode({
|
|
'model': 'deepseek-chat',
|
|
'messages': messages,
|
|
'temperature': 0.7,
|
|
'max_tokens': 2000,
|
|
}),
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
final data = jsonDecode(response.body);
|
|
final content = data['choices'][0]['message']['content'];
|
|
|
|
// Пытаемся распарсить как JSON
|
|
final jsonMatch = RegExp(r'\{.*\}', dotAll: true).firstMatch(content);
|
|
if (jsonMatch != null) {
|
|
return jsonDecode(jsonMatch.group(0)!) as Map<String, dynamic>;
|
|
}
|
|
|
|
return {'text': content};
|
|
} else {
|
|
print('DeepSeek API Error: ${response.statusCode} - ${response.body}');
|
|
}
|
|
} catch (e) {
|
|
print('DeepSeek Service Error: $e');
|
|
}
|
|
|
|
return _mockChatResponse(messages);
|
|
}
|
|
|
|
// Mock ответ для чата
|
|
Map<String, dynamic> _mockChatResponse(List<Map<String, dynamic>> messages) {
|
|
final lastMessage = messages.lastWhere(
|
|
(m) => m['role'] == 'user',
|
|
orElse: () => {'content': ''},
|
|
);
|
|
final content = lastMessage['content'] as String;
|
|
|
|
// Простая логика для демонстрации
|
|
if (content.toLowerCase().contains('добавь') ||
|
|
content.toLowerCase().contains('add')) {
|
|
return {
|
|
'ACTION': 'add',
|
|
'TARGET': null,
|
|
'CHANGES': [
|
|
{
|
|
'action': 'add',
|
|
'title': 'AI Предложение: $content',
|
|
'level': 0,
|
|
'parentId': null
|
|
},
|
|
],
|
|
'EXPLANATION': 'Я добавил новый узел на основе вашего запроса',
|
|
};
|
|
}
|
|
|
|
return {
|
|
'text':
|
|
'Понял ваш запрос: "$content". В реальном режиме я предложу изменения для mind map.',
|
|
};
|
|
}
|
|
|
|
// Генерация подзадач из основной задачи
|
|
Future<List<Map<String, dynamic>>> generateSubtasks({
|
|
required String title,
|
|
required String description,
|
|
}) async {
|
|
// Возвращаем mock данные пока нет баланса на DeepSeek
|
|
if (useMockMode || apiKey == null || apiKey!.isEmpty) {
|
|
return _mockGenerateSubtasks(title, description);
|
|
}
|
|
|
|
try {
|
|
final response = await http.post(
|
|
Uri.parse('$baseUrl/v1/chat/completions'),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer $apiKey',
|
|
},
|
|
body: jsonEncode({
|
|
'model': 'deepseek-chat',
|
|
'messages': [
|
|
{
|
|
'role': 'system',
|
|
'content':
|
|
'You are a task planning assistant. Break down tasks into logical subtasks. Return ONLY a JSON array.',
|
|
},
|
|
{
|
|
'role': 'user',
|
|
'content':
|
|
'Break down this task into 3-7 subtasks:\n\nTitle: $title\nDescription: $description\n\nReturn ONLY a JSON array with format: [{"title": "...", "estimatedHours": 0, "description": "..."}]',
|
|
},
|
|
],
|
|
'temperature': 0.7,
|
|
'max_tokens': 2000,
|
|
}),
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
final data = jsonDecode(response.body);
|
|
final content = data['choices'][0]['message']['content'];
|
|
|
|
// Извлекаем JSON из ответа
|
|
final jsonMatch = RegExp(r'\[.*\]', dotAll: true).firstMatch(content);
|
|
if (jsonMatch != null) {
|
|
final List<dynamic> subtasks = jsonDecode(jsonMatch.group(0)!);
|
|
return subtasks.cast<Map<String, dynamic>>();
|
|
}
|
|
} else {
|
|
print('DeepSeek API Error: ${response.statusCode} - ${response.body}');
|
|
print('⚠️ Используется mock режим как fallback');
|
|
}
|
|
} catch (e) {
|
|
print('DeepSeek Service Error: $e');
|
|
print('⚠️ Используется mock режим как fallback');
|
|
}
|
|
|
|
return _mockGenerateSubtasks(title, description);
|
|
}
|
|
|
|
// AI приоритизация задач
|
|
Future<TaskPriority> prioritizeTask({
|
|
required String title,
|
|
required String description,
|
|
DateTime? dueDate,
|
|
}) async {
|
|
// Возвращаем mock данные пока нет баланса на DeepSeek
|
|
if (useMockMode || apiKey == null || apiKey!.isEmpty) {
|
|
return _mockPrioritizeTask(dueDate);
|
|
}
|
|
|
|
try {
|
|
final response = await http.post(
|
|
Uri.parse('$baseUrl/v1/chat/completions'),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer $apiKey',
|
|
},
|
|
body: jsonEncode({
|
|
'model': 'deepseek-chat',
|
|
'messages': [
|
|
{
|
|
'role': 'system',
|
|
'content':
|
|
'You are a task prioritization assistant. Determine task priority: low, medium, high, or urgent. Reply with ONE word only.',
|
|
},
|
|
{
|
|
'role': 'user',
|
|
'content':
|
|
'What priority should this task have?\n\nTitle: $title\nDescription: $description\nDue: ${dueDate ?? "Not specified"}\n\nReply with ONE word: low, medium, high, or urgent',
|
|
},
|
|
],
|
|
'temperature': 0.3,
|
|
'max_tokens': 10,
|
|
}),
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
final data = jsonDecode(response.body);
|
|
final content =
|
|
data['choices'][0]['message']['content'].trim().toLowerCase();
|
|
|
|
switch (content) {
|
|
case 'urgent':
|
|
return TaskPriority.urgent;
|
|
case 'high':
|
|
return TaskPriority.high;
|
|
case 'low':
|
|
return TaskPriority.low;
|
|
default:
|
|
return TaskPriority.medium;
|
|
}
|
|
} else {
|
|
print('⚠️ Используется mock режим как fallback');
|
|
}
|
|
} catch (e) {
|
|
print('DeepSeek Service Error: $e');
|
|
print('⚠️ Используется mock режим как fallback');
|
|
}
|
|
|
|
return _mockPrioritizeTask(dueDate);
|
|
}
|
|
|
|
// Генерация описания задачи
|
|
Future<String> generateTaskDescription({
|
|
required String title,
|
|
String? context,
|
|
}) async {
|
|
// Возвращаем mock данные пока нет баланса на DeepSeek
|
|
if (useMockMode || apiKey == null || apiKey!.isEmpty) {
|
|
return _mockGenerateDescription(title, context);
|
|
}
|
|
|
|
try {
|
|
final response = await http.post(
|
|
Uri.parse('$baseUrl/v1/chat/completions'),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer $apiKey',
|
|
},
|
|
body: jsonEncode({
|
|
'model': 'deepseek-chat',
|
|
'messages': [
|
|
{
|
|
'role': 'system',
|
|
'content':
|
|
'You are a task description writer. Write clear, concise task descriptions in Russian. Keep it under 100 words.',
|
|
},
|
|
{
|
|
'role': 'user',
|
|
'content':
|
|
'Write a brief description for this task:\n\nTitle: $title\nContext: ${context ?? "No additional context"}\n\nWrite in Russian.',
|
|
},
|
|
],
|
|
'temperature': 0.5,
|
|
'max_tokens': 500,
|
|
}),
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
final data = jsonDecode(response.body);
|
|
return data['choices'][0]['message']['content'];
|
|
} else {
|
|
print('⚠️ Используется mock режим как fallback');
|
|
}
|
|
} catch (e) {
|
|
print('DeepSeek Service Error: $e');
|
|
print('⚠️ Используется mock режим как fallback');
|
|
}
|
|
|
|
return _mockGenerateDescription(title, context);
|
|
}
|
|
|
|
// Умное планирование - распределение задач по времени
|
|
Future<List<Map<String, dynamic>>> smartSchedule({
|
|
required List<TaskModel> tasks,
|
|
required DateTime startDate,
|
|
required DateTime endDate,
|
|
required int availableHoursPerDay,
|
|
}) async {
|
|
// Возвращаем mock данные пока нет баланса на DeepSeek
|
|
if (useMockMode || apiKey == null || apiKey!.isEmpty) {
|
|
return _mockSchedule(tasks, startDate, endDate, availableHoursPerDay);
|
|
}
|
|
|
|
try {
|
|
final tasksJson = tasks
|
|
.map((t) => {
|
|
'title': t.title,
|
|
'estimatedHours': t.estimatedHours,
|
|
'priority': t.priority.name,
|
|
'dueDate': t.dueDate?.toIso8601String(),
|
|
})
|
|
.toList();
|
|
|
|
final response = await http.post(
|
|
Uri.parse('$baseUrl/v1/chat/completions'),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer $apiKey',
|
|
},
|
|
body: jsonEncode({
|
|
'model': 'deepseek-chat',
|
|
'messages': [
|
|
{
|
|
'role': 'system',
|
|
'content':
|
|
'You are a smart scheduling assistant. Distribute tasks across available days optimally. Return ONLY a JSON array.',
|
|
},
|
|
{
|
|
'role': 'user',
|
|
'content': '''
|
|
Schedule these tasks optimally:
|
|
${jsonEncode(tasksJson)}
|
|
|
|
Available hours per day: $availableHoursPerDay
|
|
Start date: ${startDate.toIso8601String()}
|
|
End date: ${endDate.toIso8601String()}
|
|
|
|
Return JSON array: [{"taskId": index, "date": "YYYY-MM-DD", "hours": 0}]
|
|
''',
|
|
},
|
|
],
|
|
'temperature': 0.5,
|
|
'max_tokens': 3000,
|
|
}),
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
final data = jsonDecode(response.body);
|
|
final content = data['choices'][0]['message']['content'];
|
|
|
|
final jsonMatch = RegExp(r'\[.*\]', dotAll: true).firstMatch(content);
|
|
if (jsonMatch != null) {
|
|
final List<dynamic> schedule = jsonDecode(jsonMatch.group(0)!);
|
|
return schedule.map((s) {
|
|
return {
|
|
'taskId': s['taskId'],
|
|
'date': DateTime.parse(s['date']),
|
|
'hours': s['hours'],
|
|
};
|
|
}).toList();
|
|
}
|
|
} else {
|
|
print('⚠️ Используется mock режим как fallback');
|
|
}
|
|
} catch (e) {
|
|
print('DeepSeek Service Error: $e');
|
|
print('⚠️ Используется mock режим как fallback');
|
|
}
|
|
|
|
return _mockSchedule(tasks, startDate, endDate, availableHoursPerDay);
|
|
}
|
|
|
|
// AI анализ проекта - генерация mind-карты
|
|
Future<List<Map<String, dynamic>>> generateMindMap({
|
|
required String projectName,
|
|
required String description,
|
|
}) async {
|
|
// Возвращаем mock данные пока нет баланса на DeepSeek
|
|
if (useMockMode || apiKey == null || apiKey!.isEmpty) {
|
|
return _mockMindMap(projectName);
|
|
}
|
|
|
|
try {
|
|
final response = await http.post(
|
|
Uri.parse('$baseUrl/v1/chat/completions'),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer $apiKey',
|
|
},
|
|
body: jsonEncode({
|
|
'model': 'deepseek-chat',
|
|
'messages': [
|
|
{
|
|
'role': 'system',
|
|
'content':
|
|
'You are a project structure assistant. Create a mind-map structure for projects. Return ONLY a JSON array of nodes.',
|
|
},
|
|
{
|
|
'role': 'user',
|
|
'content': '''
|
|
Create a mind-map structure for this project:
|
|
|
|
Project: $projectName
|
|
Description: $description
|
|
|
|
Return JSON array of nodes with format:
|
|
[{"level": 0, "title": "Project Name", "parentId": null}, {"level": 1, "title": "Phase 1", "parentId": 0}, ...]
|
|
''',
|
|
},
|
|
],
|
|
'temperature': 0.7,
|
|
'max_tokens': 3000,
|
|
}),
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
final data = jsonDecode(response.body);
|
|
final content = data['choices'][0]['message']['content'];
|
|
|
|
final jsonMatch = RegExp(r'\[.*\]', dotAll: true).firstMatch(content);
|
|
if (jsonMatch != null) {
|
|
final List<dynamic> nodes = jsonDecode(jsonMatch.group(0)!);
|
|
return nodes.cast<Map<String, dynamic>>();
|
|
}
|
|
} else {
|
|
print('⚠️ Используется mock режим как fallback');
|
|
}
|
|
} catch (e) {
|
|
print('DeepSeek Service Error: $e');
|
|
print('⚠️ Используется mock режим как fallback');
|
|
}
|
|
|
|
return _mockMindMap(projectName);
|
|
}
|
|
|
|
// Mock данные для тестирования
|
|
List<Map<String, dynamic>> _mockGenerateSubtasks(
|
|
String title, String description) {
|
|
return [
|
|
{
|
|
'title': 'Анализ требований',
|
|
'estimatedHours': 2,
|
|
'description': 'Изучить требования и документацию'
|
|
},
|
|
{
|
|
'title': 'Проектирование',
|
|
'estimatedHours': 3,
|
|
'description': 'Спроектировать архитектуру решения'
|
|
},
|
|
{
|
|
'title': 'Реализация',
|
|
'estimatedHours': 8,
|
|
'description': 'Написать код'
|
|
},
|
|
{
|
|
'title': 'Тестирование',
|
|
'estimatedHours': 3,
|
|
'description': 'Протестировать функциональность'
|
|
},
|
|
{
|
|
'title': 'Документирование',
|
|
'estimatedHours': 2,
|
|
'description': 'Создать документацию'
|
|
},
|
|
];
|
|
}
|
|
|
|
TaskPriority _mockPrioritizeTask(DateTime? dueDate) {
|
|
if (dueDate == null) return TaskPriority.medium;
|
|
|
|
final daysUntilDue = dueDate.difference(DateTime.now()).inDays;
|
|
|
|
if (daysUntilDue <= 1) return TaskPriority.urgent;
|
|
if (daysUntilDue <= 3) return TaskPriority.high;
|
|
if (daysUntilDue <= 7) return TaskPriority.medium;
|
|
return TaskPriority.low;
|
|
}
|
|
|
|
String _mockGenerateDescription(String title, String? context) {
|
|
final descriptions = {
|
|
'Анализ требований':
|
|
'Изучить и проанализировать все требования проекта. Подготовить документацию с детальным описанием функционала и ожиданий пользователей.',
|
|
'Проектирование':
|
|
'Разработать архитектуру решения, создать схемы базы данных и определить основные компоненты системы.',
|
|
'Реализация':
|
|
'Написать чистый, поддерживаемый код согласно лучшим практикам. Реализовать весь запланированный функционал.',
|
|
'Тестирование':
|
|
'Провести комплексное тестирование: модульные, интеграционные и end-to-end тесты для обеспечения качества.',
|
|
'Деплой':
|
|
'Подготовить инфраструктуру, настроить CI/CD пайплайн и развернуть приложение на production сервере.',
|
|
};
|
|
|
|
return descriptions[title] ??
|
|
'Задача: $title. ${context ?? "Выполнить в срок с надлежащим качеством."}';
|
|
}
|
|
|
|
List<Map<String, dynamic>> _mockSchedule(
|
|
List<TaskModel> tasks,
|
|
DateTime startDate,
|
|
DateTime endDate,
|
|
int availableHoursPerDay,
|
|
) {
|
|
final schedule = <Map<String, dynamic>>[];
|
|
var currentDate = startDate;
|
|
var remainingHours = availableHoursPerDay;
|
|
|
|
for (var i = 0; i < tasks.length; i++) {
|
|
if (currentDate.isAfter(endDate)) break;
|
|
|
|
final task = tasks[i];
|
|
if (task.estimatedHours <= remainingHours) {
|
|
schedule.add({
|
|
'taskId': i,
|
|
'date': currentDate,
|
|
'hours': task.estimatedHours,
|
|
});
|
|
remainingHours -= task.estimatedHours;
|
|
} else {
|
|
currentDate = currentDate.add(const Duration(days: 1));
|
|
remainingHours = availableHoursPerDay;
|
|
|
|
if (currentDate.isAfter(endDate)) break;
|
|
|
|
schedule.add({
|
|
'taskId': i,
|
|
'date': currentDate,
|
|
'hours': task.estimatedHours > remainingHours
|
|
? remainingHours
|
|
: task.estimatedHours,
|
|
});
|
|
}
|
|
}
|
|
|
|
return schedule;
|
|
}
|
|
|
|
List<Map<String, dynamic>> _mockMindMap(String projectName) {
|
|
return [
|
|
{'level': 0, 'title': projectName, 'parentId': null},
|
|
{'level': 1, 'title': 'Планирование', 'parentId': 0},
|
|
{'level': 1, 'title': 'Разработка', 'parentId': 0},
|
|
{'level': 1, 'title': 'Тестирование', 'parentId': 0},
|
|
{'level': 1, 'title': 'Деплой', 'parentId': 0},
|
|
{'level': 2, 'title': 'Анализ', 'parentId': 1},
|
|
{'level': 2, 'title': 'Дизайн', 'parentId': 1},
|
|
{'level': 2, 'title': 'Frontend', 'parentId': 2},
|
|
{'level': 2, 'title': 'Backend', 'parentId': 2},
|
|
{'level': 2, 'title': 'Unit тесты', 'parentId': 3},
|
|
{'level': 2, 'title': 'E2E тесты', 'parentId': 3},
|
|
];
|
|
}
|
|
}
|
|
|
|
// Глобальный экземпляр сервиса
|
|
final deepSeekService = DeepSeekService(
|
|
// API ключ из .env файла
|
|
apiKey: dotenv.env['DEEPSEEK_API_KEY'],
|
|
);
|