Files
KnowledgeBase/app/Http/Controllers/Api/TerminalApiController.php
2026-04-16 16:20:52 +08:00

249 lines
8.1 KiB
PHP
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.
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Document;
use App\Models\Guide;
use App\Models\GuidePage;
use App\Models\GuidePageEdge;
use App\Models\KnowledgeBase;
use App\Services\KnowledgeContextService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class TerminalApiController extends Controller
{
public function __construct(
private KnowledgeContextService $knowledgeService,
) {}
/**
* GET /api/terminal/config
* 返回终端配置
*/
public function config(Request $request): JsonResponse
{
$terminal = $request->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']);
}
}