{"repo": "chaiqueeee/mathmate", "files": {"lib/services/deepseek_service.dart": {"cd430dbed71b344a768a697fd5da313464a413f6": {"t": 1785745912.7021139, "n": 3615, "h": "a009ee3366c9a8b5", "c": "import 'dart:convert';\n\nimport 'package:flutter_dotenv/flutter_dotenv.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:mathmate/services/app_logger.dart';\n\nclass DeepSeekService {\n static const String _apiKeyEnv = 'DEEPSEEK_API_KEY';\n static const String _modelIdEnv = 'DEEPSEEK_MODEL_ID';\n static const String _baseUrlEnv = 'DEEPSEEK_BASE_URL';\n static const String _defaultBaseUrl = 'https://api.deepseek.com/chat/completions';\n\n static bool _dotenvLoaded = false;\n\n Future _ensureEnvLoaded() async {\n if (_dotenvLoaded) return;\n await dotenv.load(fileName: '.env');\n _dotenvLoaded = true;\n }\n\n Future callTextPrompt({\n required String prompt,\n required String userText,\n }) async {\n await _ensureEnvLoaded();\n\n final String apiKey = (dotenv.env[_apiKeyEnv] ?? '').trim();\n final String modelId = (dotenv.env[_modelIdEnv] ?? '').trim();\n final String baseUrl = (dotenv.env[_baseUrlEnv] ?? _defaultBaseUrl).trim();\n\n AppLogger.instance.info('[DeepSeek] 请求模型: $modelId');\n AppLogger.instance.info('[DeepSeek] 请求端点: $baseUrl');\n AppLogger.instance.info('[DeepSeek] system prompt 长度: ${prompt.length} 字符');\n AppLogger.instance.info('[DeepSeek] user text 长度: ${userText.length} 字符');\n if (userText.length <= 300) {\n AppLogger.instance.info('[DeepSeek] user text 内容: $userText');\n } else {\n AppLogger.instance.info('[DeepSeek] user text 预览(前300字): ${userText.substring(0, 300)}...');\n }\n\n if (apiKey.isEmpty) {\n throw Exception('Missing env config: DEEPSEEK_API_KEY');\n }\n if (modelId.isEmpty) {\n throw Exception('Missing env config: DEEPSEEK_MODEL_ID');\n }\n\n final Map headers = {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer $apiKey',\n };\n\n final List> messages = >[\n {'role': 'system', 'content': prompt},\n {'role': 'user', 'content': userText},\n ];\n\n final Stopwatch sw = Stopwatch()..start();\n final http.Response response = await http.post(\n Uri.parse(baseUrl),\n headers: headers,\n body: jsonEncode({\n 'model': modelId,\n 'messages': messages,\n }),\n ).timeout(const Duration(seconds: 60), onTimeout: () {\n throw Exception('DeepSeek API 请求超时(60秒)');\n });\n sw.stop();\n AppLogger.instance.info('[DeepSeek] 响应状态: ${response.statusCode},耗时 ${sw.elapsedMilliseconds}ms');\n\n if (response.statusCode != 200) {\n final String detail = utf8.decode(response.bodyBytes);\n AppLogger.instance.error('[DeepSeek] API 错误响应体: $detail');\n throw Exception('DeepSeek API error: $detail');\n }\n\n final dynamic data = jsonDecode(utf8.decode(response.bodyBytes));\n final String parsed = _extractContentFromResponse(data).trim();\n AppLogger.instance.info('[DeepSeek] 提取内容长度: ${parsed.length} 字符');\n if (parsed.length <= 500) {\n AppLogger.instance.info('[DeepSeek] 响应内容: $parsed');\n } else {\n AppLogger.instance.info('[DeepSeek] 响应预览(前500字): ${parsed.substring(0, 500)}...');\n }\n\n if (parsed.isEmpty) {\n AppLogger.instance.warn('[DeepSeek] 返回空内容!原始响应: ${utf8.decode(response.bodyBytes)}');\n throw Exception('DeepSeek API returned empty content.');\n }\n return parsed;\n }\n\n String _extractContentFromResponse(dynamic data) {\n final dynamic chatContent = data['choices']?[0]?['message']?['content'];\n if (chatContent is String && chatContent.trim().isNotEmpty) {\n return chatContent.trim();\n }\n return '';\n }\n}\n"}, "94334c08701d284ffe513538298d40c09b964dd1": {"t": 1785757856.2831054, "n": 3615, "h": "a009ee3366c9a8b5", "c": "import 'dart:convert';\n\nimport 'package:flutter_dotenv/flutter_dotenv.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:mathmate/services/app_logger.dart';\n\nclass DeepSeekService {\n static const String _apiKeyEnv = 'DEEPSEEK_API_KEY';\n static const String _modelIdEnv = 'DEEPSEEK_MODEL_ID';\n static const String _baseUrlEnv = 'DEEPSEEK_BASE_URL';\n static const String _defaultBaseUrl = 'https://api.deepseek.com/chat/completions';\n\n static bool _dotenvLoaded = false;\n\n Future _ensureEnvLoaded() async {\n if (_dotenvLoaded) return;\n await dotenv.load(fileName: '.env');\n _dotenvLoaded = true;\n }\n\n Future callTextPrompt({\n required String prompt,\n required String userText,\n }) async {\n await _ensureEnvLoaded();\n\n final String apiKey = (dotenv.env[_apiKeyEnv] ?? '').trim();\n final String modelId = (dotenv.env[_modelIdEnv] ?? '').trim();\n final String baseUrl = (dotenv.env[_baseUrlEnv] ?? _defaultBaseUrl).trim();\n\n AppLogger.instance.info('[DeepSeek] 请求模型: $modelId');\n AppLogger.instance.info('[DeepSeek] 请求端点: $baseUrl');\n AppLogger.instance.info('[DeepSeek] system prompt 长度: ${prompt.length} 字符');\n AppLogger.instance.info('[DeepSeek] user text 长度: ${userText.length} 字符');\n if (userText.length <= 300) {\n AppLogger.instance.info('[DeepSeek] user text 内容: $userText');\n } else {\n AppLogger.instance.info('[DeepSeek] user text 预览(前300字): ${userText.substring(0, 300)}...');\n }\n\n if (apiKey.isEmpty) {\n throw Exception('Missing env config: DEEPSEEK_API_KEY');\n }\n if (modelId.isEmpty) {\n throw Exception('Missing env config: DEEPSEEK_MODEL_ID');\n }\n\n final Map headers = {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer $apiKey',\n };\n\n final List> messages = >[\n {'role': 'system', 'content': prompt},\n {'role': 'user', 'content': userText},\n ];\n\n final Stopwatch sw = Stopwatch()..start();\n final http.Response response = await http.post(\n Uri.parse(baseUrl),\n headers: headers,\n body: jsonEncode({\n 'model': modelId,\n 'messages': messages,\n }),\n ).timeout(const Duration(seconds: 60), onTimeout: () {\n throw Exception('DeepSeek API 请求超时(60秒)');\n });\n sw.stop();\n AppLogger.instance.info('[DeepSeek] 响应状态: ${response.statusCode},耗时 ${sw.elapsedMilliseconds}ms');\n\n if (response.statusCode != 200) {\n final String detail = utf8.decode(response.bodyBytes);\n AppLogger.instance.error('[DeepSeek] API 错误响应体: $detail');\n throw Exception('DeepSeek API error: $detail');\n }\n\n final dynamic data = jsonDecode(utf8.decode(response.bodyBytes));\n final String parsed = _extractContentFromResponse(data).trim();\n AppLogger.instance.info('[DeepSeek] 提取内容长度: ${parsed.length} 字符');\n if (parsed.length <= 500) {\n AppLogger.instance.info('[DeepSeek] 响应内容: $parsed');\n } else {\n AppLogger.instance.info('[DeepSeek] 响应预览(前500字): ${parsed.substring(0, 500)}...');\n }\n\n if (parsed.isEmpty) {\n AppLogger.instance.warn('[DeepSeek] 返回空内容!原始响应: ${utf8.decode(response.bodyBytes)}');\n throw Exception('DeepSeek API returned empty content.');\n }\n return parsed;\n }\n\n String _extractContentFromResponse(dynamic data) {\n final dynamic chatContent = data['choices']?[0]?['message']?['content'];\n if (chatContent is String && chatContent.trim().isNotEmpty) {\n return chatContent.trim();\n }\n return '';\n }\n}\n"}, "ff928563d170e0b958fbaff90b380f66c7473c8e": {"t": 1785757857.5896492, "n": 2563, "h": "b9d4678132d35982", "c": "import 'dart:convert';\n\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter_dotenv/flutter_dotenv.dart';\nimport 'package:http/http.dart' as http;\n\nclass DeepSeekService {\n static const String _apiKeyEnv = 'DEEPSEEK_API_KEY';\n static const String _modelIdEnv = 'DEEPSEEK_MODEL_ID';\n static const String _baseUrlEnv = 'DEEPSEEK_BASE_URL';\n static const String _defaultBaseUrl = 'https://api.deepseek.com/chat/completions';\n\n static bool _dotenvLoaded = false;\n\n Future _ensureEnvLoaded() async {\n if (_dotenvLoaded) return;\n await dotenv.load(fileName: '.env');\n _dotenvLoaded = true;\n }\n\n Future callTextPrompt({\n required String prompt,\n required String userText,\n }) async {\n await _ensureEnvLoaded();\n\n final String apiKey = (dotenv.env[_apiKeyEnv] ?? '').trim();\n final String modelId = (dotenv.env[_modelIdEnv] ?? '').trim();\n final String baseUrl = (dotenv.env[_baseUrlEnv] ?? _defaultBaseUrl).trim();\n\n if (apiKey.isEmpty) {\n throw Exception('Missing env config: DEEPSEEK_API_KEY');\n }\n if (modelId.isEmpty) {\n throw Exception('Missing env config: DEEPSEEK_MODEL_ID');\n }\n\n final Map headers = {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer $apiKey',\n };\n\n final List> messages = >[\n {'role': 'system', 'content': prompt},\n {'role': 'user', 'content': userText},\n ];\n\n final http.Response response = await http.post(\n Uri.parse(baseUrl),\n headers: headers,\n body: jsonEncode({\n 'model': modelId,\n 'messages': messages,\n }),\n ).timeout(const Duration(seconds: 60), onTimeout: () {\n throw Exception('DeepSeek API 请求超时(60秒)');\n });\n\n if (response.statusCode != 200) {\n final String detail = utf8.decode(response.bodyBytes);\n debugPrint('DeepSeek API error: $detail');\n throw Exception('DeepSeek API error: $detail');\n }\n\n final dynamic data = jsonDecode(utf8.decode(response.bodyBytes));\n final String parsed = _extractContentFromResponse(data).trim();\n if (parsed.isEmpty) {\n throw Exception('DeepSeek API returned empty content.');\n }\n return parsed;\n }\n\n String _extractContentFromResponse(dynamic data) {\n final dynamic chatContent = data['choices']?[0]?['message']?['content'];\n if (chatContent is String && chatContent.trim().isNotEmpty) {\n return chatContent.trim();\n }\n return '';\n }\n}\n"}, "4a914e11f67b80b9e472192e937822f0a6be4e62": {"t": 1785757858.921628, "n": 2454, "h": "c4a990b4d2d8ddd6", "c": "import 'dart:convert';\n\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter_dotenv/flutter_dotenv.dart';\nimport 'package:http/http.dart' as http;\n\nclass DeepSeekService {\n static const String _apiKeyEnv = 'DEEPSEEK_API_KEY';\n static const String _modelIdEnv = 'DEEPSEEK_MODEL_ID';\n static const String _baseUrlEnv = 'DEEPSEEK_BASE_URL';\n static const String _defaultBaseUrl = 'https://api.deepseek.com/chat/completions';\n\n static bool _dotenvLoaded = false;\n\n Future _ensureEnvLoaded() async {\n if (_dotenvLoaded) return;\n await dotenv.load(fileName: '.env');\n _dotenvLoaded = true;\n }\n\n Future callTextPrompt({\n required String prompt,\n required String userText,\n }) async {\n await _ensureEnvLoaded();\n\n final String apiKey = (dotenv.env[_apiKeyEnv] ?? '').trim();\n final String modelId = (dotenv.env[_modelIdEnv] ?? '').trim();\n final String baseUrl = (dotenv.env[_baseUrlEnv] ?? _defaultBaseUrl).trim();\n\n if (apiKey.isEmpty) {\n throw Exception('Missing env config: DEEPSEEK_API_KEY');\n }\n if (modelId.isEmpty) {\n throw Exception('Missing env config: DEEPSEEK_MODEL_ID');\n }\n\n final Map headers = {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer $apiKey',\n };\n\n final List> messages = >[\n {'role': 'system', 'content': prompt},\n {'role': 'user', 'content': userText},\n ];\n\n final http.Response response = await http.post(\n Uri.parse(baseUrl),\n headers: headers,\n body: jsonEncode({\n 'model': modelId,\n 'messages': messages,\n }),\n );\n\n if (response.statusCode != 200) {\n final String detail = utf8.decode(response.bodyBytes);\n debugPrint('DeepSeek API error: $detail');\n throw Exception('DeepSeek API error: $detail');\n }\n\n final dynamic data = jsonDecode(utf8.decode(response.bodyBytes));\n final String parsed = _extractContentFromResponse(data).trim();\n if (parsed.isEmpty) {\n throw Exception('DeepSeek API returned empty content.');\n }\n return parsed;\n }\n\n String _extractContentFromResponse(dynamic data) {\n final dynamic chatContent = data['choices']?[0]?['message']?['content'];\n if (chatContent is String && chatContent.trim().isNotEmpty) {\n return chatContent.trim();\n }\n return '';\n }\n}\n"}}, "lib/services/geogebra_agent_service.dart": {"cd430dbed71b344a768a697fd5da313464a413f6": {"t": 1785745918.7735226, "n": 8524, "h": "5db151fe9967cf49", "c": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter_dotenv/flutter_dotenv.dart';\nimport 'package:http/http.dart' as http;\n\nimport 'prompts/geogebra_agent_prompt.dart';\n\n/// Agent 流式响应块\nclass AgentStreamChunk {\n final String? textDelta; // 文本增量\n final String? toolCallName; // 工具调用名称(开始时)\n final String? toolCallArgs; // 工具调用参数(完整 JSON)\n final String? toolResult; // 工具执行结果描述\n final bool isDone;\n final String? error;\n\n AgentStreamChunk({\n this.textDelta,\n this.toolCallName,\n this.toolCallArgs,\n this.toolResult,\n this.isDone = false,\n this.error,\n });\n}\n\n/// 工具定义(发给 LLM)\nclass AgentTool {\n final String name;\n final String description;\n final Map parameters; // JSON Schema\n\n const AgentTool({required this.name, required this.description, required this.parameters});\n\n Map toOpenAITool() => {\n 'type': 'function',\n 'function': {\n 'name': name,\n 'description': description,\n 'parameters': parameters,\n },\n };\n}\n\n/// GeoGebra 7 个 MCP 工具定义\nconst List geogebraTools = [\n AgentTool(\n name: 'getCanvasContext',\n description: '获取当前 GeoGebra 画布的完整状态,包括所有对象和表达式',\n parameters: {\n 'type': 'object',\n 'properties': {},\n },\n ),\n AgentTool(\n name: 'executeGeoGebraCommand',\n description: '在 GeoGebra 画布中执行一条命令,返回执行结果',\n parameters: {\n 'type': 'object',\n 'properties': {\n 'command': {'type': 'string', 'description': '要执行的 GeoGebra 命令'},\n },\n 'required': ['command'],\n },\n ),\n AgentTool(\n name: 'deleteGeoGebraObject',\n description: '从 GeoGebra 画布中删除指定标签的对象',\n parameters: {\n 'type': 'object',\n 'properties': {\n 'label': {'type': 'string', 'description': '要删除的对象标签'},\n },\n 'required': ['label'],\n },\n ),\n AgentTool(\n name: 'setUndoPoint',\n description: '在 GeoGebra 画布中设置一个撤销点',\n parameters: {\n 'type': 'object',\n 'properties': {},\n },\n ),\n AgentTool(\n name: 'undo',\n description: '在 GeoGebra 画布中执行撤销操作',\n parameters: {\n 'type': 'object',\n 'properties': {},\n },\n ),\n AgentTool(\n name: 'setPerspective',\n description: \"切换 GeoGebra 视图模式:'G'=2D图形, 'T'=3D图形\",\n parameters: {\n 'type': 'object',\n 'properties': {\n 'mode': {'type': 'string', 'description': \"视图模式:'G' 或 'T'\"},\n },\n 'required': ['mode'],\n },\n ),\n AgentTool(\n name: 'getSelectedObjects',\n description: '获取用户当前在 GeoGebra 画布中选中的对象标签列表',\n parameters: {\n 'type': 'object',\n 'properties': {},\n },\n ),\n];\n\n/// GeoGebra Agent 服务。\n///\n/// 使用 DeepSeek API 的 function calling 能力,让 LLM 通过工具调用\n/// 操控 GeoGebra 画布。对外提供流式响应。\nclass GeogebraAgentService {\n final String _apiKeyEnv;\n final String _modelEnv;\n final String _baseUrlEnv;\n final String _defaultBaseUrl;\n final String _defaultModel;\n\n /// 工具执行回调 —— 由外部注入,桥接 JS Bridge。\n Future Function(String toolName, Map args)? onToolCall;\n\n GeogebraAgentService({\n String apiKeyEnv = 'DEEPSEEK_API_KEY',\n String modelEnv = 'DEEPSEEK_MODEL_ID',\n String baseUrlEnv = 'DEEPSEEK_BASE_URL',\n String defaultBaseUrl = 'https://api.deepseek.com/chat/completions',\n String defaultModel = 'deepseek-chat',\n }) : _apiKeyEnv = apiKeyEnv,\n _modelEnv = modelEnv,\n _baseUrlEnv = baseUrlEnv,\n _defaultBaseUrl = defaultBaseUrl,\n _defaultModel = defaultModel;\n\n static bool _dotenvLoaded = false;\n http.Client? _client;\n bool _cancelled = false;\n\n Future _ensureEnv() async {\n if (_dotenvLoaded) return;\n await dotenv.load(fileName: '.env');\n _dotenvLoaded = true;\n }\n\n void cancel() {\n _cancelled = true;\n _closeClient();\n }\n\n void _closeClient() {\n try { _client?.close(); } catch (_) {}\n _client = null;\n }\n\n /// 流式对话入口。\n /// [messages] 为历史对话,不含 system prompt(会自动添加)。\n /// 最多 [maxSteps] 轮工具调用。\n Stream chat({\n required List> messages,\n int maxSteps = 10,\n }) async* {\n await _ensureEnv();\n _cancelled = false;\n\n final apiKey = (dotenv.env[_apiKeyEnv] ?? '').trim();\n final model = (dotenv.env[_modelEnv] ?? _defaultModel).trim();\n final baseUrl = (dotenv.env[_baseUrlEnv] ?? _defaultBaseUrl).trim();\n\n if (apiKey.isEmpty) {\n yield AgentStreamChunk(error: '缺少 API Key: $_apiKeyEnv');\n return;\n }\n\n // 构建完整消息列表(含 system prompt)\n final fullMessages = >[\n {'role': 'system', 'content': geogebraAgentSystemPrompt},\n ...messages.map((m) => {'role': m['role'], 'content': m['content']}),\n ];\n\n int stepCount = 0;\n while (stepCount < maxSteps && !_cancelled) {\n stepCount++;\n\n final response = await _sendRequest(\n apiKey: apiKey,\n model: model,\n baseUrl: baseUrl,\n messages: fullMessages,\n );\n\n if (response == null) {\n yield AgentStreamChunk(error: 'API 请求失败');\n return;\n }\n\n final choice = response['choices']?[0] as Map?;\n if (choice == null) {\n yield AgentStreamChunk(error: 'API 响应格式异常');\n return;\n }\n\n final message = choice['message'] as Map?;\n if (message == null) {\n yield AgentStreamChunk(error: '消息为空');\n return;\n }\n\n // 文本响应\n final content = (message['content'] as String?)?.trim() ?? '';\n if (content.isNotEmpty) {\n yield AgentStreamChunk(textDelta: content);\n }\n\n // 工具调用\n final toolCalls = message['tool_calls'] as List?;\n if (toolCalls == null || toolCalls.isEmpty) {\n // 无工具调用,对话结束\n fullMessages.add(message);\n yield AgentStreamChunk(isDone: true);\n return;\n }\n\n // 处理工具调用\n fullMessages.add(message); // 添加 assistant 消息(含 tool_calls)\n\n for (final tc in toolCalls) {\n if (_cancelled) break;\n final toolCall = tc as Map;\n final fn = toolCall['function'] as Map?;\n if (fn == null) continue;\n\n final toolName = (fn['name'] as String?) ?? '';\n final toolArgsStr = (fn['arguments'] as String?) ?? '{}';\n\n yield AgentStreamChunk(toolCallName: toolName, toolCallArgs: toolArgsStr);\n\n // 解析参数\n Map args;\n try {\n args = jsonDecode(toolArgsStr) as Map;\n } catch (_) {\n args = {};\n }\n\n // 执行工具\n String toolResult;\n if (onToolCall != null) {\n try {\n toolResult = await onToolCall!(toolName, args);\n } catch (e) {\n toolResult = 'Error: $e';\n }\n } else {\n toolResult = '工具未配置执行回调';\n }\n\n yield AgentStreamChunk(toolResult: toolResult);\n\n // 添加 tool 响应消息\n fullMessages.add({\n 'role': 'tool',\n 'tool_call_id': toolCall['id'] ?? '',\n 'content': toolResult,\n });\n }\n }\n\n if (stepCount >= maxSteps) {\n yield AgentStreamChunk(textDelta: '\\n\\n已达到最大推理步数限制。');\n }\n yield AgentStreamChunk(isDone: true);\n }\n\n /// 发送一次请求(非流式,等完整响应后再处理,简化 tool calling 逻辑)。\n Future?> _sendRequest({\n required String apiKey,\n required String model,\n required String baseUrl,\n required List> messages,\n }) async {\n try {\n final body = jsonEncode({\n 'model': model,\n 'messages': messages,\n 'tools': geogebraTools.map((t) => t.toOpenAITool()).toList(),\n 'tool_choice': 'auto',\n 'temperature': 0.6,\n });\n\n _client = http.Client();\n final request = http.Request('POST', Uri.parse(baseUrl))\n ..headers.addAll({\n 'Content-Type': 'application/json; charset=utf-8',\n 'Authorization': 'Bearer $apiKey',\n })\n ..body = body;\n\n final streamedResponse = await _client!.send(request).timeout(\n const Duration(seconds: 60),\n onTimeout: () => throw Exception('请求超时'),\n );\n\n final responseBody = await streamedResponse.stream.bytesToString();\n\n if (streamedResponse.statusCode != 200) {\n debugPrint('[GeoAgent] API error: $responseBody');\n return null;\n }\n\n return jsonDecode(responseBody) as Map;\n } catch (e) {\n debugPrint('[GeoAgent] Request error: $e');\n return null;\n } finally {\n _closeClient();\n }\n }\n}\n"}, "27af3dafd8649ff653f8b0291f422506c388c09f": {"t": 1785757856.3879807, "n": 8524, "h": "5db151fe9967cf49", "c": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter_dotenv/flutter_dotenv.dart';\nimport 'package:http/http.dart' as http;\n\nimport 'prompts/geogebra_agent_prompt.dart';\n\n/// Agent 流式响应块\nclass AgentStreamChunk {\n final String? textDelta; // 文本增量\n final String? toolCallName; // 工具调用名称(开始时)\n final String? toolCallArgs; // 工具调用参数(完整 JSON)\n final String? toolResult; // 工具执行结果描述\n final bool isDone;\n final String? error;\n\n AgentStreamChunk({\n this.textDelta,\n this.toolCallName,\n this.toolCallArgs,\n this.toolResult,\n this.isDone = false,\n this.error,\n });\n}\n\n/// 工具定义(发给 LLM)\nclass AgentTool {\n final String name;\n final String description;\n final Map parameters; // JSON Schema\n\n const AgentTool({required this.name, required this.description, required this.parameters});\n\n Map toOpenAITool() => {\n 'type': 'function',\n 'function': {\n 'name': name,\n 'description': description,\n 'parameters': parameters,\n },\n };\n}\n\n/// GeoGebra 7 个 MCP 工具定义\nconst List geogebraTools = [\n AgentTool(\n name: 'getCanvasContext',\n description: '获取当前 GeoGebra 画布的完整状态,包括所有对象和表达式',\n parameters: {\n 'type': 'object',\n 'properties': {},\n },\n ),\n AgentTool(\n name: 'executeGeoGebraCommand',\n description: '在 GeoGebra 画布中执行一条命令,返回执行结果',\n parameters: {\n 'type': 'object',\n 'properties': {\n 'command': {'type': 'string', 'description': '要执行的 GeoGebra 命令'},\n },\n 'required': ['command'],\n },\n ),\n AgentTool(\n name: 'deleteGeoGebraObject',\n description: '从 GeoGebra 画布中删除指定标签的对象',\n parameters: {\n 'type': 'object',\n 'properties': {\n 'label': {'type': 'string', 'description': '要删除的对象标签'},\n },\n 'required': ['label'],\n },\n ),\n AgentTool(\n name: 'setUndoPoint',\n description: '在 GeoGebra 画布中设置一个撤销点',\n parameters: {\n 'type': 'object',\n 'properties': {},\n },\n ),\n AgentTool(\n name: 'undo',\n description: '在 GeoGebra 画布中执行撤销操作',\n parameters: {\n 'type': 'object',\n 'properties': {},\n },\n ),\n AgentTool(\n name: 'setPerspective',\n description: \"切换 GeoGebra 视图模式:'G'=2D图形, 'T'=3D图形\",\n parameters: {\n 'type': 'object',\n 'properties': {\n 'mode': {'type': 'string', 'description': \"视图模式:'G' 或 'T'\"},\n },\n 'required': ['mode'],\n },\n ),\n AgentTool(\n name: 'getSelectedObjects',\n description: '获取用户当前在 GeoGebra 画布中选中的对象标签列表',\n parameters: {\n 'type': 'object',\n 'properties': {},\n },\n ),\n];\n\n/// GeoGebra Agent 服务。\n///\n/// 使用 DeepSeek API 的 function calling 能力,让 LLM 通过工具调用\n/// 操控 GeoGebra 画布。对外提供流式响应。\nclass GeogebraAgentService {\n final String _apiKeyEnv;\n final String _modelEnv;\n final String _baseUrlEnv;\n final String _defaultBaseUrl;\n final String _defaultModel;\n\n /// 工具执行回调 —— 由外部注入,桥接 JS Bridge。\n Future Function(String toolName, Map args)? onToolCall;\n\n GeogebraAgentService({\n String apiKeyEnv = 'DEEPSEEK_API_KEY',\n String modelEnv = 'DEEPSEEK_MODEL_ID',\n String baseUrlEnv = 'DEEPSEEK_BASE_URL',\n String defaultBaseUrl = 'https://api.deepseek.com/chat/completions',\n String defaultModel = 'deepseek-chat',\n }) : _apiKeyEnv = apiKeyEnv,\n _modelEnv = modelEnv,\n _baseUrlEnv = baseUrlEnv,\n _defaultBaseUrl = defaultBaseUrl,\n _defaultModel = defaultModel;\n\n static bool _dotenvLoaded = false;\n http.Client? _client;\n bool _cancelled = false;\n\n Future _ensureEnv() async {\n if (_dotenvLoaded) return;\n await dotenv.load(fileName: '.env');\n _dotenvLoaded = true;\n }\n\n void cancel() {\n _cancelled = true;\n _closeClient();\n }\n\n void _closeClient() {\n try { _client?.close(); } catch (_) {}\n _client = null;\n }\n\n /// 流式对话入口。\n /// [messages] 为历史对话,不含 system prompt(会自动添加)。\n /// 最多 [maxSteps] 轮工具调用。\n Stream chat({\n required List> messages,\n int maxSteps = 10,\n }) async* {\n await _ensureEnv();\n _cancelled = false;\n\n final apiKey = (dotenv.env[_apiKeyEnv] ?? '').trim();\n final model = (dotenv.env[_modelEnv] ?? _defaultModel).trim();\n final baseUrl = (dotenv.env[_baseUrlEnv] ?? _defaultBaseUrl).trim();\n\n if (apiKey.isEmpty) {\n yield AgentStreamChunk(error: '缺少 API Key: $_apiKeyEnv');\n return;\n }\n\n // 构建完整消息列表(含 system prompt)\n final fullMessages = >[\n {'role': 'system', 'content': geogebraAgentSystemPrompt},\n ...messages.map((m) => {'role': m['role'], 'content': m['content']}),\n ];\n\n int stepCount = 0;\n while (stepCount < maxSteps && !_cancelled) {\n stepCount++;\n\n final response = await _sendRequest(\n apiKey: apiKey,\n model: model,\n baseUrl: baseUrl,\n messages: fullMessages,\n );\n\n if (response == null) {\n yield AgentStreamChunk(error: 'API 请求失败');\n return;\n }\n\n final choice = response['choices']?[0] as Map?;\n if (choice == null) {\n yield AgentStreamChunk(error: 'API 响应格式异常');\n return;\n }\n\n final message = choice['message'] as Map?;\n if (message == null) {\n yield AgentStreamChunk(error: '消息为空');\n return;\n }\n\n // 文本响应\n final content = (message['content'] as String?)?.trim() ?? '';\n if (content.isNotEmpty) {\n yield AgentStreamChunk(textDelta: content);\n }\n\n // 工具调用\n final toolCalls = message['tool_calls'] as List?;\n if (toolCalls == null || toolCalls.isEmpty) {\n // 无工具调用,对话结束\n fullMessages.add(message);\n yield AgentStreamChunk(isDone: true);\n return;\n }\n\n // 处理工具调用\n fullMessages.add(message); // 添加 assistant 消息(含 tool_calls)\n\n for (final tc in toolCalls) {\n if (_cancelled) break;\n final toolCall = tc as Map;\n final fn = toolCall['function'] as Map?;\n if (fn == null) continue;\n\n final toolName = (fn['name'] as String?) ?? '';\n final toolArgsStr = (fn['arguments'] as String?) ?? '{}';\n\n yield AgentStreamChunk(toolCallName: toolName, toolCallArgs: toolArgsStr);\n\n // 解析参数\n Map args;\n try {\n args = jsonDecode(toolArgsStr) as Map;\n } catch (_) {\n args = {};\n }\n\n // 执行工具\n String toolResult;\n if (onToolCall != null) {\n try {\n toolResult = await onToolCall!(toolName, args);\n } catch (e) {\n toolResult = 'Error: $e';\n }\n } else {\n toolResult = '工具未配置执行回调';\n }\n\n yield AgentStreamChunk(toolResult: toolResult);\n\n // 添加 tool 响应消息\n fullMessages.add({\n 'role': 'tool',\n 'tool_call_id': toolCall['id'] ?? '',\n 'content': toolResult,\n });\n }\n }\n\n if (stepCount >= maxSteps) {\n yield AgentStreamChunk(textDelta: '\\n\\n已达到最大推理步数限制。');\n }\n yield AgentStreamChunk(isDone: true);\n }\n\n /// 发送一次请求(非流式,等完整响应后再处理,简化 tool calling 逻辑)。\n Future?> _sendRequest({\n required String apiKey,\n required String model,\n required String baseUrl,\n required List> messages,\n }) async {\n try {\n final body = jsonEncode({\n 'model': model,\n 'messages': messages,\n 'tools': geogebraTools.map((t) => t.toOpenAITool()).toList(),\n 'tool_choice': 'auto',\n 'temperature': 0.6,\n });\n\n _client = http.Client();\n final request = http.Request('POST', Uri.parse(baseUrl))\n ..headers.addAll({\n 'Content-Type': 'application/json; charset=utf-8',\n 'Authorization': 'Bearer $apiKey',\n })\n ..body = body;\n\n final streamedResponse = await _client!.send(request).timeout(\n const Duration(seconds: 60),\n onTimeout: () => throw Exception('请求超时'),\n );\n\n final responseBody = await streamedResponse.stream.bytesToString();\n\n if (streamedResponse.statusCode != 200) {\n debugPrint('[GeoAgent] API error: $responseBody');\n return null;\n }\n\n return jsonDecode(responseBody) as Map;\n } catch (e) {\n debugPrint('[GeoAgent] Request error: $e');\n return null;\n } finally {\n _closeClient();\n }\n }\n}\n"}}, "deploy/proxy_server.js": {"cd430dbed71b344a768a697fd5da313464a413f6": {"t": 1785746682.0448568, "n": 4445, "h": "a93a2d5c32078cdb", "c": "const http = require('http');\nconst https = require('https');\nconst fs = require('fs');\n\n// ============================================================\n// MathMate API 代理服务\n// 用途:接收 Flutter Web 前端的 API 请求,转发到 DeepSeek/Vivo/Volc\n// 好处:API Key 只存在服务器,前端 JS 中不会泄露\n//\n// 启动: node proxy_server.js\n// 端口: 3001(Nginx 反向代理 /api/* → localhost:3001)\n// ============================================================\n\nfunction loadEnv() {\n const content = fs.readFileSync(__dirname + '/.env.server', 'utf-8');\n const env = {};\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith('#')) continue;\n const eq = trimmed.indexOf('=');\n if (eq === -1) continue;\n env[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim();\n }\n return env;\n}\n\nconst ROUTES = {\n '/api/deepseek': {\n apiKeyEnv: 'DEEPSEEK_API_KEY',\n modelEnv: 'DEEPSEEK_MODEL_ID',\n baseUrlEnv: 'DEEPSEEK_BASE_URL',\n defaultUrl: 'https://api.deepseek.com/chat/completions',\n },\n '/api/vivo': {\n apiKeyEnv: 'VIVO_API_KEY',\n modelEnv: 'VIVO_MODEL_ID',\n baseUrlEnv: 'VIVO_BASE_URL',\n defaultUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',\n },\n '/api/volc': {\n apiKeyEnv: 'VOLC_API_KEY',\n modelEnv: 'VOLC_MODEL_ID',\n baseUrlEnv: 'VOLC_BASE_URL',\n defaultUrl: 'https://ark.cn-beijing.volces.com/api/v3/chat/completions',\n },\n};\n\nconst env = loadEnv();\n\n// ---------- 代理转发 ----------\nfunction proxyRequest(clientReq, clientRes, bodyBuffer) {\n const route = ROUTES[clientReq.url];\n if (!route) {\n clientRes.writeHead(404, { 'Content-Type': 'application/json' });\n clientRes.end(JSON.stringify({ error: 'not found' }));\n return;\n }\n\n const apiKey = env[route.apiKeyEnv] || '';\n const modelId = env[route.modelEnv] || '';\n const baseUrl = env[route.baseUrlEnv] || route.defaultUrl;\n\n if (!apiKey || !modelId) {\n clientRes.writeHead(500, { 'Content-Type': 'application/json' });\n clientRes.end(JSON.stringify({ error: `Server config missing: ${route.apiKeyEnv} or ${route.modelEnv}` }));\n return;\n }\n\n const upstream = new URL(baseUrl);\n const isHttps = upstream.protocol === 'https:';\n const transport = isHttps ? https : http;\n\n const options = {\n hostname: upstream.hostname,\n port: upstream.port || (isHttps ? 443 : 80),\n path: upstream.pathname + upstream.search,\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + apiKey,\n 'Content-Length': Buffer.byteLength(bodyBuffer),\n },\n };\n\n console.log(`[proxy] → ${baseUrl}`);\n\n const upstreamReq = transport.request(options, (upstreamRes) => {\n const chunks = [];\n upstreamRes.on('data', c => chunks.push(c));\n upstreamRes.on('end', () => {\n const body = Buffer.concat(chunks);\n clientRes.writeHead(upstreamRes.statusCode, {\n 'Content-Type': 'application/json',\n 'Access-Control-Allow-Origin': '*',\n });\n clientRes.end(body);\n console.log(`[proxy] ← ${upstreamRes.statusCode} (${body.length} bytes)`);\n });\n });\n\n upstreamReq.on('error', (err) => {\n console.error('[proxy] error:', err.message);\n clientRes.writeHead(502, { 'Content-Type': 'application/json' });\n clientRes.end(JSON.stringify({ error: 'upstream error: ' + err.message }));\n });\n\n upstreamReq.write(bodyBuffer);\n upstreamReq.end();\n}\n\n// ---------- HTTP Server ----------\nconst server = http.createServer((req, res) => {\n const chunks = [];\n req.on('data', c => chunks.push(c));\n req.on('end', () => {\n const body = Buffer.concat(chunks);\n console.log(`[proxy] ${req.method} ${req.url} (${body.length} bytes)`);\n\n if (req.method === 'OPTIONS') {\n res.writeHead(200, {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'POST, OPTIONS',\n 'Access-Control-Allow-Headers': 'Content-Type, Authorization',\n });\n res.end();\n return;\n }\n\n if (req.method === 'POST' && ROUTES[req.url]) {\n proxyRequest(req, res, body);\n } else {\n res.writeHead(200, { 'Content-Type': 'text/plain' });\n res.end('MathMate Proxy OK');\n }\n });\n});\n\nserver.listen(3001, '127.0.0.1', () => {\n console.log('MathMate API Proxy running on http://127.0.0.1:3001');\n console.log('Routes:');\n for (const path of Object.keys(ROUTES)) {\n console.log(` POST ${path}`);\n }\n});\n"}, "7c567d8a98d24a78f1afcb8f155d571631901208": {"t": 1785757857.3075223, "n": 4445, "h": "a93a2d5c32078cdb", "c": "const http = require('http');\nconst https = require('https');\nconst fs = require('fs');\n\n// ============================================================\n// MathMate API 代理服务\n// 用途:接收 Flutter Web 前端的 API 请求,转发到 DeepSeek/Vivo/Volc\n// 好处:API Key 只存在服务器,前端 JS 中不会泄露\n//\n// 启动: node proxy_server.js\n// 端口: 3001(Nginx 反向代理 /api/* → localhost:3001)\n// ============================================================\n\nfunction loadEnv() {\n const content = fs.readFileSync(__dirname + '/.env.server', 'utf-8');\n const env = {};\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith('#')) continue;\n const eq = trimmed.indexOf('=');\n if (eq === -1) continue;\n env[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim();\n }\n return env;\n}\n\nconst ROUTES = {\n '/api/deepseek': {\n apiKeyEnv: 'DEEPSEEK_API_KEY',\n modelEnv: 'DEEPSEEK_MODEL_ID',\n baseUrlEnv: 'DEEPSEEK_BASE_URL',\n defaultUrl: 'https://api.deepseek.com/chat/completions',\n },\n '/api/vivo': {\n apiKeyEnv: 'VIVO_API_KEY',\n modelEnv: 'VIVO_MODEL_ID',\n baseUrlEnv: 'VIVO_BASE_URL',\n defaultUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',\n },\n '/api/volc': {\n apiKeyEnv: 'VOLC_API_KEY',\n modelEnv: 'VOLC_MODEL_ID',\n baseUrlEnv: 'VOLC_BASE_URL',\n defaultUrl: 'https://ark.cn-beijing.volces.com/api/v3/chat/completions',\n },\n};\n\nconst env = loadEnv();\n\n// ---------- 代理转发 ----------\nfunction proxyRequest(clientReq, clientRes, bodyBuffer) {\n const route = ROUTES[clientReq.url];\n if (!route) {\n clientRes.writeHead(404, { 'Content-Type': 'application/json' });\n clientRes.end(JSON.stringify({ error: 'not found' }));\n return;\n }\n\n const apiKey = env[route.apiKeyEnv] || '';\n const modelId = env[route.modelEnv] || '';\n const baseUrl = env[route.baseUrlEnv] || route.defaultUrl;\n\n if (!apiKey || !modelId) {\n clientRes.writeHead(500, { 'Content-Type': 'application/json' });\n clientRes.end(JSON.stringify({ error: `Server config missing: ${route.apiKeyEnv} or ${route.modelEnv}` }));\n return;\n }\n\n const upstream = new URL(baseUrl);\n const isHttps = upstream.protocol === 'https:';\n const transport = isHttps ? https : http;\n\n const options = {\n hostname: upstream.hostname,\n port: upstream.port || (isHttps ? 443 : 80),\n path: upstream.pathname + upstream.search,\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + apiKey,\n 'Content-Length': Buffer.byteLength(bodyBuffer),\n },\n };\n\n console.log(`[proxy] → ${baseUrl}`);\n\n const upstreamReq = transport.request(options, (upstreamRes) => {\n const chunks = [];\n upstreamRes.on('data', c => chunks.push(c));\n upstreamRes.on('end', () => {\n const body = Buffer.concat(chunks);\n clientRes.writeHead(upstreamRes.statusCode, {\n 'Content-Type': 'application/json',\n 'Access-Control-Allow-Origin': '*',\n });\n clientRes.end(body);\n console.log(`[proxy] ← ${upstreamRes.statusCode} (${body.length} bytes)`);\n });\n });\n\n upstreamReq.on('error', (err) => {\n console.error('[proxy] error:', err.message);\n clientRes.writeHead(502, { 'Content-Type': 'application/json' });\n clientRes.end(JSON.stringify({ error: 'upstream error: ' + err.message }));\n });\n\n upstreamReq.write(bodyBuffer);\n upstreamReq.end();\n}\n\n// ---------- HTTP Server ----------\nconst server = http.createServer((req, res) => {\n const chunks = [];\n req.on('data', c => chunks.push(c));\n req.on('end', () => {\n const body = Buffer.concat(chunks);\n console.log(`[proxy] ${req.method} ${req.url} (${body.length} bytes)`);\n\n if (req.method === 'OPTIONS') {\n res.writeHead(200, {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'POST, OPTIONS',\n 'Access-Control-Allow-Headers': 'Content-Type, Authorization',\n });\n res.end();\n return;\n }\n\n if (req.method === 'POST' && ROUTES[req.url]) {\n proxyRequest(req, res, body);\n } else {\n res.writeHead(200, { 'Content-Type': 'text/plain' });\n res.end('MathMate Proxy OK');\n }\n });\n});\n\nserver.listen(3001, '127.0.0.1', () => {\n console.log('MathMate API Proxy running on http://127.0.0.1:3001');\n console.log('Routes:');\n for (const path of Object.keys(ROUTES)) {\n console.log(` POST ${path}`);\n }\n});\n"}}}}