229 lines
7.1 KiB
PHP
229 lines
7.1 KiB
PHP
<?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,
|
||
'voice_wakeup_enabled' => $terminal->voice_wakeup_enabled,
|
||
'voice_wakeup_word' => $terminal->voice_wakeup_word,
|
||
],
|
||
'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']);
|
||
}
|
||
}
|