@things-factory/integration-headless 8.0.0-beta.9 → 8.0.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/dist-server/engine/connector/headless-connector.d.ts +14 -0
- package/dist-server/engine/connector/headless-connector.js +54 -0
- package/dist-server/engine/connector/headless-connector.js.map +1 -0
- package/dist-server/engine/connector/headless-pool.d.ts +3 -0
- package/dist-server/engine/connector/headless-pool.js +63 -0
- package/dist-server/engine/connector/headless-pool.js.map +1 -0
- package/dist-server/engine/connector/index.d.ts +1 -0
- package/dist-server/engine/connector/index.js +4 -0
- package/dist-server/engine/connector/index.js.map +1 -0
- package/dist-server/engine/index.d.ts +1 -0
- package/dist-server/engine/index.js +1 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/task/pdf-capture-util.d.ts +1 -1
- package/dist-server/engine/task/pdf-capture-util.js +3 -3
- package/dist-server/engine/task/pdf-capture-util.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/helps/integration/connector/headless-connector.ja.md +31 -183
- package/helps/integration/connector/headless-connector.ko.md +32 -177
- package/helps/integration/connector/headless-connector.md +31 -178
- package/helps/integration/connector/headless-connector.ms.md +32 -180
- package/helps/integration/connector/headless-connector.zh.md +31 -178
- package/package.json +6 -6
- package/server/engine/connector/headless-connector.ts +68 -0
- package/server/engine/connector/headless-pool.ts +69 -0
- package/server/engine/connector/index.ts +1 -0
- package/server/engine/index.ts +2 -0
- package/server/engine/task/headless-pdf-capture-board.ts +182 -0
- package/server/engine/task/headless-pdf-capture-markdown.ts +47 -0
- package/server/engine/task/headless-pdf-capture.ts +39 -0
- package/server/engine/task/headless-pdf-open.ts +98 -0
- package/server/engine/task/headless-pdf-save.ts +88 -0
- package/server/engine/task/index.ts +9 -0
- package/server/engine/task/pdf-capture-util.ts +331 -0
- package/server/index.ts +3 -0
- package/server/tsconfig.json +10 -0
|
@@ -1,168 +1,57 @@
|
|
|
1
|
-
###
|
|
1
|
+
### 无头浏览器池连接器 (Headless Pool Connector)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
它可以管理多个浏览器会话,支持会话保持、Cookie 处理、基于 API 的登录等功能。
|
|
3
|
+
**无头浏览器池连接器**旨在管理无头浏览器实例的池。该连接器在执行需要多个无头浏览器会话同时运行的任务时非常有用,例如生成PDF或进行网页抓取。它能够有效地管理这些无头浏览器实例的创建、使用和销毁。
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
#### 端点
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
`无头浏览器池连接器`不要求特定的端点,但可以作为连接设置的一部分提供URI。此URI通常作为占位符使用,池在内部处理实际连接。也就是说,端点可以输入任意字符串。
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
例如,可以输入 `https://example.com` 以抓取该网站的内容,或者使用 `localhost` 生成 PDF。
|
|
9
|
+
#### 参数
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
- **min** (数字):
|
|
12
|
+
- 指定池中维持的最少无头浏览器实例数量。
|
|
13
|
+
- 默认值: `2`
|
|
14
|
+
- **max** (数字):
|
|
15
|
+
- 指定池中可以处理的最大无头浏览器实例数量。
|
|
16
|
+
- 默认值: `10`
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
这些参数允许根据预期的工作负载调整池的大小。例如,如果应用程序经常同时需要多个浏览器实例,则可以增加`max`值。
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
| 参数 | 类型 | 描述 | 默认值 |
|
|
20
|
-
| -------------------- | -------- | -------------------------------------------- | ------------- |
|
|
21
|
-
| `username` | `string` | 登录用户名(账户名) | `""` |
|
|
22
|
-
| `password` | `string` | 登录密码 | `""` |
|
|
23
|
-
| `loginPagePath` | `string` | 登录页面路径(如 `/login`) | `"/login"` |
|
|
24
|
-
| `loginApiUrl` | `string` | 登录 API 端点(如果没有则 `null`) | `null` |
|
|
25
|
-
| `usernameSelector` | `string` | 登录表单的用户名输入框 CSS 选择器 | `"#username"` |
|
|
26
|
-
| `passwordSelector` | `string` | 登录表单的密码输入框 CSS 选择器 | `"#password"` |
|
|
27
|
-
| `submitSelector` | `string` | 登录按钮的 CSS 选择器 | `"#submit"` |
|
|
28
|
-
| `successSelector` | `string` | 用于判断登录成功的元素 CSS 选择器 | `null` |
|
|
29
|
-
| `shadowDomSelectors` | `string` | 处理 Shadow DOM 内部元素的选择器(逗号分隔) | `""` |
|
|
30
|
-
| `timeout` | `number` | 登录及页面加载的超时时间(毫秒) | `15000` |
|
|
31
|
-
| `retries` | `number` | 登录重试次数 | `3` |
|
|
32
|
-
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
## 🔍 3. `loginApiUrl` 参数的详细说明
|
|
36
|
-
|
|
37
|
-
### 🔹 `loginApiUrl` 是什么?
|
|
38
|
-
|
|
39
|
-
`loginApiUrl` 是指用于网站 **API 登录** 的接口地址。如果网站不使用 API 方式登录,则不需要设置此参数。
|
|
40
|
-
|
|
41
|
-
### ✅ `loginApiUrl` 为空 (`null`)
|
|
42
|
-
|
|
43
|
-
此时 **使用 HTML 表单提交的方式进行登录**。
|
|
44
|
-
无头浏览器会直接模拟用户操作,如填写用户名和密码,并点击提交按钮。
|
|
45
|
-
|
|
46
|
-
#### 📌 示例(HTML 表单登录)
|
|
20
|
+
#### 设置示例
|
|
47
21
|
|
|
48
22
|
```json
|
|
49
23
|
{
|
|
50
|
-
"
|
|
51
|
-
"params": {
|
|
52
|
-
"username": "user123",
|
|
53
|
-
"password": "securepassword",
|
|
54
|
-
"loginPagePath": "/login",
|
|
55
|
-
"usernameSelector": "#username",
|
|
56
|
-
"passwordSelector": "#password",
|
|
57
|
-
"submitSelector": "#submit-button",
|
|
58
|
-
"successSelector": ".dashboard"
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
#### 🛠 处理过程:
|
|
64
|
-
|
|
65
|
-
1. 访问 `https://example.com/login`
|
|
66
|
-
2. 在 `#username` 和 `#password` 输入框中填入账户信息
|
|
67
|
-
3. 点击 `#submit-button` 提交表单
|
|
68
|
-
4. 通过 `.dashboard` 元素是否存在来判断是否登录成功
|
|
69
|
-
|
|
70
|
-
---
|
|
71
|
-
|
|
72
|
-
### ✅ `loginApiUrl` 设定时
|
|
73
|
-
|
|
74
|
-
如果目标网站使用 **API 方式登录**,则 `loginApiUrl` 需要设置为该 API 的地址。
|
|
75
|
-
例如,如果 API 登录地址是 `/api/auth/login`,则需要手动设置该参数。
|
|
76
|
-
|
|
77
|
-
#### 📌 示例(API 登录方式)
|
|
78
|
-
|
|
79
|
-
```json
|
|
80
|
-
{
|
|
81
|
-
"endpoint": "https://example.com",
|
|
82
|
-
"params": {
|
|
83
|
-
"username": "user123",
|
|
84
|
-
"password": "securepassword",
|
|
85
|
-
"loginPagePath": "/login",
|
|
86
|
-
"loginApiUrl": "/api/auth/login",
|
|
87
|
-
"usernameSelector": "#username",
|
|
88
|
-
"passwordSelector": "#password",
|
|
89
|
-
"submitSelector": "#submit-button",
|
|
90
|
-
"successSelector": ".dashboard"
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
#### 🛠 处理过程:
|
|
96
|
-
|
|
97
|
-
1. 发送 **HTTP POST 请求** 到 `https://example.com/api/auth/login`
|
|
98
|
-
2. 服务器返回成功响应,浏览器存储会话 Cookie
|
|
99
|
-
3. 访问主页面并检查 `.dashboard` 是否存在,以确认登录成功
|
|
100
|
-
|
|
101
|
-
---
|
|
102
|
-
|
|
103
|
-
## 🏗 4. 配置示例
|
|
104
|
-
|
|
105
|
-
```json
|
|
106
|
-
{
|
|
107
|
-
"name": "headless-browser-connector",
|
|
24
|
+
"name": "headless-browser-pool",
|
|
108
25
|
"connector": "headless-connector",
|
|
109
|
-
"endpoint": "
|
|
26
|
+
"endpoint": "1",
|
|
110
27
|
"params": {
|
|
111
|
-
"
|
|
112
|
-
"
|
|
113
|
-
"loginPagePath": "/login",
|
|
114
|
-
"loginApiUrl": "/api/auth/login",
|
|
115
|
-
"usernameSelector": "#username",
|
|
116
|
-
"passwordSelector": "#password",
|
|
117
|
-
"submitSelector": "#submit",
|
|
118
|
-
"successSelector": ".dashboard",
|
|
119
|
-
"timeout": 20000,
|
|
120
|
-
"retries": 5
|
|
28
|
+
"min": 5,
|
|
29
|
+
"max": 20
|
|
121
30
|
}
|
|
122
31
|
}
|
|
123
32
|
```
|
|
124
33
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
## 🔄 5. 连接生命周期(Connection Lifecycle)
|
|
128
|
-
|
|
129
|
-
- **connect**
|
|
34
|
+
在上述示例中,池至少保持5个无头浏览器实例,并且在需要时可以扩展到最多20个实例。
|
|
130
35
|
|
|
131
|
-
|
|
132
|
-
- 存储 Cookie,以便后续请求使用。
|
|
36
|
+
#### 连接生命周期
|
|
133
37
|
|
|
134
|
-
- **
|
|
135
|
-
- 关闭浏览器会话,并清除 Cookie。
|
|
38
|
+
- **connect**:
|
|
136
39
|
|
|
137
|
-
|
|
40
|
+
- 初始化连接,根据设定的参数(`min` 和 `max`)创建无头浏览器实例池。
|
|
41
|
+
- 池会自动管理这些实例的生命周期,必要时创建新的实例并重用现有的实例。
|
|
138
42
|
|
|
139
|
-
|
|
43
|
+
- **disconnect**:
|
|
44
|
+
- 销毁无头浏览器池,确保所有实例都被适当关闭并释放资源。
|
|
140
45
|
|
|
141
|
-
|
|
142
|
-
| ---------------------- | ---------------------- |
|
|
143
|
-
| `headless-pdf-capture` | 将 HTML 转换为 PDF |
|
|
144
|
-
| `headless-scrap` | 网页爬取,提取特定数据 |
|
|
145
|
-
| `headless-post` | 自动填写表单并提交 |
|
|
46
|
+
#### 使用场景
|
|
146
47
|
|
|
147
|
-
|
|
48
|
+
`无头浏览器池连接器`主要用于需要在无头浏览器环境中执行任务的场景。例如,生成HTML内容的PDF、捕获网页截图或从网站抓取数据。
|
|
148
49
|
|
|
149
|
-
|
|
50
|
+
#### 支持的任务
|
|
150
51
|
|
|
151
|
-
|
|
152
|
-
{
|
|
153
|
-
"name": "scrape-data",
|
|
154
|
-
"taskType": "headless-scrap",
|
|
155
|
-
"params": {
|
|
156
|
-
"path": "/products",
|
|
157
|
-
"selectors": [
|
|
158
|
-
{ "text": "商品名称", "value": ".product-title" },
|
|
159
|
-
{ "text": "价格", "value": ".product-price" }
|
|
160
|
-
]
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
```
|
|
52
|
+
`无头浏览器池连接器`支持以 `headless-pdf` 为前缀的任务。这些任务将使用由此连接器管理的无头浏览器实例。
|
|
164
53
|
|
|
165
|
-
|
|
54
|
+
#### 任务示例使用
|
|
166
55
|
|
|
167
56
|
```json
|
|
168
57
|
{
|
|
@@ -177,42 +66,6 @@
|
|
|
177
66
|
}
|
|
178
67
|
```
|
|
179
68
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
```json
|
|
183
|
-
{
|
|
184
|
-
"name": "submit-form",
|
|
185
|
-
"taskType": "headless-post",
|
|
186
|
-
"params": {
|
|
187
|
-
"formPath": "/contact",
|
|
188
|
-
"fields": {
|
|
189
|
-
"#name": "张三",
|
|
190
|
-
"#email": "zhangsan@example.com",
|
|
191
|
-
"#message": "你好,我想咨询一下!"
|
|
192
|
-
},
|
|
193
|
-
"submitSelector": "#submit-button"
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
---
|
|
199
|
-
|
|
200
|
-
## 📢 7. 说明(Description)
|
|
201
|
-
|
|
202
|
-
**无头连接器** 可用于 **自动化网页爬取、PDF 生成、表单提交** 等任务。
|
|
203
|
-
它支持 **HTML 登录** 和 **API 登录**,适用于各种不同的网页登录方式。
|
|
204
|
-
|
|
205
|
-
---
|
|
206
|
-
|
|
207
|
-
### 📌 功能总结
|
|
208
|
-
|
|
209
|
-
| 功能 | 说明 |
|
|
210
|
-
| ----------------- | ------------------------------------ |
|
|
211
|
-
| **HTML 表单登录** | 直接模拟用户操作(适用于大部分网页) |
|
|
212
|
-
| **API 登录** | 通过 API 发送登录请求(更快) |
|
|
213
|
-
| **会话保持** | 存储 Cookie 并维持登录状态 |
|
|
214
|
-
| **网页爬取** | 提取页面中的特定数据 |
|
|
215
|
-
| **PDF 生成** | 将 HTML 页面转换为 PDF |
|
|
216
|
-
| **表单提交** | 自动填充并提交表单 |
|
|
69
|
+
#### 说明
|
|
217
70
|
|
|
218
|
-
|
|
71
|
+
**无头浏览器池连接器**通过高效管理无头浏览器实例池,确保在高需求场景下执行PDF生成和网页抓取等任务时,资源利用的优化和可扩展性。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/integration-headless",
|
|
3
|
-
"version": "8.0.0
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"things-factory": true,
|
|
6
6
|
"author": "heartyoh <heartyoh@hatiolab.com>",
|
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
"clean": "npm run clean:server"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@things-factory/attachment-base": "^8.0.0
|
|
26
|
-
"@things-factory/board-service": "^8.0.0
|
|
27
|
-
"@things-factory/integration-base": "^8.0.0
|
|
28
|
-
"@things-factory/shell": "^8.0.0
|
|
25
|
+
"@things-factory/attachment-base": "^8.0.0",
|
|
26
|
+
"@things-factory/board-service": "^8.0.0",
|
|
27
|
+
"@things-factory/integration-base": "^8.0.0",
|
|
28
|
+
"@things-factory/shell": "^8.0.0",
|
|
29
29
|
"ejs": "^3.1.10",
|
|
30
30
|
"pdf-lib": "^1.17.1"
|
|
31
31
|
},
|
|
32
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "07ef27d272dd9a067a9648ac7013748510556a18"
|
|
33
33
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ConnectionManager, Connector } from '@things-factory/integration-base'
|
|
2
|
+
import { createHeadlessPool, destroyHeadlessPool } from './headless-pool'
|
|
3
|
+
|
|
4
|
+
export class HeadlessConnector implements Connector {
|
|
5
|
+
async ready(connectionConfigs) {
|
|
6
|
+
await Promise.all(connectionConfigs.map(this.connect.bind(this)))
|
|
7
|
+
|
|
8
|
+
ConnectionManager.logger.info('headless-connector connections are ready')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async connect(connection) {
|
|
12
|
+
var {
|
|
13
|
+
endpoint: uri = '1',
|
|
14
|
+
params: { min = 2, max = 10 }
|
|
15
|
+
} = connection
|
|
16
|
+
|
|
17
|
+
const headlessPool = createHeadlessPool({
|
|
18
|
+
min,
|
|
19
|
+
max,
|
|
20
|
+
testOnBorrow: true,
|
|
21
|
+
acquireTimeoutMillis: 15000
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
ConnectionManager.addConnectionInstance(connection, headlessPool)
|
|
25
|
+
|
|
26
|
+
ConnectionManager.logger.info(
|
|
27
|
+
`headless-connector connection(${connection.name}:${connection.endpoint}) is connected`
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async disconnect(connection) {
|
|
32
|
+
const headlessPool = ConnectionManager.getConnectionInstance(connection)
|
|
33
|
+
destroyHeadlessPool(headlessPool)
|
|
34
|
+
|
|
35
|
+
ConnectionManager.removeConnectionInstance(connection)
|
|
36
|
+
|
|
37
|
+
ConnectionManager.logger.info(`headless-connector connection(${connection.name}) is disconnected`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get parameterSpec() {
|
|
41
|
+
return [
|
|
42
|
+
{
|
|
43
|
+
type: 'number',
|
|
44
|
+
name: 'min',
|
|
45
|
+
label: 'pool-size-min'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: 'number',
|
|
49
|
+
name: 'max',
|
|
50
|
+
label: 'pool-size-max'
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get taskPrefixes() {
|
|
56
|
+
return ['headless-pdf']
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get description() {
|
|
60
|
+
return 'Headless Pool Connector'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get help() {
|
|
64
|
+
return 'integration/connector/headless-connector'
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
ConnectionManager.registerConnector('headless-connector', new HeadlessConnector())
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as genericPool from 'generic-pool'
|
|
2
|
+
|
|
3
|
+
import { config, logger } from '@things-factory/env'
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
var puppeteer = require('puppeteer')
|
|
7
|
+
} catch (err) {
|
|
8
|
+
logger.error(err)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createHeadlessPool(options: genericPool.Options) {
|
|
12
|
+
return genericPool.createPool(
|
|
13
|
+
{
|
|
14
|
+
create() {
|
|
15
|
+
console.log('headless instance in headless-pool-integration about to create')
|
|
16
|
+
return initializeChromium()
|
|
17
|
+
},
|
|
18
|
+
validate(browser) {
|
|
19
|
+
return Promise.race([
|
|
20
|
+
new Promise(res => setTimeout(() => res(false), 1500)),
|
|
21
|
+
browser
|
|
22
|
+
//@ts-ignore
|
|
23
|
+
.version()
|
|
24
|
+
.then(_ => true)
|
|
25
|
+
.catch(_ => false)
|
|
26
|
+
])
|
|
27
|
+
},
|
|
28
|
+
destroy(browser) {
|
|
29
|
+
//@ts-ignore
|
|
30
|
+
return browser.close()
|
|
31
|
+
}
|
|
32
|
+
} as genericPool.Factory<any>,
|
|
33
|
+
options
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function destroyHeadlessPool(headlessPool: genericPool.Pool<any>) {
|
|
38
|
+
if (headlessPool) {
|
|
39
|
+
console.log('headless-pool-integration about to destroy')
|
|
40
|
+
|
|
41
|
+
await headlessPool.drain()
|
|
42
|
+
await headlessPool.clear()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const CHROMIUM_PATH = config.get('CHROMIUM_PATH')
|
|
47
|
+
|
|
48
|
+
async function initializeChromium() {
|
|
49
|
+
try {
|
|
50
|
+
if (!puppeteer) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
var launchSetting = {
|
|
55
|
+
args: ['--hide-scrollbars', '--mute-audio', '--no-sandbox', '--use-gl=egl'],
|
|
56
|
+
headless: 'shell'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (CHROMIUM_PATH) {
|
|
60
|
+
launchSetting['executablePath'] = CHROMIUM_PATH
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const browser = await puppeteer.launch(launchSetting)
|
|
64
|
+
|
|
65
|
+
return browser
|
|
66
|
+
} catch (err) {
|
|
67
|
+
logger.error(err)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './headless-connector'
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { TaskRegistry } from '@things-factory/integration-base'
|
|
2
|
+
import { PDFCaptureUtil, getCommonParameterSpec } from './pdf-capture-util'
|
|
3
|
+
import { BoardFunc } from '@things-factory/board-service'
|
|
4
|
+
import { access } from '@things-factory/utils'
|
|
5
|
+
|
|
6
|
+
const PAGE_FORMATS = {
|
|
7
|
+
A4: { width: 595.28, height: 841.89 },
|
|
8
|
+
A3: { width: 841.89, height: 1190.55 },
|
|
9
|
+
Letter: { width: 612, height: 792 },
|
|
10
|
+
Legal: { width: 612, height: 1008 }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function convertToPixels(value: string): number {
|
|
14
|
+
const dpi = 96 // PDF에서 기본적으로 사용하는 DPI
|
|
15
|
+
|
|
16
|
+
if (value.endsWith('px')) {
|
|
17
|
+
return parseFloat(value)
|
|
18
|
+
} else if (value.endsWith('in')) {
|
|
19
|
+
const inches = parseFloat(value)
|
|
20
|
+
return inches * dpi
|
|
21
|
+
} else if (value.endsWith('cm')) {
|
|
22
|
+
const cm = parseFloat(value)
|
|
23
|
+
return cm * (dpi / 2.54)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return 0
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function HeadlessPDFCaptureBoard(step, context) {
|
|
30
|
+
const pdfUtil = new PDFCaptureUtil(context)
|
|
31
|
+
await pdfUtil.initBrowser(step.connection)
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
var {
|
|
35
|
+
board: boardObject,
|
|
36
|
+
accessor,
|
|
37
|
+
boardAccessor,
|
|
38
|
+
draft,
|
|
39
|
+
format = 'A4',
|
|
40
|
+
width,
|
|
41
|
+
height,
|
|
42
|
+
landscape,
|
|
43
|
+
marginLeft = 0,
|
|
44
|
+
marginRight = 0,
|
|
45
|
+
marginTop = 0,
|
|
46
|
+
marginBottom = 0
|
|
47
|
+
} = step.params
|
|
48
|
+
var { domain, data, user, logger } = context
|
|
49
|
+
|
|
50
|
+
const boardId = boardAccessor ? access(boardAccessor, data) : boardObject?.id
|
|
51
|
+
|
|
52
|
+
if (!boardId) {
|
|
53
|
+
throw new Error('The board property must be set')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const boardInput = access(accessor, data)
|
|
57
|
+
|
|
58
|
+
const { model, base } = await BoardFunc.headlessModel({ domain, id: boardId }, draft)
|
|
59
|
+
const [fontsToUse, fontStyles] = await BoardFunc.fonts(domain)
|
|
60
|
+
|
|
61
|
+
model.fonts = fontsToUse
|
|
62
|
+
model.fontStyles = fontStyles
|
|
63
|
+
|
|
64
|
+
var widthN = width ? convertToPixels(width) : 0
|
|
65
|
+
var heightN = height ? convertToPixels(height) : 0
|
|
66
|
+
|
|
67
|
+
if (!widthN && !heightN && PAGE_FORMATS[format]) {
|
|
68
|
+
const pageDimensions = PAGE_FORMATS[format]
|
|
69
|
+
widthN = pageDimensions.width * (96 / 72)
|
|
70
|
+
heightN = pageDimensions.height * (96 / 72)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (landscape && widthN && heightN) {
|
|
74
|
+
;[widthN, heightN] = [heightN, widthN]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
marginLeft = convertToPixels(marginLeft)
|
|
78
|
+
marginTop = convertToPixels(marginTop)
|
|
79
|
+
marginBottom = convertToPixels(marginBottom)
|
|
80
|
+
marginRight = convertToPixels(marginRight)
|
|
81
|
+
|
|
82
|
+
const contentWidth = widthN - marginLeft - marginRight
|
|
83
|
+
const contentHeight = heightN - marginTop - marginBottom
|
|
84
|
+
|
|
85
|
+
const page = await pdfUtil.browser!.newPage()
|
|
86
|
+
|
|
87
|
+
await page.setViewport({ width: Math.round(contentWidth), height: Math.round(contentHeight) })
|
|
88
|
+
await page.setRequestInterception(true)
|
|
89
|
+
await page.setDefaultTimeout(10000)
|
|
90
|
+
|
|
91
|
+
page.on('console', async msg => {
|
|
92
|
+
console.log(`[browser ${msg.type()}] ${msg.text()}`)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
page.on('requestfailed', request => {
|
|
96
|
+
console.log('Request failed:', request.url())
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const protocol = 'http'
|
|
100
|
+
const host = 'localhost'
|
|
101
|
+
const port = process.env.PORT ? `:${process.env.PORT}` : ''
|
|
102
|
+
const path = '/internal-board-service-view'
|
|
103
|
+
const url = `${protocol}://${host}${port}${path}`
|
|
104
|
+
|
|
105
|
+
const token = await user?.sign()
|
|
106
|
+
|
|
107
|
+
page.on('request', request => {
|
|
108
|
+
if (request.url() === url) {
|
|
109
|
+
request.continue({
|
|
110
|
+
method: 'POST',
|
|
111
|
+
headers: {
|
|
112
|
+
'Content-Type': 'application/json',
|
|
113
|
+
'x-things-factory-domain': domain?.subdomain,
|
|
114
|
+
Authorization: 'Bearer ' + token
|
|
115
|
+
},
|
|
116
|
+
postData: JSON.stringify({
|
|
117
|
+
model,
|
|
118
|
+
base
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
} else if (request.url().startsWith(`${protocol}://${host}${port}`)) {
|
|
122
|
+
request.continue({
|
|
123
|
+
headers: {
|
|
124
|
+
...request.headers(),
|
|
125
|
+
'x-things-factory-domain': domain?.subdomain,
|
|
126
|
+
Authorization: 'Bearer ' + token
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
} else {
|
|
130
|
+
request.continue()
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
await page.goto(url)
|
|
135
|
+
|
|
136
|
+
await page.evaluate(data => {
|
|
137
|
+
//@ts-ignore
|
|
138
|
+
s.data = data
|
|
139
|
+
return new Promise(resolve => {
|
|
140
|
+
requestAnimationFrame(() => resolve(0))
|
|
141
|
+
})
|
|
142
|
+
}, boardInput)
|
|
143
|
+
|
|
144
|
+
await pdfUtil.processPageAndGeneratePDF(step.params, null, page)
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
data: context.__headless_pdf
|
|
148
|
+
}
|
|
149
|
+
} catch (error) {
|
|
150
|
+
throw error
|
|
151
|
+
} finally {
|
|
152
|
+
await pdfUtil.closeBrowser()
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
HeadlessPDFCaptureBoard.parameterSpec = [
|
|
157
|
+
...getCommonParameterSpec(),
|
|
158
|
+
{
|
|
159
|
+
type: 'string',
|
|
160
|
+
name: 'boardAccessor',
|
|
161
|
+
label: 'board-accessor'
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
type: 'resource-object',
|
|
165
|
+
name: 'board',
|
|
166
|
+
label: 'board',
|
|
167
|
+
property: {
|
|
168
|
+
queryName: 'boards'
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
type: 'boolean',
|
|
173
|
+
name: 'draft',
|
|
174
|
+
label: 'board-draft',
|
|
175
|
+
defaultValue: false,
|
|
176
|
+
description: 'Set whether to get the current working version or the last released version'
|
|
177
|
+
}
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
HeadlessPDFCaptureBoard.help = 'integration/task/headless-pdf-capture-board'
|
|
181
|
+
|
|
182
|
+
TaskRegistry.registerTaskHandler('headless-pdf-capture-board', HeadlessPDFCaptureBoard)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { TaskRegistry } from '@things-factory/integration-base'
|
|
2
|
+
import { access } from '@things-factory/utils'
|
|
3
|
+
import { marked } from 'marked'
|
|
4
|
+
|
|
5
|
+
import { PDFCaptureUtil, getCommonParameterSpec } from './pdf-capture-util'
|
|
6
|
+
|
|
7
|
+
export async function HeadlessPDFCaptureMarkdown(step, context) {
|
|
8
|
+
const pdfUtil = new PDFCaptureUtil(context)
|
|
9
|
+
await pdfUtil.initBrowser(step.connection)
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const { accessor, markdownContent, markdownContentAccessor } = step.params
|
|
13
|
+
const templateInput = access(accessor, context.data)
|
|
14
|
+
const markdownTemplate = markdownContentAccessor ? access(markdownContentAccessor, context.data) : markdownContent
|
|
15
|
+
|
|
16
|
+
const renderedMarkdown = pdfUtil.renderTemplate(markdownTemplate, templateInput)
|
|
17
|
+
const htmlContent = marked(renderedMarkdown)
|
|
18
|
+
|
|
19
|
+
await pdfUtil.processPageAndGeneratePDF(step.params, htmlContent)
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
data: context.__headless_pdf
|
|
23
|
+
}
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw error
|
|
26
|
+
} finally {
|
|
27
|
+
await pdfUtil.closeBrowser()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
HeadlessPDFCaptureMarkdown.parameterSpec = [
|
|
32
|
+
...getCommonParameterSpec(),
|
|
33
|
+
{
|
|
34
|
+
type: 'scenario-step-input',
|
|
35
|
+
name: 'markdownContentAccessor',
|
|
36
|
+
label: 'markdown-content-accessor'
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: 'textarea',
|
|
40
|
+
name: 'markdownContent',
|
|
41
|
+
label: 'markdown-content'
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
HeadlessPDFCaptureMarkdown.help = 'integration/task/headless-pdf-capture-markdown'
|
|
46
|
+
|
|
47
|
+
TaskRegistry.registerTaskHandler('headless-pdf-capture-markdown', HeadlessPDFCaptureMarkdown)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { TaskRegistry } from '@things-factory/integration-base'
|
|
2
|
+
import { access } from '@things-factory/utils'
|
|
3
|
+
|
|
4
|
+
import { PDFCaptureUtil, getCommonParameterSpec } from './pdf-capture-util'
|
|
5
|
+
|
|
6
|
+
export async function HeadlessPDFCapture(step, context) {
|
|
7
|
+
const pdfUtil = new PDFCaptureUtil(context)
|
|
8
|
+
await pdfUtil.initBrowser(step.connection)
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const { accessor, htmlContent } = step.params
|
|
12
|
+
const templateInput = access(accessor, context.data)
|
|
13
|
+
|
|
14
|
+
const renderedHtmlContent = pdfUtil.renderTemplate(htmlContent, templateInput)
|
|
15
|
+
|
|
16
|
+
await pdfUtil.processPageAndGeneratePDF(step.params, renderedHtmlContent)
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
data: context.__headless_pdf
|
|
20
|
+
}
|
|
21
|
+
} catch (error) {
|
|
22
|
+
throw error
|
|
23
|
+
} finally {
|
|
24
|
+
await pdfUtil.closeBrowser()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
HeadlessPDFCapture.parameterSpec = [
|
|
29
|
+
...getCommonParameterSpec(),
|
|
30
|
+
{
|
|
31
|
+
type: 'textarea',
|
|
32
|
+
name: 'htmlContent',
|
|
33
|
+
label: 'html-content'
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
HeadlessPDFCapture.help = 'integration/task/headless-pdf-capture'
|
|
38
|
+
|
|
39
|
+
TaskRegistry.registerTaskHandler('headless-pdf-capture', HeadlessPDFCapture)
|