@shangxueink/koishi-plugin-puppeteer-without-canvas
Version:
Browser API service for Koishi
836 lines (820 loc) • 32.7 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
SVG: () => SVG,
Tag: () => Tag,
default: () => src_default
});
module.exports = __toCommonJS(src_exports);
var import_puppeteer_core = __toESM(require("puppeteer-core"));
var import_puppeteer_finder = __toESM(require("puppeteer-finder"));
var import_koishi3 = require("koishi");
// src/svg.ts
var import_koishi = require("koishi");
function hyphenate(source) {
const result = {};
for (const key in source) {
result[key.replace(/[A-Z]/g, (str) => "-" + str.toLowerCase())] = source[key];
}
return result;
}
__name(hyphenate, "hyphenate");
function escapeHtml(source) {
return source.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
}
__name(escapeHtml, "escapeHtml");
var Tag = class _Tag {
constructor(tag) {
this.tag = tag;
}
static {
__name(this, "Tag");
}
parent;
children = [];
attributes = {};
innerText = "";
child(tag) {
const child = new _Tag(tag);
child.parent = this;
this.children.push(child);
return child;
}
attr(attributes) {
this.attributes = {
...this.attributes,
...attributes
};
return this;
}
data(innerText) {
this.innerText = innerText;
return this;
}
line(x1, y1, x2, y2, attr = {}) {
this.child("line").attr({ ...hyphenate(attr), x1, y1, x2, y2 });
return this;
}
circle(cx, cy, r, attr = {}) {
this.child("circle").attr({ ...hyphenate(attr), cx, cy, r });
return this;
}
rect(x1, y1, x2, y2, attr = {}) {
this.child("rect").attr({ ...hyphenate(attr), x: x1, y: y1, width: y2 - y1, height: x2 - x1 });
return this;
}
text(text, x, y, attr = {}) {
this.child("text").attr({ ...hyphenate(attr), x, y }).data(text);
return this;
}
g(attr = {}) {
return this.child("g").attr(hyphenate(attr));
}
get outer() {
const attrText = Object.keys(this.attributes).map((key) => ` ${key}="${escapeHtml(String(this.attributes[key]))}"`).join("");
return `<${this.tag}${attrText}>${this.inner}</${this.tag}>`;
}
get inner() {
return this.children.length ? this.children.map((child) => child.outer).join("") : this.innerText;
}
};
var SVG = class extends Tag {
static {
__name(this, "SVG");
}
view;
width;
height;
constructor(options = {}) {
super("svg");
const { size = 200, viewSize = size, width = size, height = size } = options;
this.width = width;
this.height = height;
const ratio = viewSize / size;
const { left = 0, top = 0, bottom = height * ratio, right = width * ratio } = options.viewBox || {};
this.view = { left, bottom, top, right };
this.attr({
width,
height,
viewBox: `${left} ${top} ${right} ${bottom}`,
xmlns: "http://www.w3.org/2000/svg",
version: "1.1"
});
}
fill(color) {
this.rect(this.view.top, this.view.left, this.view.bottom, this.view.right, { style: `fill: ${color}` });
return this;
}
async render(ctx) {
const page = await ctx.puppeteer.page();
await page.setContent(this.outer);
const buffer = await page.screenshot({
clip: {
x: 0,
y: 0,
width: this.width,
height: this.height
}
});
page.close();
return import_koishi.h.image(buffer, "image/png");
}
};
// src/canvas.ts
var import_canvas = __toESM(require("@koishijs/canvas"));
var import_koishi2 = require("koishi");
var import_path = require("path");
var import_url = require("url");
var kElement = Symbol("element");
var BaseElement = class {
constructor(page, id) {
this.page = page;
this.id = id;
}
static {
__name(this, "BaseElement");
}
[kElement] = true;
get selector() {
return `document.querySelector("#${this.id}")`;
}
async dispose() {
await this.page.evaluate(`${this.selector}?.remove()`);
this.id = null;
}
};
var CanvasElement = class extends BaseElement {
constructor(page, id, width, height, fontFaceSet = [], styleHandles = []) {
super(page, id);
this.width = width;
this.height = height;
this.fontFaceSet = fontFaceSet;
this.styleHandles = styleHandles;
}
static {
__name(this, "CanvasElement");
}
stmts = [];
ctx = new Proxy({
canvas: this,
direction: "inherit",
fillStyle: "#000000",
filter: "none",
font: "10px sans-serif",
fontKerning: "auto",
fontStretch: "normal",
fontVariantCaps: "normal",
globalAlpha: 1,
globalCompositeOperation: "source-over",
imageSmoothingEnabled: true,
imageSmoothingQuality: "low",
letterSpacing: "0px",
lineCap: "butt",
lineDashOffset: 0,
lineJoin: "miter",
lineWidth: 1,
miterLimit: 10,
shadowBlur: 0,
shadowColor: "rgba(0, 0, 0, 0)",
shadowOffsetX: 0,
shadowOffsetY: 0,
strokeStyle: "#000000",
textAlign: "start",
textBaseline: "alphabetic",
textRendering: "auto",
wordSpacing: "0px"
}, {
get: /* @__PURE__ */ __name((target, prop, receiver) => {
if (Reflect.has(target, prop) || typeof prop === "symbol") {
return Reflect.get(target, prop, receiver);
}
return new Proxy(() => {
}, {
apply: /* @__PURE__ */ __name((target2, thisArg, argArray) => {
this.stmts.push(`ctx.${prop}(${argArray.map((value) => {
if (value[kElement]) return value.selector;
return JSON.stringify(value);
}).join(", ")});`);
}, "apply")
});
}, "get"),
set: /* @__PURE__ */ __name((target, prop, value, receiver) => {
if (Reflect.has(target, prop)) {
if (typeof prop !== "symbol") {
this.stmts.push(`ctx.${prop} = ${JSON.stringify(value)};`);
}
return Reflect.set(target, prop, value, receiver);
}
}, "set")
});
getContext(type) {
return this.ctx;
}
async toDataURL(type) {
if (!this.id) throw new Error("canvas has been disposed");
try {
this.stmts.unshift(`(async (ctx) => {`);
const expr = this.stmts.join("\n ") + `
})(${this.selector}.getContext('2d'))`;
this.stmts = [];
await this.page.evaluate(expr);
return await this.page.evaluate(`${this.selector}.toDataURL(${JSON.stringify(type)})`);
} catch (err) {
await this.dispose();
throw err;
}
}
async toBuffer(type) {
const url = await this.toDataURL(type);
return Buffer.from(url.slice(url.indexOf(",") + 1), "base64");
}
async dispose() {
await super.dispose();
await Promise.all(this.fontFaceSet.map(async (fontFace) => {
await this.page.evaluate((fontFace2) => {
document.fonts.delete(fontFace2);
}, fontFace);
}));
await Promise.all(this.styleHandles.map(async (handle) => {
try {
await handle.evaluate((node) => node.remove());
await handle.dispose();
} catch (e) {
}
}));
}
};
var ImageElement = class extends BaseElement {
constructor(ctx, page, id, source, type) {
super(page, id);
this.ctx = ctx;
this.source = source;
this.type = type;
}
static {
__name(this, "ImageElement");
}
naturalHeight;
naturalWidth;
async initialize() {
let base64;
if (this.source instanceof URL) {
this.source = this.source.href;
}
if (typeof this.source === "string") {
const file = await this.ctx.http.file(this.source);
base64 = import_koishi2.Binary.toBase64(file.data);
} else if (Buffer.isBuffer(this.source)) {
base64 = this.source.toString("base64");
} else {
base64 = import_koishi2.Binary.toBase64(this.source);
}
const size = await this.page.evaluate(`loadImage(${JSON.stringify(this.id)}, ${JSON.stringify(base64)}, ${JSON.stringify(this.type)})`);
this.naturalWidth = size.width;
this.naturalHeight = size.height;
}
};
var canvas_default = class extends import_canvas.default {
static {
__name(this, "default");
}
static inject = ["puppeteer", "http"];
page;
counter = 0;
async start() {
const page = await this.ctx.puppeteer.page();
try {
await page.goto((0, import_url.pathToFileURL)((0, import_path.resolve)(__dirname, "../index.html")).href);
this.page = page;
} catch (err) {
await page.close();
throw err;
}
}
async stop() {
await this.page?.close();
this.page = null;
}
async createCanvas(width, height, options) {
const fontFaceSet = [];
const styleHandles = [];
try {
const name = `canvas_${++this.counter}`;
if (options && options.families.length && this.ctx.fonts) {
try {
const fonts = await this.ctx.fonts.get(options.families);
await Promise.all(fonts.map(async (font) => {
if (font.format === "google") {
const style = await this.page.addStyleTag({ url: font.path });
styleHandles.push(style);
} else {
await this.page.evaluate((font2, fontFaceSet2) => {
const fontFace = new FontFace(
font2.family,
`url(${font2.path}) format('${font2.format}')`,
font2.descriptors
);
document.fonts.add(fontFace);
fontFaceSet2.push(fontFace);
}, font, fontFaceSet);
}
}));
if (options?.text) {
await this.page.evaluate(async (text, families) => {
await document.fonts.load(
`1px ${families.join(",")}`,
text
);
}, options.text, options.families);
}
} catch (e) {
this.ctx.logger("puppeteer").warn("加载字体失败,将使用系统默认字体:", e.message);
}
}
await this.page.evaluate([
`const ${name} = document.createElement('canvas');`,
`${name}.width = ${width};`,
`${name}.height = ${height};`,
`${name}.id = ${JSON.stringify(name)};`,
`document.body.appendChild(${name});`
].join("\n"));
return new CanvasElement(this.page, name, width, height, fontFaceSet, styleHandles);
} catch (err) {
this.ctx.logger("puppeteer").warn(err);
throw err;
}
}
async render(width, height, callback, options) {
let canvas;
try {
canvas = await this.createCanvas(width, height, options);
await callback(canvas.getContext("2d"));
const buffer = await canvas.toBuffer("image/png");
return import_koishi2.h.image(buffer, "image/png");
} catch (err) {
this.ctx.logger("puppeteer").warn(err);
throw err;
} finally {
try {
await canvas.dispose();
} catch (err) {
this.ctx.logger("puppeteer").warn(err);
}
}
}
async loadImage(source, type) {
const id = `image_${++this.counter}`;
const image = new ImageElement(this.ctx, this.page, id, source, type);
await image.initialize();
return image;
}
};
// src/index.ts
var import_path2 = require("path");
var import_url2 = require("url");
var Puppeteer = class extends import_koishi3.Service {
constructor(ctx, config) {
super(ctx, "puppeteer");
this.config = config;
if (this.config.enableCanvas !== false) {
ctx.plugin(canvas_default);
}
this.registerHtmlComponent();
if (this.config.enableRestartCommand !== false) {
ctx.command("puppeteer.restart", "重启 Puppeteer 浏览器服务").action(async ({ session }) => {
try {
session?.send("正在重启 Puppeteer 服务...");
await this.stopBrowser();
await this.startBrowser();
return "✅ Puppeteer 服务重启成功";
} catch (error) {
ctx.logger.error("Puppeteer 服务重启失败:", error);
return `❌ Puppeteer 服务重启失败: ${error.message}`;
}
});
}
}
static {
__name(this, "Puppeteer");
}
static [import_koishi3.Service.provide] = "puppeteer";
static inject = ["http"];
browser;
executable;
browserWSEndpoint;
registerHtmlComponent() {
const transformStyle = /* @__PURE__ */ __name((source, base = {}) => {
return Object.entries({ ...base, ...source }).map(([key, value]) => {
return `${(0, import_koishi3.hyphenate)(key)}: ${Array.isArray(value) ? value.join(", ") : value}`;
}).join("; ");
}, "transformStyle");
this.ctx.component("html", async (attrs, children) => {
const head = [];
const transform = /* @__PURE__ */ __name((element) => {
if (element.type === "head") {
head.push(...element.children);
return;
}
const attrs2 = { ...element.attrs };
if (typeof attrs2.style === "object") {
attrs2.style = transformStyle(attrs2.style);
}
return (0, import_koishi3.h)(element.type, attrs2, element.children.map(transform).filter(Boolean));
}, "transform");
await this.ensureConnected();
const page = await this.page();
try {
if (attrs.src) {
await page.goto(attrs.src);
} else {
await page.goto((0, import_url2.pathToFileURL)((0, import_path2.resolve)(__dirname, "../index.html")).href);
const bodyStyle = typeof attrs.style === "object" ? transformStyle({ display: "inline-block" }, attrs.style) : ["display: inline-block", attrs.style].filter(Boolean).join("; ");
const content = children.map(transform).filter(Boolean).join("");
const lang = attrs.lang ? ` lang="${attrs.lang}"` : "";
await page.setContent(`<html${lang}>
<head>${head.join("")}</head>
<body style="${bodyStyle}">${content}</body>
</html>`);
}
await page.waitForNetworkIdle({
timeout: attrs.timeout ? +attrs.timeout : void 0
});
const body = await page.$(attrs.selector || "body");
const clip = await body.boundingBox();
const screenshot = await page.screenshot({ clip });
return import_koishi3.h.image(screenshot, "image/png");
} finally {
await page?.close();
}
});
}
async start() {
await this.startBrowser();
}
async startBrowser() {
const { remote, endpoint, executablePath, headers, headless, args = [], ...config } = this.config;
try {
if (remote) {
if (!endpoint) {
throw new Error("远程浏览器模式下必须提供 endpoint 参数");
}
const connectOptions = { headers, ...config };
try {
const endpointURL = new URL(endpoint);
if (["ws:", "wss:"].includes(endpointURL.protocol)) {
connectOptions.browserWSEndpoint = endpoint;
} else if (["http:", "https:"].includes(endpointURL.protocol)) {
connectOptions.browserURL = endpoint;
} else {
throw new Error(`不支持的协议: ${endpointURL.protocol},endpoint 必须以 ws://, wss://, http:// 或 https:// 开头`);
}
} catch (e) {
if (e instanceof TypeError) {
throw new Error(`无效的 endpoint URL: ${endpoint},请检查格式是否正确`);
}
throw e;
}
try {
this.ctx.logger.info("正在连接远程浏览器: %c", endpoint);
this.browser = await import_puppeteer_core.default.connect(connectOptions);
this.ctx.logger.info("远程浏览器连接成功。");
if (connectOptions.browserWSEndpoint) {
this.browserWSEndpoint = connectOptions.browserWSEndpoint;
} else if (this.browser.wsEndpoint) {
this.browserWSEndpoint = this.browser.wsEndpoint();
this.ctx.logger.debug("从 HTTP URL 获取 WebSocket 端点: %c", this.browserWSEndpoint);
}
} catch (e) {
if (e.message?.includes("ECONNREFUSED")) {
throw new Error(`无法连接到远程浏览器 ${endpoint},请确保远程浏览器已启动并且端口可访问`);
} else if (e.message?.includes("not opened")) {
throw new Error(`远程浏览器连接被拒绝,请确保提供的 endpoint 是正确的并且浏览器已启动调试模式`);
} else {
throw new Error(`连接远程浏览器失败: ${e.message || e}`);
}
}
} else {
this.executable = executablePath || (0, import_puppeteer_finder.default)();
if (!this.executable) {
throw new Error("未找到 Chrome 可执行文件,请手动指定 executablePath 参数");
}
if (!executablePath) {
this.ctx.logger.info("找到 Chrome 可执行文件: %c", this.executable);
}
const localArgs = [...args];
const { proxyAgent } = this.ctx.http.config;
if (proxyAgent && !localArgs.some((arg) => arg.startsWith("--proxy-server"))) {
localArgs.push(`--proxy-server=${proxyAgent}`);
}
try {
this.ctx.logger.info("正在启动本地浏览器...");
this.browser = await import_puppeteer_core.default.launch({
executablePath: this.executable,
headless,
args: localArgs,
...config
});
this.ctx.logger.info("本地浏览器启动成功。");
this.browserWSEndpoint = this.browser.wsEndpoint();
} catch (e) {
if (e.message?.includes("Failed to launch")) {
throw new Error(`启动浏览器失败,请检查 Chrome 是否已安装或路径是否正确: ${e.message}`);
} else {
throw new Error(`启动浏览器失败: ${e.message || e}`);
}
}
}
} catch (error) {
this.ctx.logger.error(`Puppeteer 初始化失败: `, error);
throw error;
}
}
async stop() {
await this.stopBrowser();
}
async stopBrowser() {
try {
if (this.browser) {
if (this.config.remote) {
await this.browser.disconnect();
} else {
await this.browser.close();
}
this.browser = null;
this.browserWSEndpoint = null;
}
} catch (error) {
this.ctx.logger.warn("停止浏览器时出现错误:", error.message);
}
}
// 检查浏览器连接状态并尝试重连
async ensureConnected() {
if (this.browser.connected) return;
if (this.config.enableReconnect === false) {
throw new Error("浏览器连接已断开,且未启用自动重连");
}
const hasReconnectEndpoint = this.browserWSEndpoint || this.config.endpoint;
if (!hasReconnectEndpoint) {
throw new Error("浏览器连接已断开,且没有可用的重连端点");
}
let retryCount = 0;
const maxRetries = this.config.maxReconnectRetries ?? 3;
while (retryCount < maxRetries) {
try {
this.ctx.logger.info(`浏览器连接已断开,尝试重新连接... (尝试 ${retryCount + 1}/${maxRetries})`);
if (this.config.reconnectInterval > 0) {
await new Promise((resolve3) => setTimeout(resolve3, this.config.reconnectInterval));
}
const connectOptions = { ...this.config };
if (this.browserWSEndpoint) {
connectOptions.browserWSEndpoint = this.browserWSEndpoint;
this.ctx.logger.debug("使用保存的 WebSocket 端点重连: %c", this.browserWSEndpoint);
} else if (this.config.endpoint) {
try {
const endpointURL = new URL(this.config.endpoint);
if (["ws:", "wss:"].includes(endpointURL.protocol)) {
connectOptions.browserWSEndpoint = this.config.endpoint;
this.ctx.logger.debug("使用配置的 WebSocket 端点重连: %c", this.config.endpoint);
} else if (["http:", "https:"].includes(endpointURL.protocol)) {
connectOptions.browserURL = this.config.endpoint;
this.ctx.logger.debug("使用配置的 HTTP 端点重连: %c", this.config.endpoint);
}
} catch (e) {
this.ctx.logger.warn("解析端点 URL 失败: %c", e.message);
}
}
this.browser = await import_puppeteer_core.default.connect(connectOptions);
if (this.browser.connected) {
this.ctx.logger.info("浏览器重新连接成功");
if (this.browser.wsEndpoint) {
this.browserWSEndpoint = this.browser.wsEndpoint();
this.ctx.logger.debug("保存新的 WebSocket 端点: %c", this.browserWSEndpoint);
}
return;
} else {
throw new Error("连接后浏览器状态仍为断开");
}
} catch (e) {
retryCount++;
if (e.message?.includes("404") || e.message?.includes("Unexpected server response: 404")) {
this.ctx.logger.warn("检测到 404 错误,尝试使用原始配置重新连接");
try {
this.browserWSEndpoint = null;
const originalOptions = { ...this.config };
if (this.config.endpoint) {
const endpointURL = new URL(this.config.endpoint);
if (["ws:", "wss:"].includes(endpointURL.protocol)) {
originalOptions.browserWSEndpoint = this.config.endpoint;
} else if (["http:", "https:"].includes(endpointURL.protocol)) {
originalOptions.browserURL = this.config.endpoint;
}
}
this.browser = await import_puppeteer_core.default.connect(originalOptions);
if (this.browser.connected) {
this.ctx.logger.info("使用原始配置重新连接成功");
if (this.browser.wsEndpoint) {
this.browserWSEndpoint = this.browser.wsEndpoint();
this.ctx.logger.debug("保存新的 WebSocket 端点: %c", this.browserWSEndpoint);
}
return;
}
} catch (reconnectError) {
this.ctx.logger.warn("使用原始配置重新连接失败:", reconnectError.message);
}
}
if (retryCount >= maxRetries) {
this.ctx.logger.error(`浏览器重新连接失败 (${retryCount}/${maxRetries}):`, e.message);
throw new Error(`浏览器重新连接失败: ${e.message}`);
} else {
this.ctx.logger.warn(`浏览器重新连接失败,将重试 (${retryCount}/${maxRetries}):`, e.message);
await new Promise((resolve3) => setTimeout(resolve3, this.config.reconnectInterval * retryCount));
}
}
}
}
page = /* @__PURE__ */ __name(async (options) => {
let page;
try {
await this.ensureConnected();
page = await this.browser.newPage();
if (options) {
if (options?.beforeGotoPage) {
await options.beforeGotoPage(page);
}
await page.goto(`${(0, import_url2.pathToFileURL)(options.url)}`, options?.gotoOptions);
if (options?.content) {
await page.setContent(options.content);
}
if (options?.families?.length && this.ctx.fonts) {
try {
const fonts = await this.ctx.fonts.get(options.families);
await Promise.all(fonts.map(async (font) => {
if (font.format === "google") {
await page.addStyleTag({ content: `@import url('${font.path}')` });
} else {
await page.evaluate((font2) => {
const fontFace = new FontFace(
font2.family,
`url(${font2.path}) format('${font2.format}')`,
font2.descriptors
);
document.fonts.add(fontFace);
}, font);
}
}));
} catch (e) {
this.ctx.logger.warn("加载字体失败,将使用系统默认字体:", e.message);
}
}
}
} catch (err) {
if (page) {
await page.close();
}
this.ctx.logger.error("failed to create page: %s", err);
throw err;
}
return page;
}, "page");
svg = /* @__PURE__ */ __name(async (options) => {
await this.ensureConnected();
return new SVG(options);
}, "svg");
render = /* @__PURE__ */ __name(async (content, callback, families) => {
await this.ensureConnected();
const url = (0, import_path2.resolve)(__dirname, "../index.html");
const page = await this.page({ url, content, families });
const renderConfig = this.config.render || {};
if (families?.length) {
try {
await page.addStyleTag({ content: `* {font-family: ${families.map((f) => `'${f}'`).join(", ")};}` });
await page.evaluate(async () => {
await document.fonts.ready;
await new Promise((resolve3) => setTimeout(resolve3, 100));
});
} catch (e) {
this.ctx.logger.warn("应用字体样式失败:", e.message);
}
}
callback ||= /* @__PURE__ */ __name(async (_, next) => page.$("body").then(next), "callback");
const output = await callback(page, async (handle) => {
const clip = handle ? await handle.boundingBox() : null;
const screenshotOptions = { clip, ...renderConfig };
const buffer = await page.screenshot(screenshotOptions);
const imageType = renderConfig.type || "png";
return import_koishi3.h.image(buffer, `image/${imageType}`).toString();
});
page.close();
return output;
}, "render");
};
((Puppeteer2) => {
Puppeteer2.filter = false;
Puppeteer2.usage = `
---
本插件提供浏览器 API 服务,主要用于网页截图、生成图片等功能。
**重要提示:**
1. **浏览器环境要求:** 为确保插件正常运行,请确保您的系统已安装 Chromium 浏览器,或已配置远程浏览器服务。
2. **版本匹配:** 建议保持 Chromium/Chrome 浏览器与本插件同步更新,以避免因版本不匹配导致的功能异常。
3. **Windows 系统兼容性:** 如果您在 Windows 服务器上运行,为支持最新版 Puppeteer 及其捆绑的 Chromium,您的操作系统至少需要是 **Windows Server 2016 或更高版本**。
注意: Windows Server 2012 R2 及更早版本已无法满足最新 Chrome 的运行要求。
---
**服务依赖:**
- **必需服务:** http(自带服务,无需额外安装)
- **可选服务:** [fonts](/market?keyword=font+email:shigma10826@gmail.com+email:saarchaffee@qq.com)
---
<p>➣ <a href="https://github.com/shangxueink/koishi-plugin-puppeteer-without-canvas" target="_blank">点我前往项目地址</a></p>
---
`;
Puppeteer2.Config = import_koishi3.Schema.intersect([
import_koishi3.Schema.object({
remote: import_koishi3.Schema.boolean().description("是否连接到远程浏览器。").default(false)
}).description("连接设置"),
import_koishi3.Schema.union([
import_koishi3.Schema.object({
remote: import_koishi3.Schema.const(true).required(),
endpoint: import_koishi3.Schema.string().description(
"远程浏览器的端点。<br>例:<br>WebSocket URL: `ws://localhost:14550/devtools/browser/[id]`<br>HTTP URL: `http://localhost:14550`<br>"
).required(),
headers: import_koishi3.Schema.dict(String).role("table").description(
"连接到远程浏览器时使用的 HTTP 请求头。<br>注意:这与本地模式的 `args` 不同,headers 只影响连接请求,不影响浏览器本身的行为。<br>常用于设置身份验证、API密钥或自定义标识等。详细示例请参考 README。"
)
}),
import_koishi3.Schema.object({
remote: import_koishi3.Schema.const(false).default(false),
executablePath: import_koishi3.Schema.string().description(
"Chrome/Chromium 可执行文件的路径。<br>如果不指定,将自动从系统中查找。<br>如果自动查找失败,请手动指定此路径。"
),
headless: import_koishi3.Schema.boolean().description("是否开启[无头模式](https://developer.chrome.com/blog/headless-chrome/)。无头模式下浏览器不会显示界面。").default(true),
args: import_koishi3.Schema.array(String).description(
"启动 Chrome/Chromium 浏览器时传递的命令行参数。<br>常用参数:<br>`--no-sandbox`: 禁用沙箱(在 Docker 或 root 用户下常用)<br>`--disable-gpu`: 禁用 GPU 加速<br>更多 [Chromium 参数请参考这个页面](https://peter.sh/experiments/chromium-command-line-switches/)。"
).default(process.getuid?.() === 0 ? ["--no-sandbox", "--disable-gpu"] : [])
})
]),
import_koishi3.Schema.object({
enablePuppeteer: import_koishi3.Schema.boolean().description("是否注册 puppeteer 服务。").default(true),
enableCanvas: import_koishi3.Schema.boolean().description("是否注册 canvas 服务。(默认关闭。)<br>注意: 这与[`koishi-plugin-canvas`](/market?keyword=koishi-plugin-canvas+email:shigma10826@gmail.com+email:void@anillc.cn+email:i.dlist@outlook.com)的`canvas`服务同名 但API不一致。").default(false),
enableRestartCommand: import_koishi3.Schema.boolean().description("是否注册 puppeteer.restart 重启指令。启用后可通过指令重启浏览器服务。").default(false),
enableReconnect: import_koishi3.Schema.boolean().description("是否启用浏览器自动重连功能。当浏览器连接断开时,会尝试重新连接。").default(true),
reconnectInterval: import_koishi3.Schema.number().description("浏览器重连尝试的间隔时间(毫秒)。").default(1e3),
maxReconnectRetries: import_koishi3.Schema.number().description("浏览器重连最大尝试次数。").default(3)
}).description("功能设置"),
import_koishi3.Schema.object({
render: import_koishi3.Schema.intersect([
import_koishi3.Schema.object({
type: import_koishi3.Schema.union(["png", "jpeg", "webp"]).description("默认渲染的图片类型。").default("png")
}),
import_koishi3.Schema.union([
import_koishi3.Schema.object({
type: import_koishi3.Schema.const("png")
}),
import_koishi3.Schema.object({
quality: import_koishi3.Schema.number().min(0).max(100).step(1).description("默认渲染的图片质量。").default(80)
})
])
])
}).description("渲染设置"),
import_koishi3.Schema.object({
defaultViewport: import_koishi3.Schema.object({
width: import_koishi3.Schema.natural().description("默认的视图宽度。").default(1280),
height: import_koishi3.Schema.natural().description("默认的视图高度。").default(768),
deviceScaleFactor: import_koishi3.Schema.number().min(0).description("默认的设备缩放比率。").default(2)
}),
ignoreHTTPSErrors: import_koishi3.Schema.boolean().description("在导航时忽略 HTTPS 错误。").default(false)
}).description("浏览器设置")
]);
})(Puppeteer || (Puppeteer = {}));
var src_default = Puppeteer;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
SVG,
Tag
});