refactor: 修复知识库和操作指引

This commit is contained in:
2026-03-13 14:32:37 +08:00
parent bbe8e60646
commit 58f42de9df
88 changed files with 3387 additions and 2472 deletions

View 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']);
}
}

View 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);
}
}