SDK 整合指南

@lens-os/sdk 的完整伺服器端與客戶端設定。

本指南涵蓋將 @lens-os/sdk 整合到你的應用程式中的完整伺服器端與客戶端設定。 若需要快速的 3 個檔案設定,請參閱快速開始指南。

伺服器端設定

createAgentHandler(config)

建立一個路由處理器,在伺服器端運行 SupervisorAgent 並透過 SSE 串流事件給客戶端。

AgentHandlerConfig
interface AgentHandlerConfig {
  /** Lens OS API Key(必填,僅限伺服器端)*/
  apiKey: string;

  /** OpenAI API Key(必填,僅限伺服器端)*/
  openaiKey: string;

  /** Lens OS API 的 Base URL(預設:https://osapi.ask-lens.ai)*/
  baseUrl?: string;

  /** 預先建立的 LensClient 實例,用於共享設定快取 */
  client?: LensClient;

  /** LLM 模型(預設:gpt-4o)*/
  model?: string;

  /** Agent 迴圈最大輪數(預設:10)*/
  maxTurns?: number;

  /** 語言(預設:zh-TW)*/
  language?: 'zh-TW' | 'en-US';

  /** 自訂工具執行器(參見「工具執行器」頁面)*/
  toolExecutors?: Record<string, ToolExecutorFunction | ToolExecutorConfig>;

  /** Action Request 逾時時間,單位毫秒(預設:30000)*/
  actionTimeout?: number;

  /** LLM 追蹤回呼(用於日誌記錄、分析)*/
  onTrace?: (trace: LLMTrace) => void;
}

回傳值

handler
const handler = createAgentHandler(config);

handler.POST             // (req: Request) => Promise<Response>  — 路由處理器
handler._pendingActions  // PendingActionStore — 傳給 createActionResultHandler

加入身份驗證

Handler 預期 Request Body 為 { message, sessionId, userId? }。你可以包裝 handler 來注入身份驗證:

src/app/api/agent/chat/route.ts
import { createAgentHandler } from '@lens-os/sdk/server';
import { auth } from '@/auth'; // 你的驗證函式庫

export const handler = createAgentHandler({
  apiKey: process.env.LENS_API_KEY!,
  openaiKey: process.env.OPENAI_API_KEY!,
  model: process.env.OPENAI_MODEL || 'gpt-4o',
  maxTurns: 10,
  toolExecutors: buildToolExecutors(),
});

export async function POST(req: Request) {
  // 1. 身份驗證
  const session = await auth();
  if (!session?.user) {
    return new Response(JSON.stringify({ error: 'Unauthorized' }), {
      status: 401,
      headers: { 'Content-Type': 'application/json' },
    });
  }

  // 2. 將 userId 注入到 Request Body 中
  const userId = session.user.id;
  const body = await req.json();
  const newReq = new Request(req.url, {
    method: 'POST',
    headers: req.headers,
    body: JSON.stringify({ ...body, userId }),
  });

  // 3. 委派給 SDK handler
  return handler.POST(newReq);
}

createActionResultHandler(pendingActions)

建立用於接收客戶端 DOM 動作結果的路由處理器。

src/app/api/agent/chat/action-result/route.ts
import { createActionResultHandler } from '@lens-os/sdk/server';
import { handler } from '../route';

export const { POST } = createActionResultHandler(handler._pendingActions);

Note

action-result 端點必須與 agent handler 共用同一個 _pendingActions store。這就是為什麼要從 chat route import handler

Request Body 格式

AgentRequestBody
interface AgentRequestBody {
  /** 用戶的訊息文字 */
  message: string;
  /** Session ID(由客戶端產生)*/
  sessionId: string;
  /** 用戶 ID(可選,可在伺服器端注入)*/
  userId?: string;
  /** 當前頁面狀態與螢幕截圖(可選)*/
  pageState?: PageState;
  /** 當前頁面 URL(可選)*/
  currentUrl?: string;
}

客戶端設定

useLensAgent(options)

React Hook,透過 SSE 與伺服器端的 createAgentHandler 通訊。客戶端不需要 API 金鑰。

UseLensAgentOptions
interface UseLensAgentOptions {
  /** 伺服器端點 URL(例如 '/api/agent/chat')*/
  endpoint: string;

  /** Action result 端點(預設:endpoint + '/action-result')*/
  actionResultEndpoint?: string;

  /** 用戶 ID(可選)*/
  userId?: string;

  /** 事件回呼 — 每個 SSE 事件都會觸發 */
  onEvent?: (event: SSEEvent) => void;

  /** 自訂請求標頭(例如 auth token)*/
  headers?: Record<string, string> | (() => Record<string, string>);

  /**
   * 頁面狀態提供者 — 在每次發送訊息前呼叫,
   * 擷取當前頁面狀態(螢幕截圖、markdown、元素)。
   */
  getPageState?: () => Promise<PageState>;

  /**
   * 自訂客戶端 DOM 動作處理器。
   * 若未提供,使用內建的 WebUseTool。
   */
  onActionRequest?: (action: string, params: Record<string, any>) => Promise<ToolResult>;
}

回傳值

UseLensAgentReturn
interface UseLensAgentReturn {
  /** 對話訊息歷史 */
  messages: Message[];

  /** Agent 是否正在處理中 */
  isLoading: boolean;

  /** 當前 Session ID */
  sessionId: string;

  /** 最後一次錯誤(如有)*/
  error: Error | null;

  /** 發送訊息給 Agent */
  sendMessage: (message: string, context?: {
    pageState?: PageState;
    currentUrl?: string;
  }) => Promise<void>;

  /** 中止當前 Agent 執行 */
  abort: () => void;

  /** 清除對話訊息 */
  clearMessages: () => void;

  /** 開始新 Session(清除訊息 + 產生新 sessionId)*/
  newSession: () => void;
}

完整使用範例

src/components/AgentPanel.tsx
'use client';
import { useState, useRef, useEffect } from 'react';
import { useLensAgent } from '@lens-os/sdk/react';
import type { SSEEvent } from '@lens-os/sdk';

interface ToolCard {
  id: string;
  name: string;
  parameters: Record<string, any>;
  result?: any;
  status: 'pending' | 'completed' | 'error';
}

export default function AgentPanel({ userId }: { userId: string }) {
  const [input, setInput] = useState('');
  const [toolCards, setToolCards] = useState<ToolCard[]>([]);
  const messagesEndRef = useRef<HTMLDivElement>(null);

  const {
    messages,
    isLoading,
    error,
    sendMessage,
    abort,
    newSession,
  } = useLensAgent({
    endpoint: '/api/agent/chat',
    userId,
    onEvent: (event: SSEEvent) => {
      if (event.type === 'tool_call') {
        setToolCards(prev => [...prev, {
          id: `tool-${Date.now()}`,
          name: event.name,
          parameters: event.parameters,
          status: 'pending',
        }]);
      } else if (event.type === 'tool_result') {
        setToolCards(prev => prev.map(card =>
          card.name === event.name && card.status === 'pending'
            ? { ...card, result: event.result, status: event.result.success ? 'completed' : 'error' }
            : card
        ));
      }
    },
  });

  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);

  const handleSend = async () => {
    if (!input.trim() || isLoading) return;
    const msg = input;
    setInput('');
    setToolCards([]);
    await sendMessage(msg);
  };

  return (
    <div className="agent-panel">
      <div className="messages">
        {messages.map((msg, i) => (
          <div key={i} className={`message ${msg.role}`}>
            {typeof msg.content === 'string' ? msg.content : ''}
          </div>
        ))}
        {toolCards.map(card => (
          <div key={card.id} className={`tool-card ${card.status}`}>
            <span>{card.name}</span>
            {card.status === 'pending' && <span>載入中...</span>}
            {card.status === 'completed' && <span>完成</span>}
            {card.status === 'error' && <span>錯誤</span>}
          </div>
        ))}
        <div ref={messagesEndRef} />
      </div>
      {error && <div className="error">{error.message}</div>}
      <div className="input-area">
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && handleSend()}
          placeholder="輸入訊息..."
          disabled={isLoading}
        />
        {isLoading ? (
          <button onClick={abort}>停止</button>
        ) : (
          <button onClick={handleSend}>發送</button>
        )}
        <button onClick={newSession}>新對話</button>
      </div>
    </div>
  );
}

useChat — 簡化別名

useChatuseLensAgent 的薄包裝,方法名稱更簡潔:

useChat
import { useChat } from '@lens-os/sdk/react';

const { messages, isLoading, error, send, stop, clear, reset } = useChat({
  endpoint: '/api/agent/chat',
});

// 對應關係:
// send → sendMessage
// stop → abort
// clear → clearMessages
// reset → newSession

完整 Next.js 範例

包含身份驗證、工具和 Session 管理的完整範例。

檔案結構

專案結構
src/
├── app/
│   └── api/
│       └── agent/
│           ├── chat/
│           │   ├── route.ts              # Agent handler
│           │   └── action-result/
│           │       └── route.ts          # Action result handler
│           └── sessions/
│               ├── route.ts              # 列出 sessions
│               └── [id]/
│                   └── route.ts          # 取得 session 訊息
├── lib/
│   └── tools.ts                          # 工具執行器定義
└── components/
    └── AgentPanel.tsx                    # 聊天 UI 元件

工具定義

src/lib/tools.ts
import type { ToolExecutorConfig, SessionContext } from '@lens-os/sdk';

const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';

export function buildToolExecutors(): Record<string, ToolExecutorConfig> {
  return {
    product_search: {
      description: '搜尋商品資料庫,找到相關商品',
      whenToUse: '當用戶想尋找商品、比較商品或瀏覽目錄時',
      schema: {
        query: { type: 'string', required: true, description: '搜尋關鍵字' },
        topK: { type: 'number', required: false, default: 10, description: '回傳數量' },
      },
      output: '包含 title、price、imageUrl、description 的商品陣列',
      execute: async (params) => {
        try {
          const res = await fetch(`${BASE_URL}/api/products/search`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(params),
          });
          return await res.json();
        } catch (err) {
          return { success: false, error: err instanceof Error ? err.message : '未知錯誤' };
        }
      },
    },

    user_orders: {
      description: '查詢或更新用戶訂單',
      whenToUse: '當用戶詢問訂單狀態、購買紀錄或想修改訂單時',
      schema: {
        action: { type: 'string', required: true, description: '"get" 或 "update"' },
        orderId: { type: 'string', required: false, description: '特定訂單編號' },
      },
      output: '訂單詳情,包含狀態、商品和金額',
      execute: async (params, context?: SessionContext) => {
        try {
          const res = await fetch(`${BASE_URL}/api/orders`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ ...params, userId: context?.userId }),
          });
          return await res.json();
        } catch (err) {
          return { success: false, error: err instanceof Error ? err.message : '未知錯誤' };
        }
      },
    },
  };
}

Tip

若要完整了解工具執行器,請參閱專門的工具執行器頁面。