refactor: 修复知识库和操作指引
This commit is contained in:
227
app/Http/Controllers/Api/TerminalApiController.php
Normal file
227
app/Http/Controllers/Api/TerminalApiController.php
Normal file
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Guide;
|
||||
use App\Models\GuidePage;
|
||||
use App\Services\KnowledgeContextService;
|
||||
use App\Services\PromptTemplateService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TerminalApiController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private PromptTemplateService $promptService,
|
||||
private KnowledgeContextService $knowledgeService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* GET /api/terminal/config
|
||||
* 返回终端配置(含渲染后的system prompt)
|
||||
*/
|
||||
public function config(Request $request): JsonResponse
|
||||
{
|
||||
$terminal = $request->attributes->get('terminal');
|
||||
$terminal->load(['prompt', 'knowledgeBases']);
|
||||
|
||||
// 渲染system prompt
|
||||
$systemPrompt = '';
|
||||
if ($terminal->prompt && $terminal->prompt->prompt_template) {
|
||||
$systemPrompt = $this->promptService->replaceVariables(
|
||||
$terminal->prompt->prompt_template,
|
||||
$terminal
|
||||
);
|
||||
}
|
||||
|
||||
// 获取终端关联的已发布指引数量
|
||||
$guideCount = $terminal->guides()->published()->count();
|
||||
|
||||
return response()->json([
|
||||
'terminal' => [
|
||||
'id' => $terminal->id,
|
||||
'name' => $terminal->name,
|
||||
'code' => $terminal->code,
|
||||
'station_id' => $terminal->station_id,
|
||||
'diagram_url' => $terminal->diagram_url,
|
||||
'scada_data_url' => $terminal->scada_data_url,
|
||||
'scada_tags_url' => $terminal->scada_tags_url,
|
||||
'display_config' => $terminal->display_config,
|
||||
],
|
||||
'system_prompt' => $systemPrompt,
|
||||
'guide_count' => $guideCount,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/terminal/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');
|
||||
$terminal->load('knowledgeBases');
|
||||
|
||||
$result = $this->knowledgeService->search($terminal, $request->input('query'));
|
||||
|
||||
return response()->json($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/terminal/guides?category=operation
|
||||
* 已发布的指引列表
|
||||
*/
|
||||
public function guides(Request $request): JsonResponse
|
||||
{
|
||||
$terminal = $request->attributes->get('terminal');
|
||||
$query = $terminal->guides()->published()->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
|
||||
* 组合多个指引的页面,返回递归树形结构
|
||||
*/
|
||||
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 = $terminal->guides()->published()->pluck('guides.id')->toArray();
|
||||
|
||||
$guideIds = $request->input('guide_ids');
|
||||
$pages = [];
|
||||
|
||||
foreach ($guideIds as $guideId) {
|
||||
if (!in_array($guideId, $accessibleIds)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$guide = Guide::with(
|
||||
$this->buildEagerLoadArray('trunkPages', 5)
|
||||
)->find($guideId);
|
||||
|
||||
if (!$guide) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($guide->trunkPages as $page) {
|
||||
$pages = array_merge($pages, $this->flattenSequentialPages($page, $guide->name, $guide->id));
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'pages' => $pages,
|
||||
'total_pages' => count($pages),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将树形页面结构展平:顺序节点(无 options)平铺,分支节点保留嵌套
|
||||
*/
|
||||
private function flattenSequentialPages(GuidePage $page, string $guideName, int $guideId): array
|
||||
{
|
||||
$data = [
|
||||
'id' => $page->id,
|
||||
'guide_id' => $guideId,
|
||||
'guide_name' => $guideName,
|
||||
'page_number' => $page->page_number,
|
||||
'title' => $page->title,
|
||||
'html_url' => $page->html_url,
|
||||
];
|
||||
|
||||
if ($page->options && $page->branchChildren->isNotEmpty()) {
|
||||
$data['options'] = $page->options;
|
||||
$branches = [];
|
||||
foreach ($page->branchChildren as $child) {
|
||||
$branches[$child->branch_option][] =
|
||||
$this->buildPageTree($child, $guideName, $guideId);
|
||||
}
|
||||
$data['branches'] = $branches;
|
||||
return [$data];
|
||||
}
|
||||
|
||||
if ($page->branchChildren->isNotEmpty()) {
|
||||
$result = [$data];
|
||||
foreach ($page->branchChildren as $child) {
|
||||
$result = array_merge($result, $this->flattenSequentialPages($child, $guideName, $guideId));
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
return [$data];
|
||||
}
|
||||
|
||||
private function buildPageTree(GuidePage $page, string $guideName, int $guideId): array
|
||||
{
|
||||
$data = [
|
||||
'id' => $page->id,
|
||||
'guide_id' => $guideId,
|
||||
'guide_name' => $guideName,
|
||||
'page_number' => $page->page_number,
|
||||
'title' => $page->title,
|
||||
'html_url' => $page->html_url,
|
||||
];
|
||||
|
||||
if ($page->options && $page->branchChildren->isNotEmpty()) {
|
||||
$data['options'] = $page->options;
|
||||
$branches = [];
|
||||
foreach ($page->branchChildren as $child) {
|
||||
$branches[$child->branch_option][] =
|
||||
$this->buildPageTree($child, $guideName, $guideId);
|
||||
}
|
||||
$data['branches'] = $branches;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function buildEagerLoadArray(string $base, int $depth): array
|
||||
{
|
||||
$loads = [$base => fn($q) => $q->orderBy('sort_order')];
|
||||
$current = $base;
|
||||
for ($i = 0; $i < $depth; $i++) {
|
||||
$current .= '.branchChildren';
|
||||
$loads[$current] = fn($q) => $q->orderBy('sort_order');
|
||||
}
|
||||
return $loads;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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']);
|
||||
}
|
||||
}
|
||||
38
app/Http/Middleware/IdentifyTerminal.php
Normal file
38
app/Http/Middleware/IdentifyTerminal.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\Terminal;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class IdentifyTerminal
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$macHeader = $request->header('X-Terminal-MAC');
|
||||
|
||||
if (!$macHeader) {
|
||||
return response()->json(['error' => 'Missing X-Terminal-MAC header'], 400);
|
||||
}
|
||||
|
||||
// HMI sends comma-separated MACs for all active interfaces;
|
||||
// match if any one corresponds to a registered terminal
|
||||
$macs = array_map('trim', explode(',', $macHeader));
|
||||
$terminal = Terminal::whereIn('mac_address', $macs)->first();
|
||||
|
||||
if (!$terminal) {
|
||||
return response()->json(['error' => 'Terminal not registered'], 403);
|
||||
}
|
||||
|
||||
$request->attributes->set('terminal', $terminal);
|
||||
|
||||
// Record IP address from header (for logging/diagnostics)
|
||||
if ($ip = $request->header('X-Terminal-IP')) {
|
||||
$request->attributes->set('terminal_ip', $ip);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user