attributes->get('terminal'); $terminal->load('station'); $systemPrompt = $terminal->prompt_template ?? ''; // 获取终端所属线站的已发布指引数量(含全局指引) $guideCount = $this->getTerminalGuides($terminal)->count(); return response()->json([ 'terminal' => [ 'id' => $terminal->id, 'terminal_name' => $terminal->name, 'terminal_code' => $terminal->code, 'station_name' => $terminal->station?->name, 'diagram_urls' => collect($terminal->diagram_urls ?? [])->values()->map(fn ($item) => [ 'title' => $item['title'] ?? '', 'url' => $item['url'] ?? '', ])->all(), 'scada_data_url' => $terminal->scada_data_url, 'scada_tags_url' => $terminal->scada_tags_url, 'voice_wakeup_enabled' => $terminal->voice_wakeup_enabled, 'voice_wakeup_word' => $terminal->voice_wakeup_word, ], 'system_prompt' => $systemPrompt, 'guide_count' => $guideCount, ]); } /** * GET /api/knowledge?query=xxx * RAG知识搜索(由AI tool_call触发) */ public function knowledge(Request $request): JsonResponse { $request->validate([ 'query' => 'required|string|max:500', ]); $terminal = $request->attributes->get('terminal'); $result = $this->knowledgeService->search($request->input('query'), $terminal); return response()->json($result); } /** * GET /api/terminal/guides?category=operation * 已发布的指引列表 */ public function guides(Request $request): JsonResponse { $terminal = $request->attributes->get('terminal'); $query = $this->getTerminalGuides($terminal)->withCount('pages'); if ($category = $request->input('category')) { $query->where('category', $category); } $guides = $query->orderBy('name')->get()->map(fn(Guide $guide) => [ 'id' => $guide->id, 'name' => $guide->name, 'description' => $guide->description, 'category' => $guide->category, 'tags' => $guide->tags, 'page_count' => $guide->pages_count, ]); return response()->json(['guides' => $guides]); } /** * POST /api/terminal/guides/pages * 返回指引页面(状态机格式,每页带 next 指针) */ public function guidePages(Request $request): JsonResponse { $request->validate([ 'guide_ids' => 'required|array|min:1', 'guide_ids.*' => 'integer|exists:guides,id', ]); $terminal = $request->attributes->get('terminal'); $accessibleIds = $this->getTerminalGuides($terminal)->pluck('guides.id')->toArray(); $guideIds = collect($request->input('guide_ids')) ->intersect($accessibleIds) ->values() ->toArray(); $pages = GuidePage::whereIn('guide_id', $guideIds)->get(); $edges = GuidePageEdge::whereIn('guide_id', $guideIds) ->orderBy('from_page_id') ->orderBy('sort') ->get(); $edgesByFrom = $edges->groupBy('from_page_id'); $hasIncoming = $edges->pluck('to_page_id')->unique()->flip(); $guides = []; foreach ($pages->groupBy('guide_id') as $guideId => $guidePages) { $entryPage = $guidePages->first(fn($p) => !$hasIncoming->has($p->id)); $pagesMap = []; foreach ($guidePages as $page) { $next = $edgesByFrom->get($page->id, collect()) ->map(function (GuidePageEdge $e) { $item = ['page_id' => $e->to_page_id]; if ($e->label !== null) { $item['label'] = $e->label; } return $item; })->values()->toArray(); $pagesMap[$page->id] = [ 'id' => $page->id, 'title' => $page->title, 'uri' => $page->uri, 'next' => $next, ]; } $guides[$guideId] = [ 'entry_page_id' => $entryPage?->id, 'pages' => $pagesMap, ]; } return response()->json([ 'guides' => $guides, ]); } /** * 获取终端可见的指引(线站关联 + 全局) */ private function getTerminalGuides($terminal) { $stationId = $terminal->station_id; return Guide::published()->where(function ($q) use ($stationId) { $q->whereDoesntHave('stations'); // 全局指引 if ($stationId) { $q->orWhereHas('stations', fn($sq) => $sq->where('stations.id', $stationId)); } }); } /** * GET /api/terminal/documents/{document}/content * 读取文档全文或指定行号区间 */ public function documentContent(Request $request, int $documentId): JsonResponse { $request->validate([ 'start_line' => 'sometimes|integer|min:1', 'end_line' => 'sometimes|integer|min:1', ]); $terminal = $request->attributes->get('terminal'); // Find document and verify access through station → knowledge_base $accessibleKbIds = KnowledgeBase::where(function ($q) use ($terminal) { $q->whereDoesntHave('stations'); // global knowledge bases if ($terminal->station_id) { $q->orWhereHas('stations', fn ($sq) => $sq->where('stations.id', $terminal->station_id)); } })->where('status', 'active')->pluck('id'); $document = Document::where('id', $documentId) ->whereIn('knowledge_base_id', $accessibleKbIds) ->where('conversion_status', 'completed') ->first(); if (!$document) { return response()->json(['error' => 'Document not found'], 404); } $content = $document->getMarkdownContent(); if ($content === null) { return response()->json(['error' => 'Document content unavailable'], 404); } $lines = explode("\n", $content); $totalLines = count($lines); $startLine = $request->integer('start_line', 1); $endLine = $request->integer('end_line', min($startLine + 49, $totalLines)); $endLine = min($endLine, $totalLines); if ($startLine > $totalLines) { return response()->json([ 'error' => "start_line ({$startLine}) exceeds total lines ({$totalLines})", ], 422); } $slice = array_slice($lines, $startLine - 1, $endLine - $startLine + 1); return response()->json([ 'id' => $document->id, 'title' => $document->title, 'total_lines' => $totalLines, 'start_line' => $startLine, 'end_line' => $endLine, 'content' => implode("\n", $slice), ]); } /** * POST /api/terminal/heartbeat * 终端心跳上报 */ public function heartbeat(Request $request): JsonResponse { $terminal = $request->attributes->get('terminal'); $terminal->update([ 'is_online' => true, 'last_online_at' => now(), ]); return response()->json(['status' => 'ok']); } }