node-karin 0.0.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/LICENSE +674 -0
- package/README.md +57 -0
- package/config/defSet/App.yaml +37 -0
- package/config/defSet/config.yaml +43 -0
- package/config/defSet/group.yaml +18 -0
- package/config/defSet/pm2.yaml +21 -0
- package/config/defSet/redis.yaml +18 -0
- package/config/defSet/server.yaml +42 -0
- package/config/view/App.yaml +74 -0
- package/config/view/config.yaml +100 -0
- package/config/view/group.yaml +62 -0
- package/config/view/pm2.yaml +41 -0
- package/config/view/redis.yaml +25 -0
- package/config/view/server.yaml +93 -0
- package/lib/adapter/onebot/onebot11.d.ts +430 -0
- package/lib/adapter/onebot/onebot11.js +1302 -0
- package/lib/core/init.d.ts +0 -0
- package/lib/core/init.js +4 -0
- package/lib/core/karin.d.ts +72 -0
- package/lib/core/karin.js +51 -0
- package/lib/core/listener.d.ts +121 -0
- package/lib/core/listener.js +188 -0
- package/lib/core/plugin.app.d.ts +15 -0
- package/lib/core/plugin.app.js +18 -0
- package/lib/core/plugin.d.ts +182 -0
- package/lib/core/plugin.js +138 -0
- package/lib/core/plugin.loader.d.ts +149 -0
- package/lib/core/plugin.loader.js +462 -0
- package/lib/core/server.d.ts +26 -0
- package/lib/core/server.js +213 -0
- package/lib/db/level.d.ts +20 -0
- package/lib/db/level.js +38 -0
- package/lib/db/redis.d.ts +41 -0
- package/lib/db/redis.js +137 -0
- package/lib/db/redis_level.d.ts +113 -0
- package/lib/db/redis_level.js +290 -0
- package/lib/event/event.d.ts +138 -0
- package/lib/event/event.handler.d.ts +29 -0
- package/lib/event/event.handler.js +142 -0
- package/lib/event/event.js +120 -0
- package/lib/event/message.d.ts +102 -0
- package/lib/event/message.handler.d.ts +25 -0
- package/lib/event/message.handler.js +240 -0
- package/lib/event/message.js +70 -0
- package/lib/event/notice.d.ts +49 -0
- package/lib/event/notice.js +15 -0
- package/lib/event/request.d.ts +49 -0
- package/lib/event/request.js +15 -0
- package/lib/event/review.handler.d.ts +54 -0
- package/lib/event/review.handler.js +382 -0
- package/lib/index.d.ts +23 -0
- package/lib/index.js +40 -0
- package/lib/renderer/app.d.ts +53 -0
- package/lib/renderer/app.js +93 -0
- package/lib/renderer/base.d.ts +30 -0
- package/lib/renderer/base.js +71 -0
- package/lib/renderer/client.d.ts +30 -0
- package/lib/renderer/client.js +159 -0
- package/lib/renderer/http.d.ts +19 -0
- package/lib/renderer/http.js +51 -0
- package/lib/renderer/server.d.ts +42 -0
- package/lib/renderer/server.js +112 -0
- package/lib/renderer/wormhole.d.ts +1 -0
- package/lib/renderer/wormhole.js +154 -0
- package/lib/types/adapter.d.ts +575 -0
- package/lib/types/adapter.js +1 -0
- package/lib/types/config.d.ts +327 -0
- package/lib/types/config.js +1 -0
- package/lib/types/element.d.ts +576 -0
- package/lib/types/element.js +1 -0
- package/lib/types/index.d.ts +8 -0
- package/lib/types/index.js +8 -0
- package/lib/types/logger.d.ts +109 -0
- package/lib/types/logger.js +1 -0
- package/lib/types/onebots11.d.ts +1371 -0
- package/lib/types/onebots11.js +1 -0
- package/lib/types/plugin.d.ts +282 -0
- package/lib/types/plugin.js +1 -0
- package/lib/types/render.d.ts +111 -0
- package/lib/types/render.js +1 -0
- package/lib/types/reply.d.ts +40 -0
- package/lib/types/reply.js +1 -0
- package/lib/types/types.d.ts +898 -0
- package/lib/types/types.js +1 -0
- package/lib/utils/YamlEditor.d.ts +62 -0
- package/lib/utils/YamlEditor.js +208 -0
- package/lib/utils/button.d.ts +49 -0
- package/lib/utils/button.js +79 -0
- package/lib/utils/common.d.ts +123 -0
- package/lib/utils/common.js +413 -0
- package/lib/utils/config.d.ts +72 -0
- package/lib/utils/config.js +254 -0
- package/lib/utils/exec.d.ts +22 -0
- package/lib/utils/exec.js +36 -0
- package/lib/utils/ffmpeg.d.ts +12 -0
- package/lib/utils/ffmpeg.js +25 -0
- package/lib/utils/handler.d.ts +76 -0
- package/lib/utils/handler.js +102 -0
- package/lib/utils/logger.d.ts +3 -0
- package/lib/utils/logger.js +104 -0
- package/lib/utils/segment.d.ts +276 -0
- package/lib/utils/segment.js +448 -0
- package/lib/utils/update.d.ts +69 -0
- package/lib/utils/update.js +151 -0
- package/lib/utils/updateVersion.d.ts +33 -0
- package/lib/utils/updateVersion.js +145 -0
- package/package.json +92 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import RenderBase from './base.js';
|
|
3
|
+
import { KarinRenderType } from '../types/render.js';
|
|
4
|
+
export default class RenderClient extends RenderBase {
|
|
5
|
+
url: string;
|
|
6
|
+
type: string;
|
|
7
|
+
id: string;
|
|
8
|
+
index: number;
|
|
9
|
+
retry: number;
|
|
10
|
+
reg: RegExp;
|
|
11
|
+
ws: WebSocket;
|
|
12
|
+
constructor(url: string);
|
|
13
|
+
/**
|
|
14
|
+
* 初始化
|
|
15
|
+
*/
|
|
16
|
+
start(): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* 心跳
|
|
19
|
+
*/
|
|
20
|
+
heartbeat(): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* 接受消息
|
|
23
|
+
*/
|
|
24
|
+
message(str: string): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* 渲染标准方法
|
|
27
|
+
* @param options 渲染参数
|
|
28
|
+
*/
|
|
29
|
+
render(options: KarinRenderType): Promise<string | string[]>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import WebSocket from 'ws'
|
|
3
|
+
import Renderer from './app.js'
|
|
4
|
+
import RenderBase from './base.js'
|
|
5
|
+
import { randomUUID } from 'crypto'
|
|
6
|
+
import common from '../utils/common.js'
|
|
7
|
+
import logger from '../utils/logger.js'
|
|
8
|
+
import listener from '../core/listener.js'
|
|
9
|
+
export default class RenderClient extends RenderBase {
|
|
10
|
+
url
|
|
11
|
+
type
|
|
12
|
+
id
|
|
13
|
+
index
|
|
14
|
+
retry
|
|
15
|
+
reg
|
|
16
|
+
ws
|
|
17
|
+
constructor (url) {
|
|
18
|
+
super()
|
|
19
|
+
this.url = url
|
|
20
|
+
this.type = 'image'
|
|
21
|
+
this.id = 'puppeteer'
|
|
22
|
+
this.index = 0
|
|
23
|
+
this.retry = 0
|
|
24
|
+
this.reg = new RegExp(`(${process.cwd().replace(/\\/g, '\\\\')}|${process.cwd().replace(/\\/g, '/')})`, 'g')
|
|
25
|
+
/** 连接ws */
|
|
26
|
+
this.ws = new WebSocket(this.url)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 初始化
|
|
31
|
+
*/
|
|
32
|
+
async start () {
|
|
33
|
+
/** 建立连接 */
|
|
34
|
+
this.ws.on('open', () => {
|
|
35
|
+
logger.mark(`[渲染器:${this.id}][WebSocket] 建立连接:${logger.green(this.url)}`)
|
|
36
|
+
/** 注册渲染器 */
|
|
37
|
+
try {
|
|
38
|
+
this.index = Renderer.app({ id: this.id, type: this.type, render: this.render.bind(this) })
|
|
39
|
+
this.retry = 0
|
|
40
|
+
} catch (error) {
|
|
41
|
+
logger.error(`[渲染器:${this.id}] 注册渲染器失败:`, error)
|
|
42
|
+
/** 断开连接 */
|
|
43
|
+
this.ws.close()
|
|
44
|
+
}
|
|
45
|
+
/** 心跳 */
|
|
46
|
+
this.heartbeat()
|
|
47
|
+
/** 监听消息 */
|
|
48
|
+
this.ws.on('message', data => this.message(data.toString()))
|
|
49
|
+
})
|
|
50
|
+
/** 监听断开 */
|
|
51
|
+
this.ws.once('close', async () => {
|
|
52
|
+
this.retry++
|
|
53
|
+
/** 停止监听 */
|
|
54
|
+
this.ws.removeAllListeners()
|
|
55
|
+
/** 卸载渲染器 */
|
|
56
|
+
this.index && Renderer.unapp(this.index) && (this.index = 0)
|
|
57
|
+
logger.warn(`[渲染器:${this.id}][重连次数:${this.retry}] 连接断开,5秒后将尝试重连:${this.url}`)
|
|
58
|
+
await common.sleep(5000)
|
|
59
|
+
await this.start()
|
|
60
|
+
})
|
|
61
|
+
/** 监听错误 */
|
|
62
|
+
this.ws.on('error', async (e) => {
|
|
63
|
+
logger.debug(e)
|
|
64
|
+
await common.sleep(5000)
|
|
65
|
+
this.ws.close()
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 心跳
|
|
71
|
+
*/
|
|
72
|
+
async heartbeat () {
|
|
73
|
+
/** 无限循环 错误则停止 */
|
|
74
|
+
while (true) {
|
|
75
|
+
try {
|
|
76
|
+
this.ws.send(JSON.stringify({ action: 'heartbeat' }))
|
|
77
|
+
logger.debug(`[渲染器:${this.id}] 心跳:${this.url}`)
|
|
78
|
+
} catch (e) {
|
|
79
|
+
logger.debug(`[渲染器:${this.id}] 心跳失败:`, e)
|
|
80
|
+
this.ws.close()
|
|
81
|
+
break
|
|
82
|
+
}
|
|
83
|
+
await common.sleep(5000)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 接受消息
|
|
89
|
+
*/
|
|
90
|
+
async message (str) {
|
|
91
|
+
const data = JSON.parse(str)
|
|
92
|
+
switch (data.action) {
|
|
93
|
+
/** 静态文件 */
|
|
94
|
+
case 'static': {
|
|
95
|
+
const filePath = decodeURIComponent(data.params.file)
|
|
96
|
+
logger.debug(`[渲染器:${this.id}][正向WS] 访问静态文件:${filePath}`)
|
|
97
|
+
const file = fs.readFileSync('.' + filePath)
|
|
98
|
+
const params = {
|
|
99
|
+
echo: data.echo,
|
|
100
|
+
action: 'static',
|
|
101
|
+
status: 'ok',
|
|
102
|
+
data: { file },
|
|
103
|
+
}
|
|
104
|
+
return this.ws.send(JSON.stringify(params))
|
|
105
|
+
}
|
|
106
|
+
/** 渲染结果 */
|
|
107
|
+
case 'renderRes': {
|
|
108
|
+
listener.emit(data.echo, data)
|
|
109
|
+
break
|
|
110
|
+
}
|
|
111
|
+
/** 未知数据 */
|
|
112
|
+
default: {
|
|
113
|
+
logger.warn(`[渲染器:${this.id}] 收到未知数据:`, data)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 渲染标准方法
|
|
120
|
+
* @param options 渲染参数
|
|
121
|
+
*/
|
|
122
|
+
async render (options) {
|
|
123
|
+
/** 渲染模板 */
|
|
124
|
+
let file = options.file
|
|
125
|
+
let action = 'renderHtml'
|
|
126
|
+
if (options.file.includes('http') || options.vue) {
|
|
127
|
+
action = 'render'
|
|
128
|
+
} else {
|
|
129
|
+
file = this.dealTpl(options)
|
|
130
|
+
/** 判断是本地karin-puppeteer还是远程 */
|
|
131
|
+
if (!/127\.0\.0\.1|localhost/.test(this.url)) {
|
|
132
|
+
file = fs.readFileSync(file, 'utf-8').replace(this.reg, '')
|
|
133
|
+
} else {
|
|
134
|
+
action = 'render'
|
|
135
|
+
file = 'file://' + file
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!file) {
|
|
139
|
+
logger.error(`[渲染器:${this.id}:${this.index}] 渲染文件不存在:${options.file}`)
|
|
140
|
+
return ''
|
|
141
|
+
}
|
|
142
|
+
/** 编码 */
|
|
143
|
+
file = encodeURIComponent(file)
|
|
144
|
+
const data = options
|
|
145
|
+
const echo = randomUUID()
|
|
146
|
+
/** 移除掉模板参数 */
|
|
147
|
+
if (data.data) { delete data.data }
|
|
148
|
+
data.file = file
|
|
149
|
+
const req = JSON.stringify({ echo, action, data })
|
|
150
|
+
logger.debug(`[渲染器:${this.id}:${this.index}][正向WS] 请求:${this.url} \nhtml: ${options.file} \ndata: ${JSON.stringify(data)}`)
|
|
151
|
+
this.ws.send(req)
|
|
152
|
+
return new Promise((resolve, reject) => {
|
|
153
|
+
listener.once(echo, (data) => {
|
|
154
|
+
if (data.ok) { return resolve(data.data) }
|
|
155
|
+
reject(new Error(JSON.stringify(data)))
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import RenderBase from './base.js';
|
|
2
|
+
import { KarinRenderType } from '../types/render.js';
|
|
3
|
+
export default class HttpRenderer extends RenderBase {
|
|
4
|
+
id: string;
|
|
5
|
+
host: string;
|
|
6
|
+
url: string;
|
|
7
|
+
token: string;
|
|
8
|
+
/**
|
|
9
|
+
* 构造函数
|
|
10
|
+
* @param host - 静态服务器地址
|
|
11
|
+
* @param url - 渲染接口
|
|
12
|
+
* @param token - token
|
|
13
|
+
*/
|
|
14
|
+
constructor(host: string, url: string, token: string);
|
|
15
|
+
/**
|
|
16
|
+
* 渲染
|
|
17
|
+
*/
|
|
18
|
+
render(options: KarinRenderType): Promise<string | Array<string>>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import RenderBase from './base.js'
|
|
3
|
+
import logger from '../utils/logger.js'
|
|
4
|
+
export default class HttpRenderer extends RenderBase {
|
|
5
|
+
id
|
|
6
|
+
host
|
|
7
|
+
url
|
|
8
|
+
token
|
|
9
|
+
/**
|
|
10
|
+
* 构造函数
|
|
11
|
+
* @param host - 静态服务器地址
|
|
12
|
+
* @param url - 渲染接口
|
|
13
|
+
* @param token - token
|
|
14
|
+
*/
|
|
15
|
+
constructor (host, url, token) {
|
|
16
|
+
super()
|
|
17
|
+
this.id = 'puppeteer'
|
|
18
|
+
this.host = host
|
|
19
|
+
this.url = url
|
|
20
|
+
this.token = token
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 渲染
|
|
25
|
+
*/
|
|
26
|
+
async render (options) {
|
|
27
|
+
const name = options.name || 'render'
|
|
28
|
+
let file = options.file
|
|
29
|
+
/** 非http渲染模板并转为http静态资源 */
|
|
30
|
+
if (!options.file.includes('http') && !options.vue) {
|
|
31
|
+
const isLocalhost = this.host.includes('127.0.0.1') || this.host.includes('localhost')
|
|
32
|
+
file = this.dealTpl(options, isLocalhost)
|
|
33
|
+
if (!file) {
|
|
34
|
+
logger.error(`[渲染器:${this.id}] 模板文件不存在:${name}`)
|
|
35
|
+
return ''
|
|
36
|
+
}
|
|
37
|
+
options.file = isLocalhost ? 'file://' + file : `${this.host}/api/renderHtml?html=${file}`
|
|
38
|
+
}
|
|
39
|
+
delete options.data
|
|
40
|
+
const data = options
|
|
41
|
+
const headers = {
|
|
42
|
+
Authorization: this.token,
|
|
43
|
+
}
|
|
44
|
+
logger.debug(`[渲染器:${this.id}][POST] \n请求:${this.url} \nhtml: ${options.file} \ndata: ${JSON.stringify(data)}`)
|
|
45
|
+
const res = await axios({ method: 'post', url: this.url, headers, data })
|
|
46
|
+
if (res.status === 200 && res.data.ok) {
|
|
47
|
+
return res.data.data
|
|
48
|
+
}
|
|
49
|
+
throw new Error(`渲染失败:${JSON.stringify(res.data)}`)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import RenderBase from './base.js';
|
|
3
|
+
import WebSocket from 'ws';
|
|
4
|
+
import { IncomingMessage } from 'http';
|
|
5
|
+
import { KarinRenderType } from '../types/render.js';
|
|
6
|
+
declare class Puppeteer extends RenderBase {
|
|
7
|
+
socket: WebSocket;
|
|
8
|
+
id: any;
|
|
9
|
+
type: any;
|
|
10
|
+
host: any;
|
|
11
|
+
url: string;
|
|
12
|
+
index: number;
|
|
13
|
+
constructor();
|
|
14
|
+
server(socket: WebSocket, request: IncomingMessage): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* 渲染模板
|
|
17
|
+
* @param {object} options 渲染参数
|
|
18
|
+
* @param {string} options.file http地址或本地文件路径
|
|
19
|
+
* @param {string} [options.name] 模板名称
|
|
20
|
+
* @param {string} [options.fileID] art-template后的文件名
|
|
21
|
+
* @param {object} [options.data] 传递给模板的数据 template.render(data)
|
|
22
|
+
* @param {'png'|'jpeg'|'webp'} [options.type] 截图类型 默认'webp'
|
|
23
|
+
* @param {number} [options.quality] 截图质量 默认90 1-100
|
|
24
|
+
* @param {boolean} options.omitBackground 是否隐藏背景 默认false
|
|
25
|
+
* @param {object} [options.setViewport] 设置视窗大小和设备像素比 默认1920*1080、1
|
|
26
|
+
* @param {number} [options.setViewport.width] 视窗宽度
|
|
27
|
+
* @param {number} [options.setViewport.height] 视窗高度
|
|
28
|
+
* @param {string} [options.setViewport.deviceScaleFactor] 设备像素比
|
|
29
|
+
* @param {number|boolean} [options.multiPage] 分页截图 传递数字则视为视窗高度 返回数组
|
|
30
|
+
* @param {object} [options.pageGotoParams] 页面goto时的参数
|
|
31
|
+
* @param {number} [options.pageGotoParams.timeout] 页面加载超时时间
|
|
32
|
+
* @param {'load'|'domcontentloaded'|'networkidle0'|'networkidle2'} [options.pageGotoParams.waitUntil] 页面加载状态
|
|
33
|
+
* @returns {Promise<string|string[]>} 返回图片base64或数组
|
|
34
|
+
*/
|
|
35
|
+
render(options: KarinRenderType): Promise<string | string[]>;
|
|
36
|
+
}
|
|
37
|
+
declare const _default: {
|
|
38
|
+
type: string;
|
|
39
|
+
path: string;
|
|
40
|
+
adapter: typeof Puppeteer;
|
|
41
|
+
};
|
|
42
|
+
export default _default;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import Renderer from './app.js'
|
|
2
|
+
import RenderBase from './base.js'
|
|
3
|
+
import { randomUUID } from 'crypto'
|
|
4
|
+
import logger from '../utils/logger.js'
|
|
5
|
+
import listener from '../core/listener.js'
|
|
6
|
+
class Puppeteer extends RenderBase {
|
|
7
|
+
socket
|
|
8
|
+
id
|
|
9
|
+
type
|
|
10
|
+
host
|
|
11
|
+
url
|
|
12
|
+
index
|
|
13
|
+
constructor () {
|
|
14
|
+
super()
|
|
15
|
+
this.id = 0
|
|
16
|
+
this.index = 0
|
|
17
|
+
this.type = ''
|
|
18
|
+
this.host = ''
|
|
19
|
+
this.url = ''
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async server (socket, request) {
|
|
23
|
+
this.socket = socket
|
|
24
|
+
this.id = request.headers['renderer-id']
|
|
25
|
+
this.type = request.headers['renderer-type']
|
|
26
|
+
/** 注册渲染器 */
|
|
27
|
+
this.host = request.headers.host
|
|
28
|
+
this.url = `ws://${this.host + request.url}`
|
|
29
|
+
logger.info(`[渲染器:${this.id}] 收到新的连接请求:` + logger.green(this.url))
|
|
30
|
+
/** 监听上报事件 */
|
|
31
|
+
this.socket.on('message', data => {
|
|
32
|
+
const json = JSON.parse(data.toString())
|
|
33
|
+
if (json.echo) {
|
|
34
|
+
listener.emit(json.echo, json)
|
|
35
|
+
} else if (json.action === 'heartbeat') {
|
|
36
|
+
logger.debug(`[渲染器:${this.id}] 收到心跳:${this.url}`)
|
|
37
|
+
} else {
|
|
38
|
+
logger.warn(`[渲染器:${this.id}] 收到未知数据:`, data)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
/** 监听断开 */
|
|
42
|
+
this.socket.on('close', () => {
|
|
43
|
+
logger.warn(`[渲染器:${this.id}] 连接断开:${this.url}`)
|
|
44
|
+
/** 卸载渲染器 */
|
|
45
|
+
this.index && Renderer.unapp(this.index)
|
|
46
|
+
this.index = 0
|
|
47
|
+
})
|
|
48
|
+
/** 注册渲染器 */
|
|
49
|
+
try {
|
|
50
|
+
const index = Renderer.app({
|
|
51
|
+
id: this.id,
|
|
52
|
+
type: this.type,
|
|
53
|
+
render: this.render.bind(this),
|
|
54
|
+
})
|
|
55
|
+
this.index = index
|
|
56
|
+
} catch (error) {
|
|
57
|
+
logger.error(`[渲染器:${this.id}] 注册渲染器失败:`, error)
|
|
58
|
+
/** 断开连接 */
|
|
59
|
+
this.socket.close()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 渲染模板
|
|
65
|
+
* @param {object} options 渲染参数
|
|
66
|
+
* @param {string} options.file http地址或本地文件路径
|
|
67
|
+
* @param {string} [options.name] 模板名称
|
|
68
|
+
* @param {string} [options.fileID] art-template后的文件名
|
|
69
|
+
* @param {object} [options.data] 传递给模板的数据 template.render(data)
|
|
70
|
+
* @param {'png'|'jpeg'|'webp'} [options.type] 截图类型 默认'webp'
|
|
71
|
+
* @param {number} [options.quality] 截图质量 默认90 1-100
|
|
72
|
+
* @param {boolean} options.omitBackground 是否隐藏背景 默认false
|
|
73
|
+
* @param {object} [options.setViewport] 设置视窗大小和设备像素比 默认1920*1080、1
|
|
74
|
+
* @param {number} [options.setViewport.width] 视窗宽度
|
|
75
|
+
* @param {number} [options.setViewport.height] 视窗高度
|
|
76
|
+
* @param {string} [options.setViewport.deviceScaleFactor] 设备像素比
|
|
77
|
+
* @param {number|boolean} [options.multiPage] 分页截图 传递数字则视为视窗高度 返回数组
|
|
78
|
+
* @param {object} [options.pageGotoParams] 页面goto时的参数
|
|
79
|
+
* @param {number} [options.pageGotoParams.timeout] 页面加载超时时间
|
|
80
|
+
* @param {'load'|'domcontentloaded'|'networkidle0'|'networkidle2'} [options.pageGotoParams.waitUntil] 页面加载状态
|
|
81
|
+
* @returns {Promise<string|string[]>} 返回图片base64或数组
|
|
82
|
+
*/
|
|
83
|
+
async render (options) {
|
|
84
|
+
/** 渲染模板 */
|
|
85
|
+
let file = ''
|
|
86
|
+
if (options.file.includes('http') || options.vue) {
|
|
87
|
+
file = options.file
|
|
88
|
+
} else {
|
|
89
|
+
file = 'file://' + this.dealTpl(options)
|
|
90
|
+
}
|
|
91
|
+
if (!file) { throw new Error(`[渲染器:${this.id}] 模板文件不存在:${options.name}`) }
|
|
92
|
+
const echo = randomUUID()
|
|
93
|
+
const action = 'render'
|
|
94
|
+
const data = options
|
|
95
|
+
/** 移除掉模板参数 */
|
|
96
|
+
if (data.data) { delete data.data }
|
|
97
|
+
data.file = file
|
|
98
|
+
logger.debug(`[渲染器:${this.id}][反向WS] \n请求:${this.url} \nhtml: ${options.file} \ndata: ${JSON.stringify(data)}`)
|
|
99
|
+
this.socket.send(JSON.stringify({ echo, action, data }))
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
listener.once(echo, data => {
|
|
102
|
+
if (data.ok) { return resolve(data.data) }
|
|
103
|
+
reject(new Error(JSON.stringify(data)))
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export default {
|
|
109
|
+
type: 'render',
|
|
110
|
+
path: '/puppeteer',
|
|
111
|
+
adapter: Puppeteer,
|
|
112
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function connect(): void;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import WebSocket from 'ws'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { URL } from 'url'
|
|
5
|
+
import HttpRenderer from './http.js'
|
|
6
|
+
import config from '../utils/config.js'
|
|
7
|
+
import { render } from '../index.js'
|
|
8
|
+
let ws
|
|
9
|
+
let reConnect
|
|
10
|
+
const chunkSize = 1024 * 1024 * 3 // 文件分片大小
|
|
11
|
+
export default function connect () {
|
|
12
|
+
let heartbeat
|
|
13
|
+
let index = 0
|
|
14
|
+
reConnect = undefined
|
|
15
|
+
const wsUrl = config.Server.HttpRender.WormholeClient
|
|
16
|
+
ws = new WebSocket(wsUrl)
|
|
17
|
+
ws.on('open', function open () {
|
|
18
|
+
logger.info('连接到wormhole服务器' + wsUrl)
|
|
19
|
+
// 发送心跳
|
|
20
|
+
heartbeat = setInterval(() => {
|
|
21
|
+
ws.send(JSON.stringify({ type: 'heartbeat', date: new Date() }))
|
|
22
|
+
}, 30000) // 每30秒发送一次心跳
|
|
23
|
+
})
|
|
24
|
+
ws.on('message', msg => {
|
|
25
|
+
let data = ''
|
|
26
|
+
try {
|
|
27
|
+
data = JSON.parse(msg.toString())
|
|
28
|
+
} catch (error) {
|
|
29
|
+
logger.warn(`收到非法消息${data}`)
|
|
30
|
+
}
|
|
31
|
+
const echo = data.echo
|
|
32
|
+
switch (data.type) {
|
|
33
|
+
case 'msg': {
|
|
34
|
+
const { post, token, WormholeClient } = config.Server.HttpRender
|
|
35
|
+
const parsedUrl = new URL(WormholeClient)
|
|
36
|
+
const { hostname, port } = parsedUrl
|
|
37
|
+
const ishttps = WormholeClient.includes('wss://')
|
|
38
|
+
const host = `${ishttps ? 'https' : 'http'}://${hostname}${port ? `:${port}` : ''}/web/${data.date}`
|
|
39
|
+
logger.mark(`web渲染器已连接,地址:${host}`)
|
|
40
|
+
/** 注册渲染器 */
|
|
41
|
+
const rd = new HttpRenderer(host, post, token)
|
|
42
|
+
index = render.app({ id: 'puppeteer', type: 'image', render: rd.render.bind(rd) })
|
|
43
|
+
break
|
|
44
|
+
}
|
|
45
|
+
case 'web':
|
|
46
|
+
if (data.path) {
|
|
47
|
+
const filePath = data.path
|
|
48
|
+
const query = data.query
|
|
49
|
+
if (query.html) {
|
|
50
|
+
ws.send(JSON.stringify({ type: 'web', command: 'redirect', path: filePath, target: query.html.startsWith('/') ? query.html.slice(1) : query.html, echo }))
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
const list = ['.css', '.html', '.ttf', '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.ico', '.woff', '.woff2']
|
|
54
|
+
if (!list.some(ext => path.extname(filePath).endsWith(ext))) {
|
|
55
|
+
logger.warn(`拦截非资源文件${filePath}`)
|
|
56
|
+
ws.send(JSON.stringify({ type: 'web', state: 'error', error: '非资源文件', echo }))
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
/** 判断一下是否为html 如果是需要特殊处理 */
|
|
60
|
+
if (path.extname(filePath) === '.html') {
|
|
61
|
+
let html = filePath
|
|
62
|
+
/** 获取文件路径 对路径进行处理,去掉../、./ */
|
|
63
|
+
html = `./${html.replace(/\\/g, '/').replace(/(\.\/|\.\.\/)/g, '')}`
|
|
64
|
+
/** 判断是否为html文件且路径存在 */
|
|
65
|
+
if (!fs.existsSync(html)) {
|
|
66
|
+
return ws.send(JSON.stringify({ type: 'web', state: 'error', error: '文件不存在', echo }))
|
|
67
|
+
}
|
|
68
|
+
let content = fs.readFileSync(html, 'utf-8')
|
|
69
|
+
/** 处理所有绝对路径、相对路径 */
|
|
70
|
+
content = content.replace(new RegExp(`(${process.cwd()}|${process.cwd().replace(/\\/g, '/')})`, 'g'), '')
|
|
71
|
+
// 保存到本地
|
|
72
|
+
// filePath = './1.html'
|
|
73
|
+
// fs.writeFileSync(filePath, content, 'utf-8')
|
|
74
|
+
return ws.send(JSON.stringify({
|
|
75
|
+
type: 'web',
|
|
76
|
+
path: data.path,
|
|
77
|
+
command: 'resource',
|
|
78
|
+
data: Buffer.from(content),
|
|
79
|
+
state: 'complete',
|
|
80
|
+
part: 0,
|
|
81
|
+
echo,
|
|
82
|
+
}))
|
|
83
|
+
}
|
|
84
|
+
logger.info(`获取网页文件数据:${filePath}`)
|
|
85
|
+
// 获取文件
|
|
86
|
+
const stream = fs.createReadStream(filePath, { highWaterMark: chunkSize })
|
|
87
|
+
let part = 0
|
|
88
|
+
stream.on('data', chunk => {
|
|
89
|
+
part++
|
|
90
|
+
const message = {
|
|
91
|
+
type: 'web',
|
|
92
|
+
path: data.path,
|
|
93
|
+
command: 'resource',
|
|
94
|
+
data: chunk,
|
|
95
|
+
state: 'part',
|
|
96
|
+
part,
|
|
97
|
+
echo,
|
|
98
|
+
}
|
|
99
|
+
ws.send(JSON.stringify(message))
|
|
100
|
+
})
|
|
101
|
+
stream.on('end', () => {
|
|
102
|
+
part++
|
|
103
|
+
// 如果是最后一片段,则更新状态为 'complete'
|
|
104
|
+
if (stream.readableEnded) {
|
|
105
|
+
ws.send(JSON.stringify({
|
|
106
|
+
type: 'web',
|
|
107
|
+
path: data.path,
|
|
108
|
+
command: 'resource',
|
|
109
|
+
data: '',
|
|
110
|
+
state: 'complete',
|
|
111
|
+
part,
|
|
112
|
+
echo,
|
|
113
|
+
}))
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
stream.on('error', err => {
|
|
117
|
+
ws.send(JSON.stringify({ type: 'web', state: 'error', error: err.message, echo }))
|
|
118
|
+
})
|
|
119
|
+
} else {
|
|
120
|
+
ws.send(JSON.stringify({ type: 'web', state: 'error', error: '错误的文件路径', echo }))
|
|
121
|
+
}
|
|
122
|
+
break
|
|
123
|
+
default:
|
|
124
|
+
logger.warn(`未知消息类型${JSON.stringify(data)}`)
|
|
125
|
+
break
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
ws.on('close', function close () {
|
|
129
|
+
/** 卸载渲染器 */
|
|
130
|
+
index && render.unapp(index)
|
|
131
|
+
index = 0
|
|
132
|
+
if (heartbeat) {
|
|
133
|
+
clearInterval(heartbeat)
|
|
134
|
+
heartbeat = null
|
|
135
|
+
}
|
|
136
|
+
logger.warn('连接关闭,10秒后尝试重新连接')
|
|
137
|
+
if (!reConnect) {
|
|
138
|
+
reConnect = setTimeout(connect, 10000)
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
ws.on('error', function error () {
|
|
142
|
+
/** 卸载渲染器 */
|
|
143
|
+
index && render.unapp(index)
|
|
144
|
+
index = 0
|
|
145
|
+
if (heartbeat) {
|
|
146
|
+
clearInterval(heartbeat)
|
|
147
|
+
heartbeat = null
|
|
148
|
+
}
|
|
149
|
+
logger.warn('连接错误,10秒后尝试重新连接')
|
|
150
|
+
if (!reConnect) {
|
|
151
|
+
reConnect = setTimeout(connect, 10000)
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
}
|