@action-x/ad-sdk 0.1.0 → 0.1.3
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 +160 -588
- 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 +5 -2
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
|
-
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" />
|
|
20
|
+
import '@action-x/ad-sdk/style.css';
|
|
63
21
|
```
|
|
64
22
|
|
|
65
23
|
---
|
|
66
24
|
|
|
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));
|
|
25
|
+
## AdCard Component
|
|
104
26
|
|
|
105
|
-
|
|
106
|
-
await manager.requestAds({
|
|
107
|
-
conversationContext: {
|
|
108
|
-
query: '推荐一款蓝牙耳机',
|
|
109
|
-
response: '以下是几款热门蓝牙耳机推荐...',
|
|
110
|
-
},
|
|
111
|
-
userContext: { sessionId: 'session-abc-123' },
|
|
112
|
-
});
|
|
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.
|
|
113
28
|
|
|
114
|
-
|
|
115
|
-
manager.render('banner-top', document.getElementById('ad-banner'));
|
|
116
|
-
manager.render('suffix-ad', document.getElementById('ad-suffix'));
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
---
|
|
29
|
+
### Vue 3
|
|
120
30
|
|
|
121
|
-
|
|
31
|
+
```vue
|
|
32
|
+
<!-- AdCard.vue -->
|
|
33
|
+
<template>
|
|
34
|
+
<div ref="adEl" />
|
|
35
|
+
</template>
|
|
122
36
|
|
|
123
|
-
|
|
37
|
+
<script setup>
|
|
38
|
+
import { ref, onMounted, onUnmounted } from 'vue';
|
|
39
|
+
import { AdManager } from '@action-x/ad-sdk';
|
|
40
|
+
import '@action-x/ad-sdk/style.css';
|
|
124
41
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
42
|
+
const props = defineProps(['query', 'response']);
|
|
43
|
+
const adEl = ref(null);
|
|
44
|
+
let manager;
|
|
128
45
|
|
|
129
|
-
|
|
130
|
-
{
|
|
131
|
-
|
|
132
|
-
|
|
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/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
|
-
#### 构造函数
|
|
223
|
-
|
|
224
|
-
```typescript
|
|
225
|
-
new AdManager(config: AdManagerConfig)
|
|
98
|
+
### Vanilla JavaScript
|
|
226
99
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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/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
|
-
|
|
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
114
|
|
|
272
|
-
|
|
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);
|
|
115
|
+
manager.render(document.getElementById('ad-card'));
|
|
278
116
|
```
|
|
279
117
|
|
|
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()` | 销毁实例,清理监听器 |
|
|
118
|
+
### CDN (No Build Tool)
|
|
296
119
|
|
|
297
|
-
|
|
120
|
+
```html
|
|
121
|
+
<link rel="stylesheet" href="https://unpkg.com/@action-x/ad-sdk/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
|
-
|
|
144
|
+
## Usage Example
|
|
321
145
|
|
|
322
|
-
|
|
146
|
+
### Render an Ad Card After AI Response Completes
|
|
323
147
|
|
|
324
|
-
|
|
325
|
-
import { HTMLRenderer } from '@action-x/ad-sdk';
|
|
148
|
+
Using React as an example, render AdCard after streaming output finishes:
|
|
326
149
|
|
|
327
|
-
|
|
328
|
-
|
|
150
|
+
```tsx
|
|
151
|
+
import { useState } from 'react';
|
|
152
|
+
import { AdCard } from './components/AdCard';
|
|
329
153
|
|
|
330
|
-
|
|
331
|
-
|
|
154
|
+
export function ChatMessage() {
|
|
155
|
+
const [query, setQuery] = useState('');
|
|
156
|
+
const [response, setResponse] = useState('');
|
|
157
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
332
158
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
HTMLRenderer.renderLeadGenAd(ad)
|
|
338
|
-
|
|
339
|
-
// 批量渲染
|
|
340
|
-
HTMLRenderer.renderAds(ads, renderFn?)
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
---
|
|
159
|
+
async function handleSend(userQuery: string) {
|
|
160
|
+
setQuery(userQuery);
|
|
161
|
+
setIsStreaming(true);
|
|
162
|
+
setResponse('');
|
|
344
163
|
|
|
345
|
-
|
|
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
|
+
}
|
|
346
171
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
})
|
|
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
|
-
});
|
|
191
|
+
## Configuration
|
|
408
192
|
|
|
409
|
-
|
|
410
|
-
const token = generateViewToken(adId, sessionId);
|
|
193
|
+
### `new AdManager(config)`
|
|
411
194
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
---
|
|
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 |
|
|
443
200
|
|
|
444
|
-
###
|
|
201
|
+
### Card Options `cardOption`
|
|
445
202
|
|
|
446
|
-
|
|
203
|
+
| Parameter | Type | Default | Description |
|
|
204
|
+
|------|------|--------|------|
|
|
205
|
+
| `variant` | `'horizontal' \| 'vertical' \| 'compact'` | `'horizontal'` | Layout style |
|
|
206
|
+
| `count` | `number` | `1` | Number of ads to request |
|
|
447
207
|
|
|
448
|
-
|
|
449
|
-
import { getViewabilityTracker } from '@action-x/ad-sdk';
|
|
208
|
+
**Customization example:**
|
|
450
209
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
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`。
|
|
221
|
+
### `requestAds(context)`
|
|
564
222
|
|
|
565
|
-
|
|
223
|
+
| Parameter | Type | Required | Description |
|
|
224
|
+
|------|------|------|------|
|
|
225
|
+
| `query` | `string` | Yes | User question |
|
|
226
|
+
| `response` | `string` | Yes | AI response text |
|
|
566
227
|
|
|
567
|
-
|
|
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
|
-
---
|
|
228
|
+
### `render(container, options?)`
|
|
597
229
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
|
601
|
-
|
|
602
|
-
| `
|
|
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
|
-
##
|
|
238
|
+
## Event Listeners
|
|
612
239
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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>
|
|
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
|