Compare commits

..

14 Commits

Author SHA1 Message Date
Simon b4ebb2b98f chore(version): bump version to 1.5.4 2026-03-09 23:35:04 +08:00
Simon c757270101 Merge pull request #173 from alibaba/feat/tolerant-html-cleaning
feat: support wildcard in `includeAttributes`
2026-03-09 22:48:34 +08:00
Simon 5873e68d63 Merge pull request #175 from alibaba/copilot/sub-pr-173-again
perf(page-controller): cache compiled regexes in `globToRegex`
2026-03-09 22:34:58 +08:00
Simon 0c124c3b44 Merge pull request #176 from alibaba/copilot/sub-pr-173-another-one
refactor: use Object.keys() instead of for...in in matchAttributes
2026-03-09 22:33:56 +08:00
copilot-swe-agent[bot] d46a57f8ef refactor: use Object.keys() instead of for...in in matchAttributes
Co-authored-by: gaomeng1900 <10131203+gaomeng1900@users.noreply.github.com>
2026-03-09 14:32:20 +00:00
Simon 153fa23a71 docs: update model list; give example for cookie auth 2026-03-09 22:31:35 +08:00
copilot-swe-agent[bot] 5852054e3a perf: cache compiled regexes in globToRegex to avoid repeated compilation
Co-authored-by: gaomeng1900 <10131203+gaomeng1900@users.noreply.github.com>
2026-03-09 14:29:56 +00:00
copilot-swe-agent[bot] 5988d4ba24 Initial plan 2026-03-09 14:29:10 +00:00
copilot-swe-agent[bot] 0b6a698f6b Initial plan 2026-03-09 14:26:53 +00:00
Simon 01db520881 feat: support wildcard in includeAttributes 2026-03-09 22:02:35 +08:00
Simon 30b9956c4f Merge pull request #172 from alibaba/fix/contenteditable
fix: contenteditable
2026-03-09 19:19:23 +08:00
Simon 4b37a3e538 feat(controller): add contenteditable to DEFAULT_INCLUDE_ATTRIBUTES 2026-03-09 19:15:51 +08:00
Simon 928d8d2fb3 fix: allow ContentEditable in inputTextElement; clean up code 2026-03-09 19:05:32 +08:00
Simon 8ece20f54b chore(ext): build script 2026-03-09 17:29:45 +08:00
16 changed files with 166 additions and 137 deletions
+2 -2
View File
@@ -49,8 +49,8 @@ Fastest way to try PageAgent with our free Demo LLM:
| Mirrors | URL |
| ------- | ---------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.5.3/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.5.3/files/dist/iife/page-agent.demo.js |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.5.4/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.5.4/files/dist/iife/page-agent.demo.js |
> **⚠️ For technical evaluation only.** This demo CDN uses our free [testing LLM API](https://alibaba.github.io/page-agent/docs/features/models#free-testing-api). By using it, you agree to its [terms](https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md).
+2 -2
View File
@@ -49,8 +49,8 @@
| Mirrors | URL |
| ------- | ---------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.5.3/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.5.3/files/dist/iife/page-agent.demo.js |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.5.4/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.5.4/files/dist/iife/page-agent.demo.js |
> **⚠️ 仅用于技术评估。** 该 Demo CDN 使用了免费的[测试 LLM API](https://alibaba.github.io/page-agent/docs/features/models#free-testing-api),使用即表示您同意其[条款](https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md)。
+18 -18
View File
@@ -1,12 +1,12 @@
{
"name": "root",
"version": "1.5.3",
"version": "1.5.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "root",
"version": "1.5.3",
"version": "1.5.4",
"license": "MIT",
"workspaces": [
"packages/page-controller",
@@ -11113,11 +11113,11 @@
},
"packages/core": {
"name": "@page-agent/core",
"version": "1.5.3",
"version": "1.5.4",
"license": "MIT",
"dependencies": {
"@page-agent/llms": "1.5.3",
"@page-agent/page-controller": "1.5.3",
"@page-agent/llms": "1.5.4",
"@page-agent/page-controller": "1.5.4",
"chalk": "^5.6.2"
},
"devDependencies": {
@@ -11132,10 +11132,10 @@
"version": "0.1.16",
"hasInstallScript": true,
"dependencies": {
"@page-agent/core": "1.5.3",
"@page-agent/llms": "1.5.3",
"@page-agent/page-controller": "1.5.3",
"@page-agent/ui": "1.5.3",
"@page-agent/core": "1.5.4",
"@page-agent/llms": "1.5.4",
"@page-agent/page-controller": "1.5.4",
"@page-agent/ui": "1.5.4",
"ai-motion": "^0.4.8",
"chalk": "^5.6.2"
},
@@ -11173,7 +11173,7 @@
},
"packages/llms": {
"name": "@page-agent/llms",
"version": "1.5.3",
"version": "1.5.4",
"license": "MIT",
"dependencies": {
"chalk": "^5.6.2"
@@ -11186,13 +11186,13 @@
}
},
"packages/page-agent": {
"version": "1.5.3",
"version": "1.5.4",
"license": "MIT",
"dependencies": {
"@page-agent/core": "1.5.3",
"@page-agent/llms": "1.5.3",
"@page-agent/page-controller": "1.5.3",
"@page-agent/ui": "1.5.3",
"@page-agent/core": "1.5.4",
"@page-agent/llms": "1.5.4",
"@page-agent/page-controller": "1.5.4",
"@page-agent/ui": "1.5.4",
"chalk": "^5.6.2"
},
"devDependencies": {
@@ -11204,7 +11204,7 @@
},
"packages/page-controller": {
"name": "@page-agent/page-controller",
"version": "1.5.3",
"version": "1.5.4",
"license": "MIT",
"dependencies": {
"ai-motion": "^0.4.8"
@@ -11212,12 +11212,12 @@
},
"packages/ui": {
"name": "@page-agent/ui",
"version": "1.5.3",
"version": "1.5.4",
"license": "MIT"
},
"packages/website": {
"name": "@page-agent/website",
"version": "1.5.3",
"version": "1.5.4",
"devDependencies": {
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-separator": "^1.1.8",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "root",
"private": true,
"version": "1.5.3",
"version": "1.5.4",
"type": "module",
"workspaces": [
"packages/page-controller",
@@ -30,7 +30,7 @@
"build": "npm run build:libs && npm run build:website",
"build:libs": "npm run build --workspaces --if-present",
"build:website": "npm run build:website --workspace=@page-agent/website",
"build:ext": "npm run build:libs && npm run build:ext -w @page-agent/ext",
"build:ext": "npm run build:libs && npm run zip -w @page-agent/ext",
"version": "node scripts/sync-version.js",
"version:ext": "node scripts/ext-version.js",
"lint": "eslint .",
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@page-agent/core",
"private": false,
"version": "1.5.3",
"version": "1.5.4",
"type": "module",
"main": "./dist/esm/page-agent-core.js",
"module": "./dist/esm/page-agent-core.js",
@@ -44,8 +44,8 @@
},
"dependencies": {
"chalk": "^5.6.2",
"@page-agent/llms": "1.5.3",
"@page-agent/page-controller": "1.5.3"
"@page-agent/llms": "1.5.4",
"@page-agent/page-controller": "1.5.4"
},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
+4 -4
View File
@@ -38,10 +38,10 @@
"wxt": "^0.20.18"
},
"dependencies": {
"@page-agent/core": "1.5.3",
"@page-agent/llms": "1.5.3",
"@page-agent/page-controller": "1.5.3",
"@page-agent/ui": "1.5.3",
"@page-agent/core": "1.5.4",
"@page-agent/llms": "1.5.4",
"@page-agent/page-controller": "1.5.4",
"@page-agent/ui": "1.5.4",
"ai-motion": "^0.4.8",
"chalk": "^5.6.2"
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@page-agent/llms",
"version": "1.5.3",
"version": "1.5.4",
"type": "module",
"main": "./dist/lib/page-agent-llms.js",
"module": "./dist/lib/page-agent-llms.js",
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "page-agent",
"private": false,
"version": "1.5.3",
"version": "1.5.4",
"type": "module",
"main": "./dist/esm/page-agent.js",
"module": "./dist/esm/page-agent.js",
@@ -44,10 +44,10 @@
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
},
"dependencies": {
"@page-agent/core": "1.5.3",
"@page-agent/llms": "1.5.3",
"@page-agent/page-controller": "1.5.3",
"@page-agent/ui": "1.5.3",
"@page-agent/core": "1.5.4",
"@page-agent/llms": "1.5.4",
"@page-agent/page-controller": "1.5.4",
"@page-agent/ui": "1.5.4",
"chalk": "^5.6.2"
},
"peerDependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@page-agent/page-controller",
"version": "1.5.3",
"version": "1.5.4",
"type": "module",
"main": "./dist/lib/page-controller.js",
"module": "./dist/lib/page-controller.js",
+12 -24
View File
@@ -101,41 +101,29 @@ const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(
'value'
)!.set!
/**
* create a synthetic keyboard event
* with key keycode code
*/
export async function createSyntheticInputEvent(elem: HTMLElement, key: string) {
elem.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, cancelable: true, key }))
await waitFor(0.01)
if (elem instanceof HTMLInputElement || elem instanceof HTMLTextAreaElement) {
elem.dispatchEvent(new Event('beforeinput', { bubbles: true }))
await waitFor(0.01)
elem.dispatchEvent(new Event('input', { bubbles: true }))
await waitFor(0.01)
}
elem.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, cancelable: true, key }))
}
export async function inputTextElement(element: HTMLElement, text: string) {
if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
throw new Error('Element is not an input or textarea')
const isContentEditable = element.isContentEditable
if (
!(element instanceof HTMLInputElement) &&
!(element instanceof HTMLTextAreaElement) &&
!isContentEditable
) {
throw new Error('Element is not an input, textarea, or contenteditable')
}
await clickElement(element)
if (element instanceof HTMLTextAreaElement) {
if (isContentEditable) {
element.innerText = text
} else if (element instanceof HTMLTextAreaElement) {
nativeTextAreaValueSetter.call(element, text)
} else {
nativeInputValueSetter.call(element, text)
}
const inputEvent = new Event('input', { bubbles: true })
element.dispatchEvent(inputEvent)
element.dispatchEvent(new Event('input', { bubbles: true }))
await waitFor(0.1) // Wait to ensure input event processing completes
await waitFor(0.1)
blurLastClickedElement()
}
+45 -13
View File
@@ -74,6 +74,43 @@ export function getFlatTree(config: DomConfig): FlatDomTree {
return elements
}
const globRegexCache = new Map<string, RegExp>()
function globToRegex(pattern: string): RegExp {
let regex = globRegexCache.get(pattern)
if (!regex) {
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&')
regex = new RegExp(`^${escaped.replace(/\*/g, '.*')}$`)
globRegexCache.set(pattern, regex)
}
return regex
}
function matchAttributes(
attrs: Record<string, string>,
patterns: string[]
): Record<string, string> {
const result: Record<string, string> = {}
for (const pattern of patterns) {
if (pattern.includes('*')) {
const regex = globToRegex(pattern)
for (const key of Object.keys(attrs)) {
if (regex.test(key) && attrs[key].trim()) {
result[key] = attrs[key].trim()
}
}
} else {
const value = attrs[pattern]
if (value && value.trim()) {
result[pattern] = value.trim()
}
}
}
return result
}
/**
* elementsToString 内部使用的类型
*/
@@ -140,10 +177,13 @@ export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: stri
// for jump check
'target',
// absolute 定位的下拉菜单
// absolute position dropdown menu
'aria-haspopup',
'aria-controls',
'aria-owns',
// content editable
'contenteditable',
]
const includeAttrs = [...(includeAttributes || []), ...DEFAULT_INCLUDE_ATTRIBUTES]
@@ -245,23 +285,15 @@ export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: stri
let attributesHtmlStr = ''
if (includeAttrs.length > 0 && node.attributes) {
const attributesToInclude: Record<string, string> = {}
// Filter attributes
for (const key of includeAttrs) {
const value = node.attributes[key]
if (value && value.trim() !== '') {
attributesToInclude[key] = value.trim()
}
}
const attributesToInclude = matchAttributes(node.attributes, includeAttrs)
// Remove duplicate values (for attributes longer than 5 chars)
const orderedKeys = includeAttrs.filter((key) => key in attributesToInclude)
if (orderedKeys.length > 1) {
const keys = Object.keys(attributesToInclude)
if (keys.length > 1) {
const keysToRemove = new Set<string>()
const seenValues: Record<string, string> = {}
for (const key of orderedKeys) {
for (const key of keys) {
const value = attributesToInclude[key]
if (value.length > 5) {
if (value in seenValues) {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@page-agent/ui",
"version": "1.5.3",
"version": "1.5.4",
"type": "module",
"main": "./dist/lib/page-agent-ui.js",
"module": "./dist/lib/page-agent-ui.js",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@page-agent/website",
"private": true,
"version": "1.5.3",
"version": "1.5.4",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
+2 -2
View File
@@ -1,8 +1,8 @@
// Demo build (auto-init with demo LLM, for quick testing)
export const CDN_DEMO_URL =
'https://cdn.jsdelivr.net/npm/page-agent@1.5.3/dist/iife/page-agent.demo.js'
'https://cdn.jsdelivr.net/npm/page-agent@1.5.4/dist/iife/page-agent.demo.js'
export const CDN_DEMO_CN_URL =
'https://registry.npmmirror.com/page-agent/1.5.3/files/dist/iife/page-agent.demo.js'
'https://registry.npmmirror.com/page-agent/1.5.4/files/dist/iife/page-agent.demo.js'
// Demo LLM for website testing (homepage quick trial uses flash)
export const DEMO_MODEL = 'qwen3.5-flash'
@@ -105,8 +105,8 @@ const agent = new PageAgentCore({
name: 'includeAttributes',
type: 'string[]',
description: isZh
? '在 DOM 提取中包含的额外 HTML 属性(如 data-testid)。默认已包含常见属性如 role, aria-label 等。'
: 'Additional HTML attributes to include in DOM extraction (e.g. data-testid). Common attributes like role, aria-label are included by default.',
? '在 DOM 提取中包含的额外 HTML 属性。支持通配符 *(如 data-* 匹配所有 data- 开头的属性)。默认已包含常见属性如 role, aria-label 等。'
: 'Additional HTML attributes to include in DOM extraction. Supports wildcard * (e.g. data-* matches all data- prefixed attributes). Common attributes like role, aria-label are included by default.',
},
]}
/>
@@ -23,7 +23,7 @@ const MODEL_GROUPS: Record<string, string[]> = {
'qwen-3-plus',
'qwen3:14b (ollama)',
],
OpenAI: ['gpt-5.2', 'gpt-5.1', 'gpt-5', 'gpt-5-mini', 'gpt-4.1', 'gpt-4.1-mini'],
OpenAI: ['gpt-5.4', 'gpt-5.2', 'gpt-5.1', 'gpt-5', 'gpt-5-mini', 'gpt-4.1', 'gpt-4.1-mini'],
DeepSeek: ['deepseek-3.2'],
Google: ['gemini-3-pro', 'gemini-3-flash', 'gemini-2.5'],
Anthropic: [
@@ -68,11 +68,6 @@ export default function Models() {
<Heading id="tested-models" className="text-2xl font-semibold mb-3">
{isZh ? '已测试模型' : 'Tested Models'}
</Heading>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
{isZh
? '推荐使用 ToolCall 能力强的轻量级模型。'
: 'Recommended: Fast, lightweight models with strong ToolCall capabilities.'}
</p>
<div className="bg-linear-to-br from-emerald-50 to-cyan-50 dark:from-emerald-950/30 dark:to-cyan-950/30 rounded-xl p-6 border border-emerald-200/50 dark:border-emerald-800/50">
<div className="grid grid-cols-[5rem_1fr] gap-x-3 gap-y-3 items-start">
{Object.entries(MODEL_GROUPS).map(([brand, models]) => (
@@ -96,6 +91,11 @@ export default function Models() {
<h2 className="text-2xl font-semibold mb-4">{isZh ? '提示' : 'Tips'}</h2>
<div className="p-4 bg-blue-50 dark:bg-blue-950/20 rounded-lg border border-blue-200 dark:border-blue-800">
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5">
<li>
{isZh
? '⭐ 推荐使用 ToolCall 能力强的轻量级模型'
: '⭐ Recommended: Fast, lightweight models with strong ToolCall capabilities'}
</li>
<li>
{isZh
? 'ToolCall 能力较弱的模型可能返回错误的格式,常见错误能够自动恢复,建议设置较高的 temperature'
@@ -110,39 +110,6 @@ export default function Models() {
</div>
</section>
{/* Security Section */}
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">
{isZh ? '🔐 生产环境鉴权建议' : '🔐 Production Authentication'}
</h2>
<div className="bg-yellow-50 dark:bg-yellow-950/20 border-l-4 border-yellow-500 p-5 rounded-r-lg mb-4">
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200">
{isZh
? '⚠️ 永远不要把真实的 LLM API Key 发布到前端代码'
: '⚠️ Never commit real LLM API Keys to your frontend code'}
</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900/30 rounded-lg p-5 border border-gray-200 dark:border-gray-800">
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">
{isZh ? '后端代理转发' : 'Backend Proxy Pattern'}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
{isZh
? '在后端搭建一个 LLM 流量转发接口,该接口使用与你网站上其他接口相同的鉴权方式,例如:'
: 'Set up a backend LLM proxy endpoint that uses the same authentication method as other APIs in your website, such as:'}
</p>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li>{isZh ? '• Session/Cookie 会话认证' : '• Session/Cookie-based authentication'}</li>
<li>
{isZh ? '• OIDC (OpenID Connect) 单点登录' : '• OIDC (OpenID Connect) single sign-on'}
</li>
<li>
{isZh ? '• 临时 Access Key 或 JWT Token' : '• Temporary Access Key or JWT Token'}
</li>
</ul>
</div>
</section>
{/* Configuration Section */}
<section className="mb-10">
<Heading id="configuration">{isZh ? '配置方式' : 'Configuration'}</Heading>
@@ -170,9 +137,24 @@ const pageAgent = new PageAgent({
<Heading id="free-testing-api">{isZh ? '免费测试接口' : 'Free Testing API'}</Heading>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
{isZh
? '以下免费测试接口仅供 PageAgent.js 和 PageAgent Extension 的技术评估使用。有速率限制,可能随时变更。请勿用于生产环境。'
: 'The following free testing endpoints are provided for technical evaluation of PageAgent.js and PageAgent Extension only. Rate-limited, subject to change. Not for production use.'}
? '以下免费测试接口仅供 PageAgent.js 和 PageAgent Extension 的技术评估和测试使用。'
: 'The following free testing endpoint is provided for testing and technical evaluation.'}
</p>
<div className="my-4 p-4 bg-amber-50 dark:bg-amber-950/20 rounded-lg border border-amber-200 dark:border-amber-800">
<p className="text-xs text-gray-600 dark:text-gray-400">
{isZh
? '⚠️ 仅供技术评估和研发用途,禁止用于生产环境。数据通过中国大陆服务器处理。请勿输入任何个人身份信息或敏感数据。使用即表示您同意'
: '⚠️ Strictly for technical evaluation and R&D only. Data is processed via servers in Mainland China. Do not input any PII or sensitive data. By using this API you agree to the'}{' '}
<a
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md#2-testing-api-and-demo-disclaimer--terms-of-use"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline"
>
{isZh ? '使用条款' : 'Terms of Use'}
</a>
</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900/30 rounded-lg p-5 border border-gray-200 dark:border-gray-800">
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">
Qwen (Alibaba Cloud China)
@@ -198,21 +180,6 @@ LLM_MODEL_NAME="qwen3.5-plus"
LLM_API_KEY="NA"`}
/>
</div>
<div className="mt-4 p-4 bg-amber-50 dark:bg-amber-950/20 rounded-lg border border-amber-200 dark:border-amber-800">
<p className="text-xs text-gray-600 dark:text-gray-400">
{isZh
? '⚠️ 仅供技术评估和研发用途,禁止用于生产环境。数据通过中国大陆服务器处理。请勿输入任何个人身份信息或敏感数据。使用即表示您同意'
: '⚠️ Strictly for technical evaluation and R&D only. Data is processed via servers in Mainland China. Do not input any PII or sensitive data. By using this API you agree to the'}{' '}
<a
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md#2-testing-api-and-demo-disclaimer--terms-of-use"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline"
>
{isZh ? '使用条款' : 'Terms of Use'}
</a>
</p>
</div>
</section>
{/* Ollama Section */}
@@ -277,6 +244,48 @@ LLM_MODEL_NAME="qwen3:14b"`}
</div>
</div>
</section>
{/* Production Authentication */}
<section className="mb-10">
<Heading id="production-authentication" className="text-2xl font-semibold mb-4">
{isZh ? '🔐 生产环境鉴权' : '🔐 Production Authentication'}
</Heading>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
{isZh
? '如果你只是将它用作个人助手,可以直接连接你的 LLM 服务。'
: 'If you only use it as a personal assistant, you can connect to your LLM service directly.'}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
{isZh ? (
<>
Web LLM 使{' '}
<code>customFetch</code> Cookie
</>
) : (
<>
If you plan to integrate it into your web app, it's better to have a backend proxy for
the LLM and use <code>customFetch</code> to authenticate the request with cookies or
other methods:
</>
)}
</p>
<CodeEditor
code={`const agent = new PageAgent({
baseURL: '/api/llm-proxy',
apiKey: 'NA',
model: 'gpt-5.1',
customFetch: (url, init) =>
fetch(url, { ...init, credentials: 'include' }),
});`}
/>
<div className="mt-4 bg-yellow-50 dark:bg-yellow-950/20 border-l-4 border-yellow-500 p-4 rounded-r-lg">
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200">
{isZh
? ' LLM API Key '
: ' NEVER commit real LLM API keys to your frontend code'}
</p>
</div>
</section>
</div>
)
}