@action-x/ad-sdk 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +158 -586
- package/dist/index.cjs +6 -6
- package/dist/index.d.ts +30 -13
- package/dist/index.js +163 -149
- package/dist/index.umd.js +6 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,678 +1,250 @@
|
|
|
1
1
|
# @action-x/ad-sdk
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
支持原生 JS、Vue、Angular、Svelte、React 等所有框架。
|
|
3
|
+
An ad SDK for AI conversation experiences, with zero framework dependencies. Works with Vanilla JS, Vue, React, Angular, Svelte, and more.
|
|
5
4
|
|
|
6
5
|
---
|
|
7
6
|
|
|
8
|
-
##
|
|
9
|
-
|
|
10
|
-
- [特性](#特性)
|
|
11
|
-
- [安装](#安装)
|
|
12
|
-
- [快速上手](#快速上手)
|
|
13
|
-
- [核心概念](#核心概念)
|
|
14
|
-
- [API 参考](#api-参考)
|
|
15
|
-
- [AdManager](#admanager)
|
|
16
|
-
- [fetchAds / adaptAdToKoahAd](#fetchads--adaptadtokoahad)
|
|
17
|
-
- [HTMLRenderer](#htmlrenderer)
|
|
18
|
-
- [DOMRenderer](#domrenderer)
|
|
19
|
-
- [Analytics](#analytics)
|
|
20
|
-
- [ViewabilityTracker](#viewabilitytracker)
|
|
21
|
-
- [ClientInfo](#clientinfo)
|
|
22
|
-
- [EventEmitter](#eventemitter)
|
|
23
|
-
- [类型定义](#类型定义)
|
|
24
|
-
- [CSS 样式](#css-样式)
|
|
25
|
-
- [广告格式说明](#广告格式说明)
|
|
26
|
-
- [高级用法](#高级用法)
|
|
27
|
-
- [依赖 API 端点](#依赖-api-端点)
|
|
28
|
-
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
## 特性
|
|
32
|
-
|
|
33
|
-
- **零运行时依赖** — 无 React、无 swr、无任何第三方运行时库
|
|
34
|
-
- **框架无关** — 兼容原生 JS、Vue、React、Angular、Svelte 等所有框架
|
|
35
|
-
- **OpenRTB 兼容** — 自动采集设备信息,符合 OpenRTB 2.5/2.6 规范
|
|
36
|
-
- **完整追踪** — IAB/MRC 标准可视性追踪 + 曝光/点击事件上报,自动重试
|
|
37
|
-
- **TypeScript 优先** — 完整类型声明,开箱即用,无需额外 `@types`
|
|
38
|
-
- **多格式输出** — ESM / CommonJS / UMD,适配所有构建工具和 CDN
|
|
39
|
-
|
|
40
|
-
---
|
|
41
|
-
|
|
42
|
-
## 安装
|
|
7
|
+
## Installation
|
|
43
8
|
|
|
44
9
|
```bash
|
|
45
10
|
npm install @action-x/ad-sdk
|
|
46
|
-
#
|
|
11
|
+
# or
|
|
47
12
|
yarn add @action-x/ad-sdk
|
|
48
|
-
#
|
|
13
|
+
# or
|
|
49
14
|
pnpm add @action-x/ad-sdk
|
|
50
15
|
```
|
|
51
16
|
|
|
52
|
-
|
|
17
|
+
**Import styles (required):**
|
|
53
18
|
|
|
54
19
|
```js
|
|
55
|
-
// 在你的入口文件或组件中引入
|
|
56
20
|
import '@action-x/ad-sdk/dist/style.css';
|
|
57
21
|
```
|
|
58
22
|
|
|
59
|
-
或在 HTML 中:
|
|
60
|
-
|
|
61
|
-
```html
|
|
62
|
-
<link rel="stylesheet" href="node_modules/@action-x/ad-sdk/dist/style.css" />
|
|
63
|
-
```
|
|
64
|
-
|
|
65
23
|
---
|
|
66
24
|
|
|
67
|
-
##
|
|
25
|
+
## AdCard Component
|
|
68
26
|
|
|
69
|
-
|
|
27
|
+
Below are AdCard wrapper examples for different frameworks. The component takes `query` and `response` from the AI conversation, then automatically requests and renders an ad card after the response is complete.
|
|
70
28
|
|
|
71
|
-
|
|
29
|
+
### Vue 3
|
|
30
|
+
|
|
31
|
+
```vue
|
|
32
|
+
<!-- AdCard.vue -->
|
|
33
|
+
<template>
|
|
34
|
+
<div ref="adEl" />
|
|
35
|
+
</template>
|
|
72
36
|
|
|
73
|
-
|
|
37
|
+
<script setup>
|
|
38
|
+
import { ref, onMounted, onUnmounted } from 'vue';
|
|
74
39
|
import { AdManager } from '@action-x/ad-sdk';
|
|
75
40
|
import '@action-x/ad-sdk/dist/style.css';
|
|
76
41
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
slots: [
|
|
81
|
-
{
|
|
82
|
-
slotId: 'banner-top',
|
|
83
|
-
slotName: '顶部横幅',
|
|
84
|
-
format: 'action_card',
|
|
85
|
-
variant: 'horizontal',
|
|
86
|
-
size: { width: 400, height: 120, aspectRatio: '3.3:1' },
|
|
87
|
-
count: 1,
|
|
88
|
-
placement: { position: 'above_fold', context: 'Chat response top' },
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
slotId: 'suffix-ad',
|
|
92
|
-
slotName: '底部追加广告',
|
|
93
|
-
format: 'suffix',
|
|
94
|
-
size: { width: 600, height: 60 },
|
|
95
|
-
count: 1,
|
|
96
|
-
placement: { position: 'below_fold', context: 'After AI response' },
|
|
97
|
-
},
|
|
98
|
-
],
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// 监听事件
|
|
102
|
-
manager.on('adsUpdated', (slots) => console.log('广告更新:', Object.keys(slots)));
|
|
103
|
-
manager.on('adsError', (err) => console.error('广告请求失败:', err.message));
|
|
104
|
-
|
|
105
|
-
// 请求广告(在 AI 回复完成后调用)
|
|
106
|
-
await manager.requestAds({
|
|
107
|
-
conversationContext: {
|
|
108
|
-
query: '推荐一款蓝牙耳机',
|
|
109
|
-
response: '以下是几款热门蓝牙耳机推荐...',
|
|
110
|
-
},
|
|
111
|
-
userContext: { sessionId: 'session-abc-123' },
|
|
112
|
-
});
|
|
42
|
+
const props = defineProps(['query', 'response']);
|
|
43
|
+
const adEl = ref(null);
|
|
44
|
+
let manager;
|
|
113
45
|
|
|
114
|
-
|
|
115
|
-
manager
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
---
|
|
120
|
-
|
|
121
|
-
### 方式二:fetchAds + DOMRenderer(更灵活)
|
|
122
|
-
|
|
123
|
-
适合需要自行控制渲染时机的场景。
|
|
124
|
-
|
|
125
|
-
```typescript
|
|
126
|
-
import { fetchAds, DOMRenderer } from '@action-x/ad-sdk';
|
|
127
|
-
import '@action-x/ad-sdk/dist/style.css';
|
|
128
|
-
|
|
129
|
-
const result = await fetchAds(
|
|
130
|
-
{
|
|
131
|
-
conversationContext: { query: '推荐一款笔记本电脑' },
|
|
132
|
-
slots: [{
|
|
133
|
-
slotId: 'card-1',
|
|
134
|
-
slotName: '产品推荐卡片',
|
|
135
|
-
format: 'action_card',
|
|
136
|
-
size: { width: 280, height: 100 },
|
|
137
|
-
placement: { position: 'inline', context: 'product recommendation' },
|
|
138
|
-
}],
|
|
139
|
-
},
|
|
140
|
-
{ apiBaseUrl: 'https://your-api.example.com/api/v1', apiKey: 'ak_xxx' }
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
result.slots.forEach((slot) => {
|
|
144
|
-
if (slot.status !== 'filled') return;
|
|
145
|
-
slot.ads.forEach((ad, index) => {
|
|
146
|
-
const container = document.createElement('div');
|
|
147
|
-
document.getElementById('ad-area').appendChild(container);
|
|
148
|
-
DOMRenderer.renderActionCard(ad, container, {
|
|
149
|
-
analytics: {
|
|
150
|
-
requestId: result.requestId,
|
|
151
|
-
slotId: slot.slotId,
|
|
152
|
-
position: index,
|
|
153
|
-
totalAds: slot.ads.length,
|
|
154
|
-
},
|
|
155
|
-
onClick: (ad) => console.log('点击:', ad.original.id),
|
|
156
|
-
});
|
|
46
|
+
onMounted(async () => {
|
|
47
|
+
manager = new AdManager({
|
|
48
|
+
apiBaseUrl: 'https://your-api.example.com/api/v1',
|
|
49
|
+
apiKey: 'ak_your_api_key',
|
|
157
50
|
});
|
|
158
|
-
});
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
---
|
|
162
51
|
|
|
163
|
-
|
|
52
|
+
await manager.requestAds({ query: props.query, response: props.response });
|
|
164
53
|
|
|
165
|
-
|
|
166
|
-
|
|
54
|
+
manager.render(adEl.value);
|
|
55
|
+
});
|
|
167
56
|
|
|
168
|
-
|
|
169
|
-
|
|
57
|
+
onUnmounted(() => manager?.destroy());
|
|
58
|
+
</script>
|
|
170
59
|
```
|
|
171
60
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
## 核心概念
|
|
61
|
+
### React
|
|
175
62
|
|
|
176
|
-
|
|
63
|
+
```tsx
|
|
64
|
+
// components/AdCard.tsx
|
|
65
|
+
import { useEffect, useRef } from 'react';
|
|
66
|
+
import { AdManager } from '@action-x/ad-sdk';
|
|
67
|
+
import '@action-x/ad-sdk/dist/style.css';
|
|
177
68
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
↓
|
|
183
|
-
SDK 自动采集 ClientInfo(设备、系统、地理位置)
|
|
184
|
-
↓
|
|
185
|
-
POST /api/v1/ads/request(conversationContext + slots + clientInfo)
|
|
186
|
-
↓
|
|
187
|
-
后端返回 AdResponseBatch(requestId + 各广告位广告列表)
|
|
188
|
-
↓
|
|
189
|
-
调用 render() / DOMRenderer.renderXxx()
|
|
190
|
-
↓
|
|
191
|
-
DOM 绑定点击事件 → 点击追踪(异步)+ 打开目标 URL
|
|
192
|
-
IntersectionObserver → 50% 可见 + 1s 延迟 → 曝光追踪
|
|
193
|
-
```
|
|
69
|
+
interface Props {
|
|
70
|
+
query: string;
|
|
71
|
+
response: string;
|
|
72
|
+
}
|
|
194
73
|
|
|
195
|
-
|
|
74
|
+
export function AdCard({ query, response }: Props) {
|
|
75
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
196
76
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
const manager = new AdManager({
|
|
79
|
+
apiBaseUrl: 'https://your-api.example.com/api/v1',
|
|
80
|
+
apiKey: 'ak_your_api_key',
|
|
81
|
+
});
|
|
202
82
|
|
|
203
|
-
|
|
83
|
+
manager
|
|
84
|
+
.requestAds({ query, response })
|
|
85
|
+
.then(() => {
|
|
86
|
+
if (containerRef.current) {
|
|
87
|
+
manager.render(containerRef.current);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
204
90
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
new AdManager({ apiKey: 'ak_xxx', ... })
|
|
208
|
-
fetchAds(params, { apiKey: 'ak_xxx' })
|
|
91
|
+
return () => manager.destroy();
|
|
92
|
+
}, [query, response]);
|
|
209
93
|
|
|
210
|
-
|
|
211
|
-
|
|
94
|
+
return <div ref={containerRef} />;
|
|
95
|
+
}
|
|
212
96
|
```
|
|
213
97
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
## API 参考
|
|
217
|
-
|
|
218
|
-
### AdManager
|
|
219
|
-
|
|
220
|
-
主控类,管理广告请求、缓存、渲染和事件。
|
|
221
|
-
|
|
222
|
-
#### 构造函数
|
|
98
|
+
### Vanilla JavaScript
|
|
223
99
|
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
interface AdManagerConfig {
|
|
228
|
-
apiBaseUrl: string; // 必填:API 基础地址
|
|
229
|
-
apiKey?: string; // 推荐:API 认证密钥
|
|
230
|
-
slots: AdSlotRequest[]; // 必填:广告位配置列表
|
|
231
|
-
enabled?: boolean; // 是否启用,默认 true
|
|
232
|
-
debug?: boolean; // 调试日志,默认 false
|
|
233
|
-
}
|
|
234
|
-
```
|
|
100
|
+
```js
|
|
101
|
+
import { AdManager } from '@action-x/ad-sdk';
|
|
102
|
+
import '@action-x/ad-sdk/dist/style.css';
|
|
235
103
|
|
|
236
|
-
|
|
104
|
+
const manager = new AdManager({
|
|
105
|
+
apiBaseUrl: 'https://your-api.example.com/api/v1',
|
|
106
|
+
apiKey: 'ak_your_api_key',
|
|
107
|
+
});
|
|
237
108
|
|
|
238
|
-
|
|
109
|
+
// Request and render ads after the AI response is complete
|
|
239
110
|
await manager.requestAds({
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
response?: string; // AI 回复(post-response 阶段)
|
|
243
|
-
intent?: IntentResult;
|
|
244
|
-
conversationHistory?: ConversationMessage[];
|
|
245
|
-
},
|
|
246
|
-
userContext?: {
|
|
247
|
-
sessionId: string; // 必填
|
|
248
|
-
userId?: string;
|
|
249
|
-
demographics?: { country?: string; language?: string };
|
|
250
|
-
profile?: UserProfile;
|
|
251
|
-
},
|
|
111
|
+
query: 'Recommend a Bluetooth headset',
|
|
112
|
+
response: 'Here are several popular Bluetooth headset options...',
|
|
252
113
|
});
|
|
253
|
-
```
|
|
254
114
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
```typescript
|
|
258
|
-
manager.render(
|
|
259
|
-
slotId: string,
|
|
260
|
-
container: HTMLElement,
|
|
261
|
-
options?: {
|
|
262
|
-
adIndex?: number; // 渲染第几条广告,默认 0
|
|
263
|
-
variant?: string; // 'horizontal' | 'block' | 'card' 等
|
|
264
|
-
onClick?: (ad) => void;
|
|
265
|
-
onImpression?: (ad) => void;
|
|
266
|
-
}
|
|
267
|
-
)
|
|
115
|
+
manager.render(document.getElementById('ad-card'));
|
|
268
116
|
```
|
|
269
117
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
```typescript
|
|
273
|
-
manager.on('adsUpdated', (slots: Record<string, AdSlotResponse>) => void);
|
|
274
|
-
manager.on('adsLoading', () => void);
|
|
275
|
-
manager.on('adsError', (error: Error) => void);
|
|
276
|
-
manager.on('adClicked', (adId: string, slotId: string) => void);
|
|
277
|
-
manager.on('adImpression', (adId: string, slotId: string) => void);
|
|
278
|
-
```
|
|
118
|
+
### CDN (No Build Tool)
|
|
279
119
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|------|------|
|
|
284
|
-
| `getSlots(slotId?)` | 获取广告位数据 |
|
|
285
|
-
| `hasAds(slotId)` | 广告位是否有广告 |
|
|
286
|
-
| `getAds(slotId)` | 获取广告数组 |
|
|
287
|
-
| `getLoadingStatus()` | 是否请求中 |
|
|
288
|
-
| `setEnabled(enabled)` | 启用/禁用广告 |
|
|
289
|
-
| `updateConfig(config)` | 动态更新配置 |
|
|
290
|
-
| `clearSlots()` | 清空广告数据 |
|
|
291
|
-
| `getCurrentRequestId()` | 获取最近 requestId |
|
|
292
|
-
| `getAdAnalytics(adId)` | 获取广告 Analytics 信息 |
|
|
293
|
-
| `trackAdClick(adId, url, adData?)` | 手动触发点击追踪 |
|
|
294
|
-
| `trackAdImpressionAPI(adId, adData?)` | 手动触发曝光追踪 |
|
|
295
|
-
| `destroy()` | 销毁实例,清理监听器 |
|
|
296
|
-
|
|
297
|
-
---
|
|
120
|
+
```html
|
|
121
|
+
<link rel="stylesheet" href="https://unpkg.com/@action-x/ad-sdk/dist/style.css" />
|
|
122
|
+
<script src="https://unpkg.com/@action-x/ad-sdk/dist/index.umd.js"></script>
|
|
298
123
|
|
|
299
|
-
|
|
124
|
+
<div id="ad-card"></div>
|
|
300
125
|
|
|
301
|
-
|
|
302
|
-
|
|
126
|
+
<script>
|
|
127
|
+
const { AdManager } = ActionXAdSDK;
|
|
303
128
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
userContext?: { sessionId: '...' },
|
|
309
|
-
slots: AdSlotRequest[],
|
|
310
|
-
},
|
|
311
|
-
{ apiBaseUrl?: string, apiKey?: string }
|
|
312
|
-
);
|
|
129
|
+
const manager = new AdManager({
|
|
130
|
+
apiBaseUrl: 'https://your-api.example.com/api/v1',
|
|
131
|
+
apiKey: 'ak_your_api_key',
|
|
132
|
+
});
|
|
313
133
|
|
|
314
|
-
|
|
315
|
-
|
|
134
|
+
manager
|
|
135
|
+
.requestAds({ query: 'User question', response: 'AI response' })
|
|
136
|
+
.then(() => {
|
|
137
|
+
manager.render(document.getElementById('ad-card'));
|
|
138
|
+
});
|
|
139
|
+
</script>
|
|
316
140
|
```
|
|
317
141
|
|
|
318
142
|
---
|
|
319
143
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
生成广告 HTML 字符串,适合 SSR 或自行控制 DOM。
|
|
323
|
-
|
|
324
|
-
```typescript
|
|
325
|
-
import { HTMLRenderer } from '@action-x/ad-sdk';
|
|
326
|
-
|
|
327
|
-
// Action Card
|
|
328
|
-
HTMLRenderer.renderActionCard(ad, { variant?: 'horizontal' | 'vertical' | 'compact', includeWrapper?: boolean })
|
|
144
|
+
## Usage Example
|
|
329
145
|
|
|
330
|
-
|
|
331
|
-
HTMLRenderer.renderSuffixAd(ad)
|
|
146
|
+
### Render an Ad Card After AI Response Completes
|
|
332
147
|
|
|
333
|
-
|
|
334
|
-
HTMLRenderer.renderSponsoredSource(ad)
|
|
148
|
+
Using React as an example, render AdCard after streaming output finishes:
|
|
335
149
|
|
|
336
|
-
|
|
337
|
-
|
|
150
|
+
```tsx
|
|
151
|
+
import { useState } from 'react';
|
|
152
|
+
import { AdCard } from './components/AdCard';
|
|
338
153
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
---
|
|
344
|
-
|
|
345
|
-
### DOMRenderer
|
|
346
|
-
|
|
347
|
-
创建真实 DOM 并绑定事件追踪,不依赖 AdManager。
|
|
348
|
-
|
|
349
|
-
```typescript
|
|
350
|
-
import { DOMRenderer } from '@action-x/ad-sdk';
|
|
154
|
+
export function ChatMessage() {
|
|
155
|
+
const [query, setQuery] = useState('');
|
|
156
|
+
const [response, setResponse] = useState('');
|
|
157
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
351
158
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
onClick?: (ad) => void,
|
|
357
|
-
onImpression?: (ad) => void,
|
|
358
|
-
})
|
|
159
|
+
async function handleSend(userQuery: string) {
|
|
160
|
+
setQuery(userQuery);
|
|
161
|
+
setIsStreaming(true);
|
|
162
|
+
setResponse('');
|
|
359
163
|
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
164
|
+
// Receive AI response as a stream
|
|
165
|
+
const stream = await fetchAIResponse(userQuery);
|
|
166
|
+
let fullResponse = '';
|
|
167
|
+
for await (const chunk of stream) {
|
|
168
|
+
fullResponse += chunk;
|
|
169
|
+
setResponse(fullResponse);
|
|
170
|
+
}
|
|
365
171
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
analytics?, onClick?, onImpression?,
|
|
370
|
-
})
|
|
172
|
+
setIsStreaming(false);
|
|
173
|
+
// After response completes, AdCard will request and render automatically
|
|
174
|
+
}
|
|
371
175
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
})
|
|
176
|
+
return (
|
|
177
|
+
<div>
|
|
178
|
+
<div className="response">{response}</div>
|
|
376
179
|
|
|
377
|
-
|
|
378
|
-
|
|
180
|
+
{/* Show ad card after response streaming ends */}
|
|
181
|
+
{!isStreaming && response && (
|
|
182
|
+
<AdCard query={query} response={response} />
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
379
187
|
```
|
|
380
188
|
|
|
381
|
-
**点击流程**:异步发送 Analytics click 事件(不阻塞跳转)→ `window.open(clickUrl, '_blank')`
|
|
382
|
-
|
|
383
|
-
**曝光流程**:IntersectionObserver 监听 → 50% 可见 + 1000ms → Analytics impression 事件
|
|
384
|
-
|
|
385
189
|
---
|
|
386
190
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
```typescript
|
|
390
|
-
import {
|
|
391
|
-
trackAdImpression, trackAdClick,
|
|
392
|
-
getSessionId, generateViewToken,
|
|
393
|
-
trackImpressionsBatch, trackClicksBatch,
|
|
394
|
-
SessionManager, AnalyticsSender,
|
|
395
|
-
createAnalytics, createSession,
|
|
396
|
-
} from '@action-x/ad-sdk';
|
|
397
|
-
|
|
398
|
-
// 快捷追踪
|
|
399
|
-
await trackAdImpression({
|
|
400
|
-
requestId, adId, position, totalAds, sessionId: getSessionId(),
|
|
401
|
-
slotId?, adTitle?, format?,
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
await trackAdClick({
|
|
405
|
-
requestId, adId, sessionId: getSessionId(), destinationUrl,
|
|
406
|
-
slotId?, adTitle?, format?,
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
// View Token
|
|
410
|
-
const token = generateViewToken(adId, sessionId);
|
|
191
|
+
## Configuration
|
|
411
192
|
|
|
412
|
-
|
|
413
|
-
await trackImpressionsBatch([event1, event2]);
|
|
414
|
-
await trackClicksBatch([event1, event2]);
|
|
193
|
+
### `new AdManager(config)`
|
|
415
194
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
#### SessionManager
|
|
195
|
+
| Parameter | Type | Required | Description |
|
|
196
|
+
|------|------|------|------|
|
|
197
|
+
| `apiBaseUrl` | `string` | Yes | API base URL, e.g. `https://your-api.example.com/api/v1` |
|
|
198
|
+
| `apiKey` | `string` | Yes | API authentication key |
|
|
199
|
+
| `cardOption` | `CardOption` | No | Card options with sensible defaults |
|
|
422
200
|
|
|
423
|
-
|
|
424
|
-
new SessionManager({ sessionKey?: string, storage?: 'sessionStorage' | 'memory' })
|
|
425
|
-
// .getSessionId() 获取或创建 session ID
|
|
426
|
-
// .regenerateSessionId() 强制生成新 ID
|
|
427
|
-
// .clearSessionId() 清除 ID
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
#### AnalyticsSender
|
|
431
|
-
|
|
432
|
-
```typescript
|
|
433
|
-
new AnalyticsSender({
|
|
434
|
-
baseUrl?: string, // 默认 '/api/v1/ads'
|
|
435
|
-
timeout?: number, // 默认 10000ms
|
|
436
|
-
retryAttempts?: number, // 默认 2
|
|
437
|
-
retryDelay?: number, // 默认 1000ms(指数退避)
|
|
438
|
-
debug?: boolean,
|
|
439
|
-
})
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
---
|
|
201
|
+
### Card Options `cardOption`
|
|
443
202
|
|
|
444
|
-
|
|
203
|
+
| Parameter | Type | Default | Description |
|
|
204
|
+
|------|------|--------|------|
|
|
205
|
+
| `variant` | `'horizontal' \| 'vertical' \| 'compact'` | `'horizontal'` | Layout style |
|
|
206
|
+
| `count` | `number` | `1` | Number of ads to request |
|
|
445
207
|
|
|
446
|
-
|
|
208
|
+
**Customization example:**
|
|
447
209
|
|
|
448
|
-
```
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
minViewableDuration?: 1000, // 默认 1000ms
|
|
456
|
-
maxTrackingDuration?: 60000, // 默认 60000ms
|
|
457
|
-
batchConfig?: { maxBatchSize?: 5, maxBatchWaitMs?: 10000 },
|
|
458
|
-
debug?: false,
|
|
210
|
+
```js
|
|
211
|
+
new AdManager({
|
|
212
|
+
apiBaseUrl: '...',
|
|
213
|
+
apiKey: '...',
|
|
214
|
+
cardOption: {
|
|
215
|
+
variant: 'vertical',
|
|
216
|
+
count: 2,
|
|
459
217
|
},
|
|
460
|
-
baseUrl?: string // 默认 '/api/v1'
|
|
461
|
-
);
|
|
462
|
-
|
|
463
|
-
tracker.startTracking(adId, element, sessionId, requestId?, viewToken?);
|
|
464
|
-
tracker.getMetrics(adId); // ViewabilityMetrics
|
|
465
|
-
tracker.stopTracking(adId);
|
|
466
|
-
tracker.stopAll();
|
|
467
|
-
```
|
|
468
|
-
|
|
469
|
-
---
|
|
470
|
-
|
|
471
|
-
### ClientInfo
|
|
472
|
-
|
|
473
|
-
零配置,自动采集 OpenRTB 2.5/2.6 规范的客户端信息。
|
|
474
|
-
|
|
475
|
-
```typescript
|
|
476
|
-
import {
|
|
477
|
-
getClientInfo, getDeviceInfo, getUserInfo,
|
|
478
|
-
getAppInfo, getGeoInfo, getUserId,
|
|
479
|
-
clearClientInfoCache, getClientInfoJSON, getClientInfoSummary,
|
|
480
|
-
} from '@action-x/ad-sdk';
|
|
481
|
-
|
|
482
|
-
const info = getClientInfo();
|
|
483
|
-
// info.device.os → 'macOS'
|
|
484
|
-
// info.device.model → 'iPhone14,2'
|
|
485
|
-
// info.user.id → 'uuid-xxx'(localStorage 持久化)
|
|
486
|
-
// info.geo.country → 'CN'(从时区/语言推断)
|
|
487
|
-
// info.app.bundle → 'example.com'
|
|
488
|
-
|
|
489
|
-
// 覆盖默认值
|
|
490
|
-
const info = getClientInfo({
|
|
491
|
-
device: { ifa: 'your-idfa', lmt: 0 },
|
|
492
|
-
app: { bundle: 'com.your.app', ver: '2.1.0' },
|
|
493
|
-
geo: { lat: 39.9, lon: 116.4 },
|
|
494
218
|
});
|
|
495
219
|
```
|
|
496
220
|
|
|
497
|
-
|
|
221
|
+
### `requestAds(context)`
|
|
498
222
|
|
|
499
|
-
|
|
223
|
+
| Parameter | Type | Required | Description |
|
|
224
|
+
|------|------|------|------|
|
|
225
|
+
| `query` | `string` | Yes | User question |
|
|
226
|
+
| `response` | `string` | Yes | AI response text |
|
|
500
227
|
|
|
501
|
-
|
|
502
|
-
import { EventEmitter } from '@action-x/ad-sdk';
|
|
228
|
+
### `render(container, options?)`
|
|
503
229
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
emitter.listenerCount('event');
|
|
510
|
-
```
|
|
511
|
-
|
|
512
|
-
---
|
|
513
|
-
|
|
514
|
-
## 类型定义
|
|
515
|
-
|
|
516
|
-
```typescript
|
|
517
|
-
// 广告格式
|
|
518
|
-
type AdFormat = 'action_card' | 'suffix' | 'followup' | 'source' | 'static' | 'lead_gen' | 'entity_link';
|
|
519
|
-
|
|
520
|
-
// 广告位请求
|
|
521
|
-
interface AdSlotRequest {
|
|
522
|
-
slotId: string;
|
|
523
|
-
slotName: string;
|
|
524
|
-
format: AdFormat;
|
|
525
|
-
variant?: string;
|
|
526
|
-
size: { width: number; height: number; aspectRatio?: string };
|
|
527
|
-
count?: number;
|
|
528
|
-
preferences?: { maxTitleLength?: number; requireImage?: boolean; showPrice?: boolean };
|
|
529
|
-
placement: { position: 'above_fold' | 'below_fold' | 'sidebar' | 'inline'; context: string };
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// 广告响应批次
|
|
533
|
-
interface AdResponseBatch {
|
|
534
|
-
requestId: string;
|
|
535
|
-
timestamp: number;
|
|
536
|
-
intent: IntentResult;
|
|
537
|
-
slots: AdSlotResponse[];
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// 广告位响应
|
|
541
|
-
interface AdSlotResponse {
|
|
542
|
-
slotId: string;
|
|
543
|
-
status: 'filled' | 'no_fill' | 'error';
|
|
544
|
-
ads: AdaptedAdContent[];
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// 适配后的广告内容
|
|
548
|
-
interface AdaptedAdContent {
|
|
549
|
-
original: { id: string; type: AdFormat; score: number };
|
|
550
|
-
adapted: { title: string; body: string; ctaText: string; image?; price?; rating?; brand? };
|
|
551
|
-
tracking: { impressionUrl: string; clickUrl: string; viewToken: string };
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Analytics 信息(传给 DOMRenderer)
|
|
555
|
-
interface AdAnalyticsInfo {
|
|
556
|
-
requestId: string;
|
|
557
|
-
slotId: string;
|
|
558
|
-
position: number;
|
|
559
|
-
totalAds: number;
|
|
560
|
-
}
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
完整类型定义参见 `dist/index.d.ts`。
|
|
564
|
-
|
|
565
|
-
---
|
|
566
|
-
|
|
567
|
-
## CSS 样式
|
|
568
|
-
|
|
569
|
-
所有样式使用 `ax-ad-*` 命名空间,不污染全局。
|
|
570
|
-
|
|
571
|
-
| CSS 类 | 用途 |
|
|
572
|
-
|-------|------|
|
|
573
|
-
| `.ax-ad-card` | Action Card 基础容器 |
|
|
574
|
-
| `.ax-ad-card-horizontal/vertical/compact` | 布局变体 |
|
|
575
|
-
| `.ax-ad-cta` | CTA 按钮(蓝色 #3b82f6) |
|
|
576
|
-
| `.ax-ad-suffix` | Suffix 广告容器 |
|
|
577
|
-
| `.ax-ad-suffix-link` | Suffix 链接 |
|
|
578
|
-
| `.ax-ad-variant-block/inline/minimal` | Suffix 变体 |
|
|
579
|
-
| `.ax-ad-source-link` | Source 广告链接 |
|
|
580
|
-
| `.ax-ad-variant-card/list-item/minimal` | Source 变体 |
|
|
581
|
-
| `.ax-ad-leadgen-cta` | LeadGen CTA |
|
|
582
|
-
| `.ax-ad-sponsored-badge` | 赞助商标识 |
|
|
583
|
-
| `.ax-ads-container` | 多广告外层容器 |
|
|
584
|
-
|
|
585
|
-
**自定义样式:**
|
|
586
|
-
|
|
587
|
-
```css
|
|
588
|
-
/* 覆盖 CTA 颜色 */
|
|
589
|
-
.ax-ad-cta { background: #7c3aed; }
|
|
590
|
-
.ax-ad-cta:hover { background: #6d28d9; }
|
|
591
|
-
|
|
592
|
-
/* 覆盖卡片圆角 */
|
|
593
|
-
.ax-ad-card { border-radius: 12px; }
|
|
594
|
-
```
|
|
595
|
-
|
|
596
|
-
---
|
|
597
|
-
|
|
598
|
-
## 广告格式说明
|
|
599
|
-
|
|
600
|
-
| 格式 | 说明 | 典型位置 |
|
|
601
|
-
|------|------|---------|
|
|
602
|
-
| `action_card` | 产品卡片,含图片/价格/评分/CTA | AI 回复下方 |
|
|
603
|
-
| `suffix` | 低干扰追加文本,append 在回复末尾 | AI 回复末尾 |
|
|
604
|
-
| `source` | 混入引用来源列表的赞助链接 | 引用/来源区域 |
|
|
605
|
-
| `lead_gen` | 线索收集,引导留资 / 注册 | 侧边栏 / 对话末尾 |
|
|
606
|
-
| `followup` | 混入相关问题建议 | 问题推荐区域 |
|
|
607
|
-
| `static` | 静态 Banner 展示 | 固定位置 |
|
|
230
|
+
| Parameter | Type | Description |
|
|
231
|
+
|------|------|------|
|
|
232
|
+
| `container` | `HTMLElement` | Mount container |
|
|
233
|
+
| `options.onClick` | `(ad) => void` | Click callback |
|
|
234
|
+
| `options.onImpression` | `(ad) => void` | Impression callback |
|
|
608
235
|
|
|
609
236
|
---
|
|
610
237
|
|
|
611
|
-
##
|
|
612
|
-
|
|
613
|
-
### Vue 3
|
|
614
|
-
|
|
615
|
-
```typescript
|
|
616
|
-
// composables/useAds.ts
|
|
617
|
-
import { ref, onUnmounted } from 'vue';
|
|
618
|
-
import { AdManager } from '@action-x/ad-sdk';
|
|
619
|
-
|
|
620
|
-
export function useAds(slots) {
|
|
621
|
-
const isLoading = ref(false);
|
|
622
|
-
const manager = new AdManager({
|
|
623
|
-
apiBaseUrl: import.meta.env.VITE_AD_API_URL,
|
|
624
|
-
apiKey: import.meta.env.VITE_AD_API_KEY,
|
|
625
|
-
slots,
|
|
626
|
-
});
|
|
627
|
-
manager.on('adsLoading', () => (isLoading.value = true));
|
|
628
|
-
manager.on('adsUpdated', () => (isLoading.value = false));
|
|
629
|
-
onUnmounted(() => manager.destroy());
|
|
630
|
-
return { manager, isLoading };
|
|
631
|
-
}
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
### React
|
|
635
|
-
|
|
636
|
-
```typescript
|
|
637
|
-
// hooks/useAdManager.ts
|
|
638
|
-
import { useEffect, useRef } from 'react';
|
|
639
|
-
import { AdManager } from '@action-x/ad-sdk';
|
|
238
|
+
## Event Listeners
|
|
640
239
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
useEffect(() => () => ref.current.destroy(), []);
|
|
648
|
-
return ref.current;
|
|
649
|
-
}
|
|
650
|
-
```
|
|
651
|
-
|
|
652
|
-
### CDN / 无构建工具
|
|
653
|
-
|
|
654
|
-
```html
|
|
655
|
-
<link rel="stylesheet" href="https://unpkg.com/@action-x/ad-sdk/dist/style.css" />
|
|
656
|
-
<script src="https://unpkg.com/@action-x/ad-sdk/dist/index.umd.js"></script>
|
|
657
|
-
<script>
|
|
658
|
-
const { AdManager } = ActionXAdSDK;
|
|
659
|
-
const manager = new AdManager({ apiBaseUrl: '...', apiKey: '...', slots: [...] });
|
|
660
|
-
</script>
|
|
240
|
+
```js
|
|
241
|
+
manager.on('adsUpdated', (slots) => { /* ad data ready */ });
|
|
242
|
+
manager.on('adsLoading', () => { /* requesting */ });
|
|
243
|
+
manager.on('adsError', (err) => { /* request failed */ });
|
|
244
|
+
manager.on('adClicked', (adId, slotId) => { });
|
|
245
|
+
manager.on('adImpression', (adId, slotId) => { });
|
|
661
246
|
```
|
|
662
247
|
|
|
663
248
|
---
|
|
664
249
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
| 端点 | 方法 | 用途 |
|
|
668
|
-
|------|------|------|
|
|
669
|
-
| `/api/v1/ads/request` | POST | 请求广告批次 |
|
|
670
|
-
| `/api/v1/ads/impression` | POST | 曝光事件上报 |
|
|
671
|
-
| `/api/v1/ads/click` | POST | 点击事件上报 |
|
|
672
|
-
| `/api/v1/ads/viewability/batch` | POST | 可视性事件批量上报 |
|
|
673
|
-
|
|
674
|
-
所有请求若配置了 `apiKey`,均自动携带 `X-API-Key` 请求头。
|
|
675
|
-
|
|
676
|
-
---
|
|
677
|
-
|
|
678
|
-
**版本**:`0.1.0` | **许可证**:MIT
|
|
250
|
+
**Version**: `0.1.0` | **License**: MIT
|