@action-x/ad-sdk 0.1.0
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 +678 -0
- package/dist/index.cjs +139 -0
- package/dist/index.d.ts +1552 -0
- package/dist/index.js +1478 -0
- package/dist/index.umd.js +139 -0
- package/dist/style.css +1 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
# @action-x/ad-sdk
|
|
2
|
+
|
|
3
|
+
零框架依赖的广告 SDK,适用于任何 JavaScript / TypeScript 项目。
|
|
4
|
+
支持原生 JS、Vue、Angular、Svelte、React 等所有框架。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
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
|
+
## 安装
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install @action-x/ad-sdk
|
|
46
|
+
# 或
|
|
47
|
+
yarn add @action-x/ad-sdk
|
|
48
|
+
# 或
|
|
49
|
+
pnpm add @action-x/ad-sdk
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**引入样式(必须):**
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
// 在你的入口文件或组件中引入
|
|
56
|
+
import '@action-x/ad-sdk/dist/style.css';
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
或在 HTML 中:
|
|
60
|
+
|
|
61
|
+
```html
|
|
62
|
+
<link rel="stylesheet" href="node_modules/@action-x/ad-sdk/dist/style.css" />
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 快速上手
|
|
68
|
+
|
|
69
|
+
### 方式一:AdManager(推荐)
|
|
70
|
+
|
|
71
|
+
适合需要统一管理多个广告位、自动处理 Analytics 的场景。
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { AdManager } from '@action-x/ad-sdk';
|
|
75
|
+
import '@action-x/ad-sdk/dist/style.css';
|
|
76
|
+
|
|
77
|
+
const manager = new AdManager({
|
|
78
|
+
apiBaseUrl: 'https://your-api.example.com/api/v1',
|
|
79
|
+
apiKey: 'ak_your_api_key',
|
|
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
|
+
});
|
|
113
|
+
|
|
114
|
+
// 渲染到 DOM
|
|
115
|
+
manager.render('banner-top', document.getElementById('ad-banner'));
|
|
116
|
+
manager.render('suffix-ad', document.getElementById('ad-suffix'));
|
|
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
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
### 方式三:HTMLRenderer(纯字符串,适合 SSR)
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { HTMLRenderer } from '@action-x/ad-sdk';
|
|
167
|
+
|
|
168
|
+
const html = HTMLRenderer.renderActionCard(ad, { variant: 'horizontal' });
|
|
169
|
+
document.getElementById('ad-container').innerHTML = html;
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## 核心概念
|
|
175
|
+
|
|
176
|
+
### 广告请求流程
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
用户发起对话
|
|
180
|
+
↓
|
|
181
|
+
调用 requestAds() / fetchAds()
|
|
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
|
+
```
|
|
194
|
+
|
|
195
|
+
### Analytics 追踪机制
|
|
196
|
+
|
|
197
|
+
| 事件 | 触发时机 | 端点 |
|
|
198
|
+
|------|---------|------|
|
|
199
|
+
| 曝光(impression)| 50% 可见 + 持续 1000ms | `POST /api/v1/ads/impression` |
|
|
200
|
+
| 点击(click)| 用户点击 CTA 按钮 | `POST /api/v1/ads/click` |
|
|
201
|
+
| 可视性(viewability)| 全程监测,批量上报 | `POST /api/v1/ads/viewability/batch` |
|
|
202
|
+
|
|
203
|
+
### API Key 传递
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
// 方式 1:构造函数传入(推荐)
|
|
207
|
+
new AdManager({ apiKey: 'ak_xxx', ... })
|
|
208
|
+
fetchAds(params, { apiKey: 'ak_xxx' })
|
|
209
|
+
|
|
210
|
+
// 方式 2:全局变量(适合动态配置场景)
|
|
211
|
+
window.__AD_CONFIG__ = { apiKey: 'ak_xxx', apiBaseUrl: '...' };
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## API 参考
|
|
217
|
+
|
|
218
|
+
### AdManager
|
|
219
|
+
|
|
220
|
+
主控类,管理广告请求、缓存、渲染和事件。
|
|
221
|
+
|
|
222
|
+
#### 构造函数
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
new AdManager(config: AdManagerConfig)
|
|
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
|
+
```
|
|
235
|
+
|
|
236
|
+
#### `requestAds(context): Promise<AdResponseBatch>`
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
await manager.requestAds({
|
|
240
|
+
conversationContext: {
|
|
241
|
+
query: string; // 必填:用户查询
|
|
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
|
+
},
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
#### `render(slotId, container, options?): HTMLElement | null`
|
|
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
|
+
)
|
|
268
|
+
```
|
|
269
|
+
|
|
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
|
+
```
|
|
279
|
+
|
|
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
|
+
---
|
|
298
|
+
|
|
299
|
+
### fetchAds / adaptAdToKoahAd
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
import { fetchAds, adaptAdToKoahAd } from '@action-x/ad-sdk';
|
|
303
|
+
|
|
304
|
+
// 请求广告(自动注入 ClientInfo)
|
|
305
|
+
const result: AdResponseBatch = await fetchAds(
|
|
306
|
+
{
|
|
307
|
+
conversationContext: { query: '...', response?: '...' },
|
|
308
|
+
userContext?: { sessionId: '...' },
|
|
309
|
+
slots: AdSlotRequest[],
|
|
310
|
+
},
|
|
311
|
+
{ apiBaseUrl?: string, apiKey?: string }
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
// 转换 AdaptedAdContent → KoahAd
|
|
315
|
+
const koahAd: KoahAd = adaptAdToKoahAd(adaptedAdContent);
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
### HTMLRenderer
|
|
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 })
|
|
329
|
+
|
|
330
|
+
// Suffix Ad
|
|
331
|
+
HTMLRenderer.renderSuffixAd(ad)
|
|
332
|
+
|
|
333
|
+
// Sponsored Source
|
|
334
|
+
HTMLRenderer.renderSponsoredSource(ad)
|
|
335
|
+
|
|
336
|
+
// Lead Gen
|
|
337
|
+
HTMLRenderer.renderLeadGenAd(ad)
|
|
338
|
+
|
|
339
|
+
// 批量渲染
|
|
340
|
+
HTMLRenderer.renderAds(ads, renderFn?)
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
### DOMRenderer
|
|
346
|
+
|
|
347
|
+
创建真实 DOM 并绑定事件追踪,不依赖 AdManager。
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
import { DOMRenderer } from '@action-x/ad-sdk';
|
|
351
|
+
|
|
352
|
+
// Action Card(click 绑定 .ax-ad-cta)
|
|
353
|
+
DOMRenderer.renderActionCard(ad, container, {
|
|
354
|
+
variant?: 'horizontal' | 'vertical' | 'compact',
|
|
355
|
+
analytics?: AdAnalyticsInfo,
|
|
356
|
+
onClick?: (ad) => void,
|
|
357
|
+
onImpression?: (ad) => void,
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
// Suffix Ad(click 绑定 .ax-ad-suffix-link)
|
|
361
|
+
DOMRenderer.renderSuffixAd(ad, container, {
|
|
362
|
+
variant?: 'block' | 'inline' | 'minimal', // 默认 'block'
|
|
363
|
+
analytics?, onClick?, onImpression?,
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
// Sponsored Source(click 绑定 .ax-ad-source-link)
|
|
367
|
+
DOMRenderer.renderSponsoredSource(ad, container, {
|
|
368
|
+
variant?: 'card' | 'list-item' | 'minimal', // 默认 'card'
|
|
369
|
+
analytics?, onClick?, onImpression?,
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
// Lead Gen(click 绑定 .ax-ad-leadgen-cta)
|
|
373
|
+
DOMRenderer.renderLeadGenAd(ad, container, {
|
|
374
|
+
analytics?, onClick?, onImpression?,
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
// 批量渲染
|
|
378
|
+
DOMRenderer.renderAds(ads, container, renderFn?, options?)
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**点击流程**:异步发送 Analytics click 事件(不阻塞跳转)→ `window.open(clickUrl, '_blank')`
|
|
382
|
+
|
|
383
|
+
**曝光流程**:IntersectionObserver 监听 → 50% 可见 + 1000ms → Analytics impression 事件
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
### Analytics
|
|
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);
|
|
411
|
+
|
|
412
|
+
// 批量
|
|
413
|
+
await trackImpressionsBatch([event1, event2]);
|
|
414
|
+
await trackClicksBatch([event1, event2]);
|
|
415
|
+
|
|
416
|
+
// 自定义实例
|
|
417
|
+
const analytics = createAnalytics({ baseUrl: '...', timeout: 5000, debug: true });
|
|
418
|
+
const session = createSession({ storage: 'memory' });
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
#### SessionManager
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
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
|
+
---
|
|
443
|
+
|
|
444
|
+
### ViewabilityTracker
|
|
445
|
+
|
|
446
|
+
IAB/MRC 标准可视性追踪(>50% 可见 + >1 秒 = 达标)。
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
import { getViewabilityTracker } from '@action-x/ad-sdk';
|
|
450
|
+
|
|
451
|
+
// 推荐:使用单例,自动注册 beforeunload
|
|
452
|
+
const tracker = getViewabilityTracker(
|
|
453
|
+
{
|
|
454
|
+
minVisiblePercentage?: 50, // 默认 50
|
|
455
|
+
minViewableDuration?: 1000, // 默认 1000ms
|
|
456
|
+
maxTrackingDuration?: 60000, // 默认 60000ms
|
|
457
|
+
batchConfig?: { maxBatchSize?: 5, maxBatchWaitMs?: 10000 },
|
|
458
|
+
debug?: false,
|
|
459
|
+
},
|
|
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
|
+
});
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
### EventEmitter
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
import { EventEmitter } from '@action-x/ad-sdk';
|
|
503
|
+
|
|
504
|
+
const emitter = new EventEmitter();
|
|
505
|
+
emitter.on('event', handler);
|
|
506
|
+
emitter.emit('event', data);
|
|
507
|
+
emitter.off('event', handler);
|
|
508
|
+
emitter.removeAllListeners('event');
|
|
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 展示 | 固定位置 |
|
|
608
|
+
|
|
609
|
+
---
|
|
610
|
+
|
|
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';
|
|
640
|
+
|
|
641
|
+
export function useAdManager(slots) {
|
|
642
|
+
const ref = useRef(new AdManager({
|
|
643
|
+
apiBaseUrl: process.env.NEXT_PUBLIC_AD_API_URL,
|
|
644
|
+
apiKey: process.env.NEXT_PUBLIC_AD_API_KEY,
|
|
645
|
+
slots,
|
|
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>
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
## 依赖 API 端点
|
|
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
|