定制 anwar/gunma-ai-agent 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

anwar/gunma-ai-agent

最新稳定版本:v1.1.0

Composer 安装命令:

composer require anwar/gunma-ai-agent

包简介

AI Agent orchestrator for Gunma Halal Food — OpenAI tool calling, Qdrant vector search, Ollama embeddings

README 文档

README

Packages covered: anwar/gunma-ai-agent (Laravel) · gunma-chat-widget (React) · gunma-agent-dashboard (React)

Table of Contents

  1. Architecture Overview
  2. Backend Package — gunma-ai-agent
  3. Chat Widget — gunma-chat-widget
  4. Admin Dashboard — gunma-agent-dashboard
  5. Cross-Package Auth Flow Diagrams
  6. Cookbook — Common Scenarios
  7. Environment Variable Reference

1. Architecture Overview

┌─────────────────────────────────────────────────────────────────┐
│                        Host Laravel App                         │
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │              anwar/gunma-ai-agent  (PHP package)         │  │
│  │                                                          │  │
│  │  routes/api.php ──► ChatController                       │  │
│  │       │                    │                             │  │
│  │  ResolveCustomer      AgentOrchestrator                 │  │
│  │  Middleware            (OpenAI + Qdrant)                 │  │
│  │  (tries guards:                │                         │  │
│  │   customer/sanctum/web)   SSE stream                     │  │
│  │       │                        │                         │  │
│  │  auth()->user()          Broadcasting                    │  │
│  │  (or null for guest)     (Pusher/Soketi)                │  │
│  └──────────────────────────────────────────────────────────┘  │
│            ▲                       ▲                            │
│            │ HTTP + SSE            │ WebSocket                  │
│            │                       │                            │
│   ┌────────┴────────┐   ┌──────────┴─────────┐                │
│   │ gunma-chat-     │   │  gunma-agent-       │                │
│   │ widget (React)  │   │  dashboard (React)  │                │
│   │                 │   │                     │                │
│   │ Guest ✓         │   │  Admin only         │                │
│   │ Authenticated ✓ │   │  (web/sanctum auth) │                │
│   └─────────────────┘   └─────────────────────┘                │
└─────────────────────────────────────────────────────────────────┘

2. Backend Package — gunma-ai-agent

2.1 Installation

composer require anwar/gunma-ai-agent

php artisan vendor:publish --tag=gunma-agent-config
php artisan vendor:publish --tag=gunma-agent-migrations
php artisan migrate

2.2 Configuration Reference

All keys are in config/gunma-agent.php. Every value can be overridden via .env.

Config Key Env Variable Default Description
route_prefix GUNMA_ROUTE_PREFIX api/chat Public chat route prefix
admin_route_prefix GUNMA_ADMIN_PREFIX api/admin/chat Admin route prefix
middleware GUNMA_MIDDLEWARE api Middleware stack for public routes (pipe-separated in .env)
admin_middleware GUNMA_ADMIN_MIDDLEWARE web Middleware for admin routes
auth_guards GUNMA_AUTH_GUARDS customer|sanctum|web Guards tried by ResolveCustomer middleware (pipe-separated)
broadcast_admin_channel GUNMA_BROADCAST_ADMIN_CHANNEL gunma-admin.chats Private channel for admin dashboard
broadcast_chat_prefix GUNMA_BROADCAST_CHAT_PREFIX gunma-chat Channel prefix for per-session chat rooms

2.3 Authentication — How It Works

The package uses a ResolveCustomer middleware on all public chat routes. It is non-blocking — guests are never rejected; authenticated users are silently identified.

The Resolution Flow

Incoming request
      │
      ▼
Does request have Authorization: Bearer <token> header?
      │
      ├─ YES ─► Try each guard in auth_guards order:
      │            1. 'customer' guard (Laravel Passport)
      │            2. 'sanctum' guard (Sanctum token)
      │            3. 'api' guard (custom token driver)
      │         If a guard succeeds → auth()->shouldUse(that guard)
      │                              → auth()->user() is available
      │
      ├─ NO, but has session cookie ─► Try session-based guards:
      │            1. 'web' guard
      │            2. 'sanctum' guard (Sanctum session)
      │         If a guard succeeds → auth()->shouldUse(that guard)
      │
      └─ NO auth at all ─► auth()->user() returns null → Guest mode

What Changes When a Customer is Authenticated

Feature Guest Authenticated Customer
Session creation visitor_id based customer_id + visitor_id
Display name From customer_name param Auto-read from user->name
Session resumption By visitor_id By customer_id (more reliable)
Anonymous → auth upgrade N/A Existing session gets customer_id bound
Cart operations Cookie-based Token-based (cookie ignored)

Configuring Your Auth Guard

Sanctum token authentication:

GUNMA_AUTH_GUARDS=sanctum|web
GUNMA_MIDDLEWARE=api|throttle:60,1

Laravel Passport with a customer guard:

GUNMA_AUTH_GUARDS=customer|sanctum|web
GUNMA_MIDDLEWARE=api|throttle:60,1

Session-only (SPA with Sanctum cookie):

GUNMA_AUTH_GUARDS=sanctum|web
GUNMA_MIDDLEWARE=web|throttle:60,1

No authentication (pure public widget):

GUNMA_AUTH_GUARDS=
GUNMA_MIDDLEWARE=api|throttle:60,1

Accessing the User in Your Own Code

After installing the package, you can check customer identity in custom listeners or observers:

// In any code running within a request
$user = auth()->user();   // null for guests

if ($user) {
    // Authenticated customer
    $customerId = $user->id;
}

2.4 Route Map

Public Routes (Guest + Authenticated)

Method Path Description
POST /{prefix}/sessions Create or resume a chat session
GET /{prefix}/sessions/{id} Get session details + messages
POST /{prefix}/sessions/{id}/messages Send message (SSE streaming)
POST /{prefix}/sessions/{id}/messages/sync Send message (JSON, no streaming)
GET /{prefix}/sessions/{id}/messages Get message history
POST /{prefix}/sessions/{id}/end End session
POST /{prefix}/sessions/{id}/typing Broadcast typing indicator
POST /{prefix}/cart/bulk Bulk add-to-cart
POST /{prefix}/upload Upload image file

Default {prefix} = api/chat. Override with GUNMA_ROUTE_PREFIX.

Admin Routes (Authenticated Only)

Method Path Description
GET /{admin_prefix}/sessions List all sessions
GET /{admin_prefix}/sessions/{id} Get session detail
POST /{admin_prefix}/sessions/{id}/toggle-ai Toggle AI for session
POST /{admin_prefix}/sessions/{id}/messages Send manual message
POST /{admin_prefix}/sessions/{id}/typing Broadcast typing
GET /{admin_prefix}/stats Dashboard statistics
GET /{admin_prefix}/tickets List support tickets
POST /{admin_prefix}/tickets/{id}/status Update ticket status

Default {admin_prefix} = api/admin/chat. Override with GUNMA_ADMIN_PREFIX. Admin middleware defaults to web. For Sanctum SPA: GUNMA_ADMIN_MIDDLEWARE=web|auth:sanctum.

3. Chat Widget — gunma-chat-widget

3.1 Installation

npm install gunma-chat-widget
import 'gunma-chat-widget/styles.css';
import { ChatWidget } from 'gunma-chat-widget';

3.2 Guest Usage

No authentication needed. The widget creates an anonymous session using a browser-generated visitor_id.

<ChatWidget
  apiUrl="https://api.yourdomain.com"
  brandName="Your Store"
  welcomeMessage="Hi! How can I help you today?"
/>

How it works internally:

  • A visitor_id is generated and stored in localStorage['gunma_visitor_id']
  • Session ID is persisted in localStorage['gunma_session_id']
  • For cart operations, the host app cookie is read from localStorage['gunma_cookie']
  • No Authorization header is sent → backend serves as guest

3.3 Authenticated Customer Usage

Pass the customer's token so the backend can link the chat to their account.

Option A: Direct token (simplest)

// Read from wherever your auth system stores it
const token = localStorage.getItem('customer_token');

<ChatWidget
  apiUrl="https://api.yourdomain.com"
  apiToken={token ?? undefined}
  brandName="Your Store"
/>

Option B: Dynamic resolver (Redux / Zustand / Context)

import { useSelector } from 'react-redux';

function MyApp() {
  const authToken = useSelector(state => state.auth.token);

  return (
    <ChatWidget
      apiUrl="https://api.yourdomain.com"
      getToken={() => authToken}  // re-evaluated on each request
      brandName="Your Store"
    />
  );
}

Option C: Custom localStorage key

<ChatWidget
  apiUrl="https://api.yourdomain.com"
  storage={{ tokenKeys: ['customer_access_token', 'auth_token'] }}
  brandName="Your Store"
/>

Option D: Sanctum SPA (session cookie, no token)

When using Sanctum with cookie-based sessions (same domain), no token is needed. The CSRF cookie is automatically handled:

<ChatWidget
  apiUrl="https://api.yourdomain.com"
  // No apiToken needed — session cookie is sent automatically (withCredentials)
  brandName="Your Store"
/>

Backend requirement: Set GUNMA_MIDDLEWARE=web (not api) so the session middleware is active.

3.4 Full Config Reference

interface ChatWidgetConfig {
  // ── Required ────────────────────────────────────────────────
  apiUrl: string;               // Backend base URL

  // ── Branding ────────────────────────────────────────────────
  brandName?: string;           // Widget header title
  brandColor?: string;          // Primary color (hex/hsl)
  welcomeMessage?: string;      // First message shown to user
  placeholder?: string;         // Input placeholder text
  position?: 'bottom-right' | 'bottom-left';  // Default: 'bottom-right'
  theme?: 'light' | 'dark' | 'auto';          // Default: 'auto'
  zIndex?: number;              // CSS z-index (default: 9999)
  websiteUrl?: string;          // Used for product link generation

  // ── Identity ─────────────────────────────────────────────────
  visitorId?: string;           // Override auto-generated visitor ID
  customerName?: string;        // Display name override
  channel?: 'web' | 'admin';   // Defaults to 'web'
  cookieId?: string;            // Guest cart cookie override

  // ── Authentication (choose one) ──────────────────────────────
  apiToken?: string;            // Explicit Bearer token
  getToken?: () => string | null; // Dynamic token resolver

  // ── Route Configuration ───────────────────────────────────────
  routes?: {
    prefix?: string;            // Must match GUNMA_ROUTE_PREFIX. Default: 'api/chat'
    csrfCookie?: string;        // CSRF endpoint. Default: '/sanctum/csrf-cookie'
  };

  // ── Storage Key Overrides ─────────────────────────────────────
  storage?: {
    visitorIdKey?: string;      // Default: 'gunma_visitor_id'
    sessionIdKey?: string;      // Default: 'gunma_session_id'
    tokenKeys?: string[];       // Default: ['tk', 'token']
    // Note: cookieKey is on useCartActions config
  };

  // ── Real-Time (Pusher / Soketi) ───────────────────────────────
  pusher?: {
    key: string;                // Pusher app key (REQUIRED for real-time)
    cluster?: string;           // e.g. 'eu', 'ap3'
    wsHost?: string;            // Self-hosted Soketi host
    wsPort?: number;            // Default: 6001
    forceTLS?: boolean;         // Default: false
    authEndpoint?: string;      // Default: '/api/broadcasting/auth'
  };

  // ── Cart Integration ──────────────────────────────────────────
  cartUrl?: string;             // Host app single-product cart endpoint
                                // Example: 'https://mystore.com/customer/Frontend/Carts'
                                // Required for "Add to Cart" buttons in chat
}

4. Admin Dashboard — gunma-agent-dashboard

4.1 Installation

npm install gunma-agent-dashboard
import 'gunma-agent-dashboard/styles.css';
import { AgentDashboard } from 'gunma-agent-dashboard';
// or use the hook directly:
import { useMonitor } from 'gunma-agent-dashboard';

4.2 Auth Patterns

The dashboard calls admin-only routes which require authenticated middleware on the backend. The hook sends the admin token on every request.

Session-based (Sanctum SPA)

// Works when admin is logged in via the same domain session
<AgentDashboard
  apiUrl="https://api.yourdomain.com"
  pusher={{ key: 'your-pusher-key' }}
/>

No token needed — session cookie is sent automatically (withCredentials: true).

Token-based (Sanctum Personal Access Token)

<AgentDashboard
  apiUrl="https://api.yourdomain.com"
  auth={{ tokenKeys: ['admin_token'] }}
  pusher={{ key: 'your-pusher-key', cluster: 'eu' }}
/>

Dynamic token (Redux/Zustand)

const token = useSelector(state => state.admin.token);

<AgentDashboard
  apiUrl="https://api.yourdomain.com"
  auth={{ getToken: () => token }}
  pusher={{ key: 'your-pusher-key' }}
/>

4.3 Full Options Reference

interface UseMonitorOptions {
  // ── Polling ────────────────────────────────────────────────────
  pollInterval?: number;        // Session list refresh interval (ms). Default: 15000. 0 = disabled.

  // ── Authentication ─────────────────────────────────────────────
  auth?: {
    tokenKeys?: string[];       // localStorage keys to try. Default: ['token', 'tk']
    getToken?: () => string | null; // Dynamic resolver (overrides tokenKeys)
  };

  // ── Real-Time (Pusher / Soketi) ────────────────────────────────
  pusher?: {
    key: string;                // REQUIRED for real-time. Omit to disable WebSockets.
    cluster?: string;
    wsHost?: string;
    wsPort?: number;            // Default: 6001
    forceTLS?: boolean;         // Default: false
    authEndpoint?: string;      // Default: '/api/broadcasting/auth'
  };

  // ── Broadcasting ───────────────────────────────────────────────
  broadcastChannel?: string;    // Must match GUNMA_BROADCAST_ADMIN_CHANNEL.
                                // Default: 'gunma-admin.chats'

  // ── Route Overrides ────────────────────────────────────────────
  routes?: {
    prefix?: string;            // Must match GUNMA_ADMIN_PREFIX. Default: '/api/admin/chat'
    sessions?: string;          // Full override for sessions path
    tickets?: string;           // Full override for tickets path
    stats?: string;             // Full override for stats path
    endSession?: string;        // Public session end path. Default: '/api/chat/sessions'
    csrfCookie?: string;        // Default: '/sanctum/csrf-cookie'
  };
}

5. Cross-Package Auth Flow Diagrams

Guest Chat Flow

Browser (no token)
    │
    ├─ POST /api/chat/sessions  { visitor_id: "v_123..." }
    │       │
    │  ResolveCustomer middleware
    │  → No Authorization header, no session cookie
    │  → auth()->user() = null
    │       │
    │  ChatController::createSession()
    │  → customer_id = null
    │  → Creates session with visitor_id only
    │       │
    │  ◄── { session: { id: "...", visitor_id: "v_123..." } }
    │
    ├─ POST /api/chat/sessions/{id}/messages  { message: "..." }
    │       │
    │  SSE stream response ─────────────────────────────────►
    │                                                  Browser renders

Authenticated Customer Chat Flow

Browser (has token in localStorage['tk'])
    │
    ├─ POST /api/chat/sessions
    │   Headers: { Authorization: "Bearer eyJ..." }
    │       │
    │  ResolveCustomer middleware
    │  → Finds Authorization header
    │  → Tries 'customer' guard → success
    │  → auth()->shouldUse('customer')
    │  → auth()->user() = Customer { id: 42, name: "Alice" }
    │       │
    │  ChatController::createSession()
    │  → customer_id = 42
    │  → customer_name auto-resolved = "Alice"
    │  → Searches existing session by customer_id first
    │  → Creates or upgrades session
    │       │
    │  ◄── { session: { id: "...", customer_id: 42, customer_name: "Alice" } }

Admin Dashboard Auth Flow (Sanctum SPA)

Admin Browser (session cookie from same domain)
    │
    ├─ GET /sanctum/csrf-cookie  (useMonitor initialization)
    │  ◄── Set-Cookie: XSRF-TOKEN=...
    │
    ├─ GET /api/admin/chat/sessions
    │   Headers: {
    │     Cookie: "laravel_session=...",
    │     X-XSRF-TOKEN: "..."
    │   }
    │       │
    │  admin_middleware: ['web', 'auth']
    │  → Session resolved → admin user identified
    │       │
    │  ◄── { data: [...sessions] }
    │
    ├─ WebSocket: CONNECT → pusher-server
    ├─ POST /api/broadcasting/auth
    │   Headers: { Cookie: "...", X-XSRF-TOKEN: "..." }
    │   → Authorizes 'gunma-admin.chats' private channel
    │  ◄── { auth: "..." }
    │
    └─ Private channel subscribed → real-time events flowing

6. Cookbook — Common Scenarios

Scenario A: Next.js SaaS — Sanctum SPA auth

// Both widget and dashboard on same domain as backend
// No tokens needed — session cookies flow automatically

// Widget (customer-facing):
<ChatWidget
  apiUrl={process.env.NEXT_PUBLIC_API_URL}
  brandName="My Store AI"
  pusher={{
    key: process.env.NEXT_PUBLIC_PUSHER_KEY!,
    wsHost: process.env.NEXT_PUBLIC_PUSHER_HOST,
    wsPort: Number(process.env.NEXT_PUBLIC_PUSHER_PORT),
  }}
/>

// Dashboard (admin):
<AgentDashboard
  apiUrl={process.env.NEXT_PUBLIC_API_URL}
  pusher={{
    key: process.env.NEXT_PUBLIC_PUSHER_KEY!,
    wsHost: process.env.NEXT_PUBLIC_PUSHER_HOST,
    wsPort: Number(process.env.NEXT_PUBLIC_PUSHER_PORT),
  }}
/>
# Backend .env
GUNMA_MIDDLEWARE=web|throttle:60,1
GUNMA_AUTH_GUARDS=sanctum|web
GUNMA_ADMIN_MIDDLEWARE=web|auth

Scenario B: Decoupled API — Laravel Passport customers

// Customer frontend (separate domain, Passport tokens)
const token = localStorage.getItem('passport_token');

<ChatWidget
  apiUrl="https://api.mystore.com"
  apiToken={token ?? undefined}
  routes={{ prefix: 'api/chat' }}
  pusher={{ key: 'abc123', cluster: 'eu', forceTLS: true }}
  cartUrl="https://api.mystore.com/customer/Frontend/Carts"
  storage={{ cookieKey: 'my_guest_cookie' }}
/>
# Backend .env
GUNMA_MIDDLEWARE=api|throttle:60,1
GUNMA_AUTH_GUARDS=customer|sanctum
GUNMA_ADMIN_MIDDLEWARE=api|auth:sanctum

Scenario C: Embedded widget in a non-Next.js app (Vanilla JS)

<script>
  window.__gunmaConfig = {
    apiUrl: 'https://api.mystore.com',
    apiToken: document.querySelector('meta[name="customer-token"]')?.content,
    brandName: 'Support Chat',
    pusher: { key: 'abc123', cluster: 'ap3' },
  };
</script>
// In your React mount point
<ChatWidget {...window.__gunmaConfig} />

Scenario D: Different backend route prefixes

# Backend .env — custom prefixes
GUNMA_ROUTE_PREFIX=api/v2/chat
GUNMA_ADMIN_PREFIX=api/v2/dashboard/chat
GUNMA_BROADCAST_ADMIN_CHANNEL=myapp.admin.chats
GUNMA_BROADCAST_CHAT_PREFIX=myapp.chat
// Widget must match GUNMA_ROUTE_PREFIX
<ChatWidget
  apiUrl="https://api.mystore.com"
  routes={{ prefix: 'api/v2/chat' }}
/>

// Dashboard must match GUNMA_ADMIN_PREFIX and GUNMA_BROADCAST_ADMIN_CHANNEL
<AgentDashboard
  apiUrl="https://api.mystore.com"
  broadcastChannel="myapp.admin.chats"
  routes={{ prefix: '/api/v2/dashboard/chat' }}
/>

7. Environment Variable Reference

Backend (.env)

Variable Default Description
GUNMA_OPENAI_API_KEY OpenAI API key
GUNMA_OPENAI_MODEL gpt-4o-mini Chat completion model
GUNMA_OPENAI_BASE_URL https://api.openai.com/v1 OpenAI-compatible base URL
GUNMA_OLLAMA_URL http://localhost:11435 Ollama embeddings server
GUNMA_QDRANT_URL http://localhost:6333 Qdrant vector database
GUNMA_ROUTE_PREFIX api/chat Public chat routes prefix
GUNMA_ADMIN_PREFIX api/admin/chat Admin routes prefix
GUNMA_MIDDLEWARE api Public route middleware (pipe-separated)
GUNMA_ADMIN_MIDDLEWARE web Admin route middleware (pipe-separated)
GUNMA_AUTH_GUARDS customer|sanctum|web Guard resolution order (pipe-separated)
GUNMA_BROADCAST_ADMIN_CHANNEL gunma-admin.chats Admin WebSocket channel name
GUNMA_BROADCAST_CHAT_PREFIX gunma-chat Per-session WebSocket channel prefix
GUNMA_RATE_LIMIT 30 Messages per minute per visitor
GUNMA_SESSION_TTL 86400 Chat session Redis TTL (seconds)
GUNMA_WEBSITE_URL Used for product link generation
GUNMA_CORS_ORIGINS * Allowed CORS origins (comma-separated)
GUNMA_MODEL_ORDER App\Models\Order Host app Order model class
GUNMA_MODEL_PRODUCT App\Models\Product Host app Product model class
GUNMA_MODEL_CART App\Models\Cart Host app Cart model class

Frontend (.env.local / build-time)

The frontend packages do not read process.env directly anymore. All configuration is passed via props to ensure framework-agnostic compatibility. Use your framework's own env loading:

// Next.js example
<ChatWidget
  apiUrl={process.env.NEXT_PUBLIC_API_URL!}
  pusher={{
    key:    process.env.NEXT_PUBLIC_PUSHER_KEY!,
    wsHost: process.env.NEXT_PUBLIC_PUSHER_HOST,
    wsPort: Number(process.env.NEXT_PUBLIC_PUSHER_PORT ?? 6001),
  }}
/>

Why no process.env in packages? Reading process.env inside a published npm package locks consumers to specific env key names and requires their bundler to inject them. Passing configuration via props makes the package framework-agnostic (Next.js, Vite, Remix, vanilla React).

统计信息

  • 总下载量: 37
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 3
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-04-21

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固