249 lines
8.1 KiB
PHP
249 lines
8.1 KiB
PHP
<?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']);
|
||
}
|
||
}
|