Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/chaiqueeee_mathmate.json
T

1 line
42 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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<void> _ensureEnvLoaded() async {\n if (_dotenvLoaded) return;\n await dotenv.load(fileName: '.env');\n _dotenvLoaded = true;\n }\n\n Future<String> 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<String, String> headers = <String, String>{\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer $apiKey',\n };\n\n final List<Map<String, String>> messages = <Map<String, String>>[\n <String, String>{'role': 'system', 'content': prompt},\n <String, String>{'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(<String, dynamic>{\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<void> _ensureEnvLoaded() async {\n if (_dotenvLoaded) return;\n await dotenv.load(fileName: '.env');\n _dotenvLoaded = true;\n }\n\n Future<String> 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<String, String> headers = <String, String>{\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer $apiKey',\n };\n\n final List<Map<String, String>> messages = <Map<String, String>>[\n <String, String>{'role': 'system', 'content': prompt},\n <String, String>{'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(<String, dynamic>{\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<void> _ensureEnvLoaded() async {\n if (_dotenvLoaded) return;\n await dotenv.load(fileName: '.env');\n _dotenvLoaded = true;\n }\n\n Future<String> 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<String, String> headers = <String, String>{\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer $apiKey',\n };\n\n final List<Map<String, String>> messages = <Map<String, String>>[\n <String, String>{'role': 'system', 'content': prompt},\n <String, String>{'role': 'user', 'content': userText},\n ];\n\n final http.Response response = await http.post(\n Uri.parse(baseUrl),\n headers: headers,\n body: jsonEncode(<String, dynamic>{\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<void> _ensureEnvLoaded() async {\n if (_dotenvLoaded) return;\n await dotenv.load(fileName: '.env');\n _dotenvLoaded = true;\n }\n\n Future<String> 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<String, String> headers = <String, String>{\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer $apiKey',\n };\n\n final List<Map<String, String>> messages = <Map<String, String>>[\n <String, String>{'role': 'system', 'content': prompt},\n <String, String>{'role': 'user', 'content': userText},\n ];\n\n final http.Response response = await http.post(\n Uri.parse(baseUrl),\n headers: headers,\n body: jsonEncode(<String, dynamic>{\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<String, dynamic> parameters; // JSON Schema\n\n const AgentTool({required this.name, required this.description, required this.parameters});\n\n Map<String, dynamic> 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<AgentTool> 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<String> Function(String toolName, Map<String, dynamic> 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<void> _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<AgentStreamChunk> chat({\n required List<Map<String, String>> 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 = <Map<String, dynamic>>[\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<String, dynamic>?;\n if (choice == null) {\n yield AgentStreamChunk(error: 'API 响应格式异常');\n return;\n }\n\n final message = choice['message'] as Map<String, dynamic>?;\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<dynamic>?;\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<String, dynamic>;\n final fn = toolCall['function'] as Map<String, dynamic>?;\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<String, dynamic> args;\n try {\n args = jsonDecode(toolArgsStr) as Map<String, dynamic>;\n } catch (_) {\n args = <String, dynamic>{};\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<Map<String, dynamic>?> _sendRequest({\n required String apiKey,\n required String model,\n required String baseUrl,\n required List<Map<String, dynamic>> 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<String, dynamic>;\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<String, dynamic> parameters; // JSON Schema\n\n const AgentTool({required this.name, required this.description, required this.parameters});\n\n Map<String, dynamic> 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<AgentTool> 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<String> Function(String toolName, Map<String, dynamic> 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<void> _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<AgentStreamChunk> chat({\n required List<Map<String, String>> 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 = <Map<String, dynamic>>[\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<String, dynamic>?;\n if (choice == null) {\n yield AgentStreamChunk(error: 'API 响应格式异常');\n return;\n }\n\n final message = choice['message'] as Map<String, dynamic>?;\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<dynamic>?;\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<String, dynamic>;\n final fn = toolCall['function'] as Map<String, dynamic>?;\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<String, dynamic> args;\n try {\n args = jsonDecode(toolArgsStr) as Map<String, dynamic>;\n } catch (_) {\n args = <String, dynamic>{};\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<Map<String, dynamic>?> _sendRequest({\n required String apiKey,\n required String model,\n required String baseUrl,\n required List<Map<String, dynamic>> 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<String, dynamic>;\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"}}}}