690612:1407 ADR-035-235 #01
This commit is contained in:
@@ -8,12 +8,13 @@
|
|||||||
// - 2026-05-25: เพิ่ม AI Model Management UI สำหรับเลือกโมเดลแบบไดนามิก (ADR-027).
|
// - 2026-05-25: เพิ่ม AI Model Management UI สำหรับเลือกโมเดลแบบไดนามิก (ADR-027).
|
||||||
// - 2026-05-30: นำเข้าและแสดงผล OcrEngineSelector component ใน Overview tab (T019, T020)
|
// - 2026-05-30: นำเข้าและแสดงผล OcrEngineSelector component ใน Overview tab (T019, T020)
|
||||||
// - 2026-06-02: เพิ่มตัวบ่งชี้โมเดลหลักที่กำลังใช้งาน (Active Global Model badge) บนการ์ด System Toggle (T010, ADR-033)
|
// - 2026-06-02: เพิ่มตัวบ่งชี้โมเดลหลักที่กำลังใช้งาน (Active Global Model badge) บนการ์ด System Toggle (T010, ADR-033)
|
||||||
|
// - 2026-06-13: [235] ลบ AI Model Management (ADR-027) และ OCR Engine Selector ออก; แก้ System Toggle แสดง canonical names (np-dms-ai/np-dms-ocr); แก้ label OCR Sidecar
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Brain, Loader2, Power, ShieldCheck, Cpu, Database, Activity, Search, Info, HelpCircle, AlertCircle, Settings2, Trash2, ScanText } from 'lucide-react';
|
import { Brain, Loader2, Power, ShieldCheck, Cpu, Database, Activity, Search, Info, HelpCircle, AlertCircle, ScanText } from 'lucide-react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
@@ -27,12 +28,10 @@ import { projectService } from '@/lib/services/project.service';
|
|||||||
import {
|
import {
|
||||||
adminAiService,
|
adminAiService,
|
||||||
AiSandboxJobResult,
|
AiSandboxJobResult,
|
||||||
AiAvailableModel,
|
|
||||||
AiRagCitation,
|
AiRagCitation,
|
||||||
} from '@/lib/services/admin-ai.service';
|
} from '@/lib/services/admin-ai.service';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import OcrSandboxPromptManager from '@/components/admin/ai/OcrSandboxPromptManager';
|
import OcrSandboxPromptManager from '@/components/admin/ai/OcrSandboxPromptManager';
|
||||||
import OcrEngineSelector from '@/components/admin/ai/OcrEngineSelector';
|
|
||||||
|
|
||||||
interface SandboxProject {
|
interface SandboxProject {
|
||||||
publicId: string;
|
publicId: string;
|
||||||
@@ -96,6 +95,13 @@ function normalizeLoadedModels(value: unknown): VramLoadedModelView[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toCanonicalModel(rawName: string): string {
|
||||||
|
const name = rawName.toLowerCase();
|
||||||
|
if (name.includes('ocr') || name.includes('typhoon-np-dms-ocr')) return 'np-dms-ocr';
|
||||||
|
if (name.includes('typhoon') || name.includes('np-dms-ai')) return 'np-dms-ai';
|
||||||
|
return rawName;
|
||||||
|
}
|
||||||
|
|
||||||
export default function AiAdminConsolePage() {
|
export default function AiAdminConsolePage() {
|
||||||
const { data, isLoading, isError, refetch, isFetching } = useAiStatus();
|
const { data, isLoading, isError, refetch, isFetching } = useAiStatus();
|
||||||
const { data: health, isLoading: isHealthLoading, refetch: refetchHealth } = useAiHealth();
|
const { data: health, isLoading: isHealthLoading, refetch: refetchHealth } = useAiHealth();
|
||||||
@@ -110,16 +116,6 @@ export default function AiAdminConsolePage() {
|
|||||||
const [sandboxProgress, setSandboxProgress] = useState<number>(0);
|
const [sandboxProgress, setSandboxProgress] = useState<number>(0);
|
||||||
const [sandboxStatusText, setSandboxStatusText] = useState<string>('');
|
const [sandboxStatusText, setSandboxStatusText] = useState<string>('');
|
||||||
|
|
||||||
// AI Model Management State (ADR-027)
|
|
||||||
const { data: aiModelsData, refetch: refetchModels } = useQuery<{ models: AiAvailableModel[]; activeModel: string }>({
|
|
||||||
queryKey: ['ai-available-models'],
|
|
||||||
queryFn: async () => {
|
|
||||||
return await adminAiService.getAvailableModels();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const availableModels = ensureArray<AiAvailableModel>(aiModelsData?.models);
|
|
||||||
const activeModel = aiModelsData?.activeModel ?? '';
|
|
||||||
|
|
||||||
// VRAM Monitoring State (T034, T036, US2)
|
// VRAM Monitoring State (T034, T036, US2)
|
||||||
const { data: vramStatus, refetch: refetchVram } = useQuery({
|
const { data: vramStatus, refetch: refetchVram } = useQuery({
|
||||||
queryKey: ['ai-vram-status'],
|
queryKey: ['ai-vram-status'],
|
||||||
@@ -154,44 +150,8 @@ export default function AiAdminConsolePage() {
|
|||||||
await toggleMutation.mutateAsync(enabled);
|
await toggleMutation.mutateAsync(enabled);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleModelChange = async (modelId: string): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const selectedModel = availableModels.find(m => m.modelId === modelId || String(m.id) === modelId);
|
|
||||||
const name = selectedModel?.modelName || modelId;
|
|
||||||
await adminAiService.setActiveModel(modelId);
|
|
||||||
toast.success(`เปลี่ยนโมเดลเป็น ${name} สำเร็จ`);
|
|
||||||
await refetchModels();
|
|
||||||
refetchVram();
|
|
||||||
} catch (err: unknown) {
|
|
||||||
const errorResponse = err as { response?: { data?: { message?: string } } };
|
|
||||||
const errorMsg = errorResponse.response?.data?.message || 'ไม่สามารถเปลี่ยนโมเดลได้เนื่องจาก VRAM ไม่เพียงพอ';
|
|
||||||
toast.error(errorMsg);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleModel = async (modelName: string): Promise<void> => {
|
|
||||||
try {
|
|
||||||
await adminAiService.toggleModelActive(modelName);
|
|
||||||
toast.success(`เปลี่ยนสถานะโมเดล ${modelName} สำเร็จ`);
|
|
||||||
await refetchModels();
|
|
||||||
} catch {
|
|
||||||
toast.error('ไม่สามารถเปลี่ยนสถานะโมเดลได้');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveModel = async (modelName: string): Promise<void> => {
|
|
||||||
if (!confirm(`ต้องการลบโมเดล ${modelName} ใช่หรือไม่?`)) return;
|
|
||||||
try {
|
|
||||||
await adminAiService.removeModel(modelName);
|
|
||||||
toast.success(`ลบโมเดล ${modelName} สำเร็จ`);
|
|
||||||
await refetchModels();
|
|
||||||
} catch {
|
|
||||||
toast.error('ไม่สามารถลบโมเดลได้');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRefreshAll = async (): Promise<void> => {
|
const handleRefreshAll = async (): Promise<void> => {
|
||||||
await Promise.all([refetch(), refetchHealth(), refetchModels(), refetchVram()]);
|
await Promise.all([refetch(), refetchHealth(), refetchVram()]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmitSandbox = async (e: React.FormEvent): Promise<void> => {
|
const handleSubmitSandbox = async (e: React.FormEvent): Promise<void> => {
|
||||||
@@ -368,7 +328,7 @@ export default function AiAdminConsolePage() {
|
|||||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
<CardTitle className="flex items-center gap-2 text-sm font-medium">
|
<CardTitle className="flex items-center gap-2 text-sm font-medium">
|
||||||
<ScanText className="h-4 w-4 text-primary" />
|
<ScanText className="h-4 w-4 text-primary" />
|
||||||
OCR Sidecar (Tesseract)
|
OCR Sidecar (np-dms-ocr)
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
{isHealthLoading ? <Loader2 className="h-3 w-3 animate-spin text-muted-foreground" /> : renderStatusBadge(health?.ocr?.status)}
|
{isHealthLoading ? <Loader2 className="h-3 w-3 animate-spin text-muted-foreground" /> : renderStatusBadge(health?.ocr?.status)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -496,7 +456,7 @@ export default function AiAdminConsolePage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-lg">
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
@@ -513,10 +473,14 @@ export default function AiAdminConsolePage() {
|
|||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
Superadmin ยังสามารถเข้าถึงส่วนทดสอบและดูแลระบบได้ตามสิทธิ์
|
Superadmin ยังสามารถเข้าถึงส่วนทดสอบและดูแลระบบได้ตามสิทธิ์
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground flex items-center gap-1.5 pt-1">
|
<div className="text-xs text-muted-foreground flex items-center gap-1.5 pt-1 flex-wrap">
|
||||||
<span>Active Global Model:</span>
|
<span>Active Models:</span>
|
||||||
<Badge variant="outline" className="text-[10px] py-0 px-1.5 border-primary/20 text-primary bg-primary/5 font-semibold">
|
<Badge variant="outline" className="text-[10px] py-0 px-1.5 border-primary/20 text-primary bg-primary/5 font-semibold">
|
||||||
{activeModel || 'Loading...'}
|
{isHealthLoading ? 'Loading...' : toCanonicalModel(health?.activeModels?.main ?? 'np-dms-ai')}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-muted-foreground/50">+</span>
|
||||||
|
<Badge variant="outline" className="text-[10px] py-0 px-1.5 border-purple-500/20 text-purple-600 dark:text-purple-400 bg-purple-500/5 font-semibold">
|
||||||
|
{isHealthLoading ? 'Loading...' : toCanonicalModel(health?.activeModels?.ocr ?? 'np-dms-ocr')}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -538,114 +502,6 @@ export default function AiAdminConsolePage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* AI Model Management Card (ADR-027) */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2 text-lg">
|
|
||||||
<Settings2 className="h-5 w-5" />
|
|
||||||
AI Model Management
|
|
||||||
<Badge variant="outline" className="text-[10px]">ADR-027</Badge>
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
|
||||||
<div className="space-y-2 flex-1">
|
|
||||||
<label htmlFor="model-select" className="text-sm font-medium text-foreground">
|
|
||||||
โมเดล AI ที่ใช้งานอยู่ (Global)
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
value={availableModels.find((m) => m.modelName === activeModel)?.modelId || availableModels.find((m) => m.modelName === activeModel)?.id?.toString() || ''}
|
|
||||||
onValueChange={handleModelChange}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="model-select" className="w-full sm:w-[300px]">
|
|
||||||
<SelectValue placeholder="-- เลือกโมเดล --" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{availableModels
|
|
||||||
.filter((m) => m.isActive)
|
|
||||||
.map((model) => (
|
|
||||||
<SelectItem key={model.modelId || model.modelName} value={model.modelId || model.id?.toString() || model.modelName}>
|
|
||||||
{model.modelName}
|
|
||||||
{model.isDefault && (
|
|
||||||
<Badge variant="secondary" className="ml-2 text-[10px]">Default</Badge>
|
|
||||||
)}
|
|
||||||
{model.vramRequirementMB && (
|
|
||||||
<span className="ml-1 text-muted-foreground">({Math.round(model.vramRequirementMB / 1024 * 10) / 10}GB VRAM)</span>
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
โมเดลปัจจุบัน: <Badge variant="default">{activeModel || 'Loading...'}</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-t pt-4">
|
|
||||||
<h4 className="text-sm font-medium mb-3">รายการโมเดลทั้งหมด</h4>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{availableModels.length === 0 ? (
|
|
||||||
<p className="text-sm text-muted-foreground">ไม่มีโมเดลในระบบ</p>
|
|
||||||
) : (
|
|
||||||
availableModels.map((model) => (
|
|
||||||
<div
|
|
||||||
key={model.modelId || model.modelName}
|
|
||||||
className="flex items-center justify-between p-2 rounded border bg-background/50"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge
|
|
||||||
variant={model.isActive ? 'default' : 'secondary'}
|
|
||||||
className="text-[10px]"
|
|
||||||
>
|
|
||||||
{model.isActive ? 'Active' : 'Inactive'}
|
|
||||||
</Badge>
|
|
||||||
<span className="text-sm font-medium">{model.modelName}</span>
|
|
||||||
{model.isDefault && (
|
|
||||||
<Badge variant="outline" className="text-[10px]">Default</Badge>
|
|
||||||
)}
|
|
||||||
{activeModel === model.modelName && (
|
|
||||||
<Badge variant="default" className="text-[10px] bg-emerald-500">Current</Badge>
|
|
||||||
)}
|
|
||||||
{model.vramRequirementMB && (
|
|
||||||
<Badge variant="outline" className="text-[10px] border-amber-500/20 text-amber-500 bg-amber-500/5">
|
|
||||||
{Math.round(model.vramRequirementMB / 1024 * 10) / 10} GB VRAM
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{!model.isDefault && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleToggleModel(model.modelName)}
|
|
||||||
disabled={activeModel === model.modelName && model.isActive}
|
|
||||||
>
|
|
||||||
{model.isActive ? 'Deactivate' : 'Activate'}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleRemoveModel(model.modelName)}
|
|
||||||
disabled={model.isDefault || activeModel === model.modelName}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* OCR Engine Management Card (ADR-032) */}
|
|
||||||
<OcrEngineSelector />
|
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -675,7 +531,7 @@ export default function AiAdminConsolePage() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="playground" className="space-y-6">
|
<TabsContent value="playground" className="space-y-6">
|
||||||
<Card className="border border-border/50 bg-background/50 backdrop-blur-md">
|
<Card className="border border-border/50 bg-background/50 backdrop-blur-md">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// File: specs/200-fullstacks/235-ai-runtime-policy-refactor/quickstart.md
|
// File: specs/200-fullstacks/235-ai-runtime-policy-refactor/quickstart.md
|
||||||
// Change Log:
|
// Change Log:
|
||||||
// - 2026-06-11: Verification quickstart for AI Runtime Policy Refactor
|
// - 2026-06-11: Verification quickstart for AI Runtime Policy Refactor
|
||||||
|
// - 2026-06-12: เพิ่ม PowerShell syntax และ environment variable setup
|
||||||
|
|
||||||
# Quickstart: AI Runtime Policy Refactor — Verification Guide
|
# Quickstart: AI Runtime Policy Refactor — Verification Guide
|
||||||
|
|
||||||
@@ -11,51 +12,188 @@
|
|||||||
- Ollama running with `np-dms-ai` and `np-dms-ocr` tags registered
|
- Ollama running with `np-dms-ai` and `np-dms-ocr` tags registered
|
||||||
- Admin user token available
|
- Admin user token available
|
||||||
|
|
||||||
|
## Environment Setup
|
||||||
|
|
||||||
|
### การเข้าถึง Backend (สำคัญ)
|
||||||
|
|
||||||
|
จาก `@/specs/04-Infrastructure-OPS/04-00-docker-compose/QNAP/app/docker-compose-app.yml`:
|
||||||
|
|
||||||
|
| Environment | Backend URL | ใช้เมื่อ |
|
||||||
|
|-------------|-------------|----------|
|
||||||
|
| **Production (QNAP + NPM)** | `https://backend.np-dms.work/api` | ทดสอบจากเครื่องภายนอก (WSL, บ้าน) |
|
||||||
|
| **QNAP Internal** | `http://backend:3000` | ทดสอบจากภายใน QNAP (docker network) |
|
||||||
|
| **Local dev** | `http://localhost:3001` | รัน backend บนเครื่องตัวเอง |
|
||||||
|
|
||||||
|
**หมายเหตุ:** Backend container ใช้ port **3000** (ไม่ใช่ 3001) และอยู่ behind nginx proxy manager
|
||||||
|
|
||||||
|
### Bash (Linux/macOS/Git Bash on Windows)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# สำหรับ Production QNAP (ผ่าน HTTPS + NPM)
|
||||||
|
export BACKEND_URL="https://backend.np-dms.work/api"
|
||||||
|
|
||||||
|
# หรือถ้า SSH tunnel ไป QNAP แล้ว
|
||||||
|
# export BACKEND_URL="http://localhost:3000"
|
||||||
|
|
||||||
|
export TOKEN="your-jwt-token-here"
|
||||||
|
```
|
||||||
|
|
||||||
|
### PowerShell (Windows)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# สำหรับ Production QNAP (ผ่าน HTTPS + NPM)
|
||||||
|
$env:BACKEND_URL = "https://backend.np-dms.work/api"
|
||||||
|
|
||||||
|
# หรือถ้า SSH tunnel ไป QNAP แล้ว
|
||||||
|
# $env:BACKEND_URL = "http://localhost:3000"
|
||||||
|
|
||||||
|
$env:TOKEN = "your-jwt-token-here"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### วิธีหา TOKEN
|
||||||
|
|
||||||
|
**วิธีที่ 1: Login ผ่าน API (Bash)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Login แล้วดึง token จาก response
|
||||||
|
# หมายเหตุ: Backend ใช้ 'username' (ไม่ใช่ email) ใน login field
|
||||||
|
RESPONSE=$(curl -s -X POST "$BACKEND_URL/auth/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"username": "admin", "password": "Center2025"}')
|
||||||
|
|
||||||
|
# วิธีดึง TOKEN (เลือก 1 จาก 3):
|
||||||
|
|
||||||
|
# วิธี 1: ใช้ jq (ถ้าติดตั้งแล้ว)
|
||||||
|
# export TOKEN=$(echo $RESPONSE | jq -r '.access_token')
|
||||||
|
|
||||||
|
# วิธี 2: ใช้ Python (ทั่วไปมีอยู่แล้ว) — แนะนำ
|
||||||
|
export TOKEN=$(echo $RESPONSE | python3 -c "import sys, json; print(json.load(sys.stdin)['data']['access_token'])")
|
||||||
|
|
||||||
|
# วิธี 3: ดู response แล้ว copy เอง (ถ้าไม่มีทั้ง jq และ Python)
|
||||||
|
# echo $RESPONSE
|
||||||
|
# export TOKEN="paste_token_here"
|
||||||
|
```
|
||||||
|
|
||||||
|
**วิธีที่ 2: ดึงจาก Browser DevTools**
|
||||||
|
|
||||||
|
1. เปิด browser ไปที่ `http://192.168.10.8:3000` (frontend)
|
||||||
|
2. Login ด้วย account ที่มีสิทธิ์ admin
|
||||||
|
3. กด F12 → Network tab
|
||||||
|
4. รีเฟรชหน้า หรือ ทำ action ใดก็ได้
|
||||||
|
5. ดู request ที่ส่งไป backend → Headers → `Authorization: Bearer eyJhbG...`
|
||||||
|
6. Copy ค่าหลัง `Bearer ` มาใส่ใน `$TOKEN`
|
||||||
|
|
||||||
|
**วิธีที่ 3: ถ้ามี Access ตรงกับ Database**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- ดู username ที่มี role = 'admin' (หลังจากนั้นต้อง login ผ่าน API เพื่อเอา token)
|
||||||
|
SELECT username FROM users WHERE role = 'admin' LIMIT 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Default Users (จาก Seed Data)
|
||||||
|
|
||||||
|
ถ้าใช้ seed data เริ่มต้น มี users นี้ให้ใช้:
|
||||||
|
|
||||||
|
| Username | Role | Password |
|
||||||
|
|----------|------|----------|
|
||||||
|
| `superadmin` | Superadmin | `Center2025` |
|
||||||
|
| `admin` | Org Admin | `Center2025` |
|
||||||
|
| `editor01` | Editor | `Center2025` |
|
||||||
|
| `viewer01` | Viewer | `Center2025` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Gate 1: Policy Contract Verification
|
## Gate 1: Policy Contract Verification
|
||||||
|
|
||||||
### 1A. Reject model.key (should return 400)
|
### 1A. Reject model.key (should return 400)
|
||||||
|
|
||||||
|
**Bash:**
|
||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:3001/api/ai/jobs \
|
curl -X POST "$BACKEND_URL/ai/jobs" \
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"type": "rag-query", "model": {"key": "typhoon2.5-np-dms:latest"}}' \
|
-d '{"type": "rag-query", "model": {"key": "typhoon2.5-np-dms:latest"}}' \
|
||||||
| jq '.statusCode, .message'
|
| python3 -c "import sys, json; d=json.load(sys.stdin); e=d.get('error', {}); print(e.get('statusCode'), e.get('message'))"
|
||||||
|
# Expected: 400, message about model.key not allowed
|
||||||
|
```
|
||||||
|
|
||||||
|
**PowerShell:**
|
||||||
|
```powershell
|
||||||
|
$body = '{"type": "rag-query", "model": {"key": "typhoon2.5-np-dms:latest"}}'
|
||||||
|
Invoke-RestMethod -Uri "$env:BACKEND_URL/ai/jobs" -Method POST -Headers @{
|
||||||
|
"Authorization" = "Bearer $env:TOKEN"
|
||||||
|
"Content-Type" = "application/json"
|
||||||
|
} -Body $body | Select-Object statusCode, message
|
||||||
# Expected: 400, message about model.key not allowed
|
# Expected: 400, message about model.key not allowed
|
||||||
```
|
```
|
||||||
|
|
||||||
### 1B. Reject parameter overrides (should return 400)
|
### 1B. Reject parameter overrides (should return 400)
|
||||||
|
|
||||||
|
**Bash:**
|
||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:3001/api/ai/jobs \
|
curl -X POST "$BACKEND_URL/ai/jobs" \
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"type": "rag-query", "temperature": 0.9}' \
|
-d '{"type": "rag-query", "temperature": 0.9}' \
|
||||||
| jq '.statusCode'
|
| python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('error', {}).get('statusCode'))"
|
||||||
|
# Expected: 400
|
||||||
|
```
|
||||||
|
|
||||||
|
**PowerShell:**
|
||||||
|
```powershell
|
||||||
|
$body = '{"type": "rag-query", "temperature": 0.9}'
|
||||||
|
(Invoke-RestMethod -Uri "$env:BACKEND_URL/ai/jobs" -Method POST -Headers @{
|
||||||
|
"Authorization" = "Bearer $env:TOKEN"
|
||||||
|
"Content-Type" = "application/json"
|
||||||
|
} -Body $body).statusCode
|
||||||
# Expected: 400
|
# Expected: 400
|
||||||
```
|
```
|
||||||
|
|
||||||
### 1C. Valid executionProfile (should return 201)
|
### 1C. Valid executionProfile (should return 201)
|
||||||
|
|
||||||
|
**Bash:**
|
||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:3001/api/ai/jobs \
|
curl -X POST "$BACKEND_URL/ai/jobs" \
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"type": "rag-query", "executionProfile": "balanced", "documentPublicId": "<uuid>"}' \
|
-d '{"type": "rag-query", "executionProfile": "balanced", "documentPublicId": "<uuid-here>"}' \
|
||||||
| jq '.data.modelUsed'
|
| python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('data', {}).get('modelUsed'))"
|
||||||
|
# Expected: "np-dms-ai"
|
||||||
|
```
|
||||||
|
|
||||||
|
**PowerShell:**
|
||||||
|
```powershell
|
||||||
|
$body = '{"type": "rag-query", "executionProfile": "balanced", "documentPublicId": "<uuid-here>"}'
|
||||||
|
(Invoke-RestMethod -Uri "$env:BACKEND_URL/ai/jobs" -Method POST -Headers @{
|
||||||
|
"Authorization" = "Bearer $env:TOKEN"
|
||||||
|
"Content-Type" = "application/json"
|
||||||
|
} -Body $body).data.modelUsed
|
||||||
# Expected: "np-dms-ai"
|
# Expected: "np-dms-ai"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 1D. large-context by non-admin (should return 403)
|
### 1D. large-context by non-admin (should return 403)
|
||||||
|
|
||||||
|
**Bash:**
|
||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:3001/api/ai/jobs \
|
curl -X POST "$BACKEND_URL/ai/jobs" \
|
||||||
-H "Authorization: Bearer $NON_ADMIN_TOKEN" \
|
-H "Authorization: Bearer $NON_ADMIN_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"type": "rag-query", "executionProfile": "large-context"}' \
|
-d '{"type": "rag-query", "executionProfile": "large-context"}' \
|
||||||
| jq '.statusCode'
|
| python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('error', {}).get('statusCode'))"
|
||||||
|
# Expected: 403
|
||||||
|
```
|
||||||
|
|
||||||
|
**PowerShell:**
|
||||||
|
```powershell
|
||||||
|
$body = '{"type": "rag-query", "executionProfile": "large-context"}'
|
||||||
|
(Invoke-RestMethod -Uri "$env:BACKEND_URL/ai/jobs" -Method POST -Headers @{
|
||||||
|
"Authorization" = "Bearer $env:NON_ADMIN_TOKEN"
|
||||||
|
"Content-Type" = "application/json"
|
||||||
|
} -Body $body).statusCode
|
||||||
# Expected: 403
|
# Expected: 403
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -103,18 +241,34 @@ docker logs ocr-sidecar --tail 20
|
|||||||
|
|
||||||
### 4A. Force GPU pressure then run RAG
|
### 4A. Force GPU pressure then run RAG
|
||||||
|
|
||||||
|
**Step 1: Force load large model (Bash)**
|
||||||
```bash
|
```bash
|
||||||
# 1. Force load large model
|
# ถ้า Ollama รันบน Desk-5439 (192.168.10.100)
|
||||||
curl http://localhost:11434/api/generate -d '{"model":"np-dms-ai","prompt":"warmup","keep_alive":-1}'
|
curl http://192.168.10.100:11434/api/generate -d '{"model":"np-dms-ai","prompt":"warmup","keep_alive":-1}'
|
||||||
|
```
|
||||||
|
|
||||||
# 2. Run RAG query
|
**Step 2: Run RAG query**
|
||||||
curl -X POST http://localhost:3001/api/ai/jobs \
|
|
||||||
|
*Bash:*
|
||||||
|
```bash
|
||||||
|
curl -X POST "$BACKEND_URL/ai/jobs" \
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
-d '{"type":"rag-query","executionProfile":"balanced","documentPublicId":"<uuid>"}' \
|
-d '{"type":"rag-query","executionProfile":"balanced","documentPublicId":"<uuid>"}' \
|
||||||
| jq '.data.status'
|
| jq '.data.status'
|
||||||
# Expected: "completed" (ไม่ fail)
|
# Expected: "completed" (ไม่ fail)
|
||||||
|
```
|
||||||
|
|
||||||
# 3. ตรวจ sidecar log
|
*PowerShell:*
|
||||||
|
```powershell
|
||||||
|
$body = '{"type":"rag-query","executionProfile":"balanced","documentPublicId":"<uuid-here>"}'
|
||||||
|
(Invoke-RestMethod -Uri "$env:BACKEND_URL/ai/jobs" -Method POST -Headers @{
|
||||||
|
"Authorization" = "Bearer $env:TOKEN"
|
||||||
|
} -Body $body).data.status
|
||||||
|
# Expected: "completed" (ไม่ fail)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: ตรวจ sidecar log**
|
||||||
|
```bash
|
||||||
docker logs ocr-sidecar --tail 20
|
docker logs ocr-sidecar --tail 20
|
||||||
# Expected: device=cpu reason=gpu-headroom-below-threshold
|
# Expected: device=cpu reason=gpu-headroom-below-threshold
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user