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 — 簡化別名
useChat 是 useLensAgent 的薄包裝,方法名稱更簡潔:
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
若要完整了解工具執行器,請參閱專門的工具執行器頁面。