@xiaozhu2007/koishi-plugin-litematic-renderer 0.5.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,13 +6,35 @@
6
6
 
7
7
  安装、配置、渲染引擎与命令说明请查看[原作者仓库](https://github.com/halfkite/koishi-plugin-litematic-renderer#readme)
8
8
 
9
- ## 维护版变更
9
+ ## 此版本变更
10
10
 
11
11
  - 可通过实验性 `sentryDsn` 配置或 `SENTRY_DSN` 环境变量上报渲染错误
12
12
  - 渲染图片改用 OneBot `base64://` 发送,支持跨数据中心和 Kubernetes namespace 部署
13
- - 独立 Java 渲染器版本更新至 `0.2.7-patch.1`,支持通过 `--six-face-only` 输出六面正交图,并可配置 128~4096 分辨率及横向 3×2、纵向 2×3 布局
14
- - Java 与 TypeScript 渲染器统一使用同一套 11×11 上下东西南北点阵字形,修复六视图方向标签不一致的问题
13
+ - 独立 Java 渲染器版本更新至 `0.2.8-patch.1`,统一使用同一套点阵字形,**修复六视图方向标签不一致的问题**
15
14
  - 合并转发只发送渲染结果,不再额外发送「渲染结果如上」
16
- - 升级至 TypeScript 6,构建时使用 Terser 压缩 `lib/index.js`
15
+ - 升级至 TypeScript 6,构建时使用 Terser 压缩
17
16
  - 为代码添加可读性类型注解,便于 IDE 自动补全
18
17
  - 补齐独立 Java 渲染器的 Gradle 8.10 Wrapper 与 Gson 2.11.0 依赖
18
+ - \[上游\] 新增主动连接的 GPU Agent v2、Windows Agent 与 Minecraft 26.2 Runtime 源码;原有 `standalone`、`java`、`webgl`、`cpu` 和外部 GPU 命令均继续保留
19
+ - \[上游\] 新增默认关闭的私聊渲染、独立 `privateMaxFileSize` 限制,以及黑名单优先的群白名单/黑名单
20
+ - \[上游\] Koishi 控制台可上传不超过 256 MiB 的 ZIP 材质包,并继续按列表顺序决定覆盖优先级
21
+ - \[上游\] 官方 QQ 可实验性配置固定出口代理;8 分钟静默重载看门狗默认关闭且**必须单独启用**
22
+
23
+ ### 新增配置兼容说明
24
+
25
+ - `renderEngine` 新增 `gpuAgent`,但默认值仍为 `standalone`;`gpuRendererCommand` 非空时仍最先尝试
26
+ - `gpuAgentEnabled` 控制 WebSocket 服务是否监听,`gpuAgentNodes` 为每个节点配置独立 HMAC 密钥,`gpuAgentFallback` 默认回退本地独立渲染器
27
+ - `allowPrivateRender` 默认 `false`;私聊使用 `privateMaxFileSize`,群聊继续使用 `maxFileSize`
28
+ - `groupBlacklist` 优先于 `groupWhitelist`;私聊没有群 ID,因此不参与群名单判断
29
+ - `officialProxyMode` 及全部 `sshProxy*` 字段均为实验性;`officialGatewayWatchdog` 默认 `false`
30
+ - Java 路径继续使用维护版原有的 `standaloneJavaCommand`,没有引入含义重复的 `javaPath`
31
+
32
+ ### 独立渲染器源码
33
+
34
+ `standalone-renderer` 源码已合并上游 `0.2.8` 的网格烘焙、预览与模型修复,并保留维护版六面方向字形,源码版本标记为 `0.2.8-patch.1`。
35
+
36
+ 当前 npm 包引用已验证的 `litematic-standalone-renderer-0.2.8-patch.1.jar`,与仓库源码版本一致。
37
+
38
+ ## TODO
39
+
40
+ - [ ] 拆分 `src/index.ts`
@@ -3016,21 +3016,18 @@
3016
3016
  static fromNbt(t) {
3017
3017
  const e = Mi.parse(Si(t.value, "Name", "string")),
3018
3018
  i = Ii(() => Si(t.value, "Properties", "compound"), {}),
3019
- n = Object.keys(i).reduce(
3020
- (t, e) => {
3021
- return (
3022
- (n = ((t, e) => {
3023
- for (var i in e || (e = {})) Fi.call(e, i) && Di(t, i, e[i]);
3024
- if (Pi) for (var i of Pi(e)) Li.call(e, i) && Di(t, i, e[i]);
3025
- return t;
3026
- })({}, t)),
3027
- (s = { [e]: Si(i, e, "string") }),
3028
- Ci(n, Oi(s))
3029
- );
3030
- var n, s;
3031
- },
3032
- {},
3033
- );
3019
+ n = Object.keys(i).reduce((t, e) => {
3020
+ return (
3021
+ (n = ((t, e) => {
3022
+ for (var i in e || (e = {})) Fi.call(e, i) && Di(t, i, e[i]);
3023
+ if (Pi) for (var i of Pi(e)) Li.call(e, i) && Di(t, i, e[i]);
3024
+ return t;
3025
+ })({}, t)),
3026
+ (s = { [e]: Si(i, e, "string") }),
3027
+ Ci(n, Oi(s))
3028
+ );
3029
+ var n, s;
3030
+ }, {});
3034
3031
  return new zi(e, n);
3035
3032
  }
3036
3033
  static fromJson(t) {
@@ -0,0 +1,136 @@
1
+ import { message, send } from "@koishijs/client";
2
+ import "./style.css";
3
+
4
+ function nextFrame() {
5
+ return new Promise<void>((resolve) =>
6
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve())),
7
+ );
8
+ }
9
+
10
+ function findResourcePackItems() {
11
+ return [...document.querySelectorAll<HTMLElement>(".k-schema-item")].filter((item) => {
12
+ return item.querySelector("h3")?.textContent?.trim() === "resourcePackPaths";
13
+ });
14
+ }
15
+
16
+ function setupResourcePackControl(item: HTMLElement) {
17
+ if (item.dataset.litematicResourcePacks === "ready") return;
18
+ const controls = item.querySelector<HTMLElement>(".k-schema-right");
19
+ if (!controls) return;
20
+ item.dataset.litematicResourcePacks = "ready";
21
+
22
+ const toolbar = document.createElement("div");
23
+ toolbar.className = "litematic-resource-pack-toolbar";
24
+ const uploadButton = createButton("上传材质包", true);
25
+ const upButton = createButton("上移");
26
+ const downButton = createButton("下移");
27
+ const fileInput = document.createElement("input");
28
+ fileInput.type = "file";
29
+ fileInput.accept = ".zip,application/zip";
30
+ fileInput.hidden = true;
31
+ toolbar.append(uploadButton, upButton, downButton, fileInput);
32
+ controls.prepend(toolbar);
33
+
34
+ let selected = -1;
35
+ const rows = () =>
36
+ [...item.querySelectorAll<HTMLTableRowElement>(".k-schema-table tr")].filter(
37
+ (row) => !!row.querySelector("td"),
38
+ );
39
+ const refresh = () => {
40
+ const currentRows = rows();
41
+ if (selected >= currentRows.length) selected = currentRows.length - 1;
42
+ currentRows.forEach((row, index) =>
43
+ row.classList.toggle("litematic-selected", index === selected),
44
+ );
45
+ upButton.disabled = selected <= 0;
46
+ downButton.disabled = selected < 0 || selected >= currentRows.length - 1;
47
+ };
48
+
49
+ item.addEventListener("click", (event) => {
50
+ const row = (event.target as Element).closest<HTMLTableRowElement>(".k-schema-table tr");
51
+ if (!row || !row.querySelector("td")) return;
52
+ selected = rows().indexOf(row);
53
+ refresh();
54
+ });
55
+
56
+ const move = async (offset: -1 | 1) => {
57
+ const currentRows = rows();
58
+ if (selected < 0 || selected + offset < 0 || selected + offset >= currentRows.length) return;
59
+ const actions = currentRows[selected].querySelectorAll<HTMLElement>("td.k-schema-table-button");
60
+ actions[offset < 0 ? 0 : 1]?.click();
61
+ selected += offset;
62
+ await nextFrame();
63
+ refresh();
64
+ };
65
+ upButton.addEventListener("click", () => void move(-1));
66
+ downButton.addEventListener("click", () => void move(1));
67
+ uploadButton.addEventListener("click", () => fileInput.click());
68
+
69
+ fileInput.addEventListener("change", async () => {
70
+ const file = fileInput.files?.[0];
71
+ fileInput.value = "";
72
+ if (!file) return;
73
+ if (!/\.zip$/i.test(file.name)) return message.error("只允许上传 ZIP 材质包");
74
+ if (file.size > 256 * 1024 * 1024) return message.error("材质包超过 256 MB 上传上限");
75
+
76
+ uploadButton.disabled = true;
77
+ uploadButton.textContent = "上传中...";
78
+ try {
79
+ const path = (await send(
80
+ "litematic/resource-pack-upload",
81
+ file.name,
82
+ await readBase64(file),
83
+ )) as string;
84
+ const addButton = [...controls.querySelectorAll<HTMLButtonElement>("button")].find((button) =>
85
+ /添加(行|项目)/.test(button.textContent ?? ""),
86
+ );
87
+ if (!addButton) throw new Error("无法找到资源包列表的添加按钮");
88
+ addButton.click();
89
+ await nextFrame();
90
+ const inputs = item.querySelectorAll<HTMLInputElement>(".k-schema-table input");
91
+ const target = inputs[inputs.length - 1];
92
+ if (!target) throw new Error("无法写入资源包路径");
93
+ const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
94
+ setter?.call(target, path);
95
+ target.dispatchEvent(new Event("input", { bubbles: true }));
96
+ target.dispatchEvent(new Event("change", { bubbles: true }));
97
+ selected = rows().length - 1;
98
+ refresh();
99
+ message.success("材质包已上传并设为最高优先级,请保存配置");
100
+ } catch (error) {
101
+ message.error(error instanceof Error ? error.message : String(error));
102
+ } finally {
103
+ uploadButton.disabled = false;
104
+ uploadButton.textContent = "上传材质包";
105
+ }
106
+ });
107
+
108
+ new MutationObserver(refresh).observe(item, { childList: true, subtree: true });
109
+ refresh();
110
+ }
111
+
112
+ function createButton(label: string, primary = false) {
113
+ const button = document.createElement("button");
114
+ button.type = "button";
115
+ button.className = `el-button${primary ? " el-button--primary" : ""}`;
116
+ button.textContent = label;
117
+ return button;
118
+ }
119
+
120
+ function readBase64(file: File) {
121
+ return new Promise<string>((resolve, reject) => {
122
+ const reader = new FileReader();
123
+ reader.onerror = () => reject(reader.error ?? new Error("读取文件失败"));
124
+ reader.onload = () => resolve(String(reader.result).split(",", 2)[1] ?? "");
125
+ reader.readAsDataURL(file);
126
+ });
127
+ }
128
+
129
+ function scan() {
130
+ findResourcePackItems().forEach(setupResourcePackControl);
131
+ }
132
+
133
+ new MutationObserver(scan).observe(document.body, { childList: true, subtree: true });
134
+ scan();
135
+
136
+ export default () => {};
@@ -0,0 +1,18 @@
1
+ .litematic-resource-pack-toolbar {
2
+ display: inline-flex;
3
+ flex-wrap: wrap;
4
+ gap: 8px;
5
+ margin-right: 8px;
6
+ }
7
+
8
+ .litematic-resource-pack-toolbar .el-button {
9
+ margin-left: 0;
10
+ }
11
+
12
+ .k-schema-table tr.litematic-selected td {
13
+ background: var(--k-button-hover-bg);
14
+ }
15
+
16
+ .k-schema-table tr.litematic-selected td:first-child {
17
+ box-shadow: inset 3px 0 var(--k-color-active);
18
+ }
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{message as m,send as S}from"@koishijs/client";function E(){return new Promise(t=>requestAnimationFrame(()=>requestAnimationFrame(()=>t())))}function L(){return[...document.querySelectorAll(".k-schema-item")].filter(t=>{var a,r;return((r=(a=t.querySelector("h3"))==null?void 0:a.textContent)==null?void 0:r.trim())==="resourcePackPaths"})}function P(t){if(t.dataset.litematicResourcePacks==="ready")return;const a=t.querySelector(".k-schema-right");if(!a)return;t.dataset.litematicResourcePacks="ready";const r=document.createElement("div");r.className="litematic-resource-pack-toolbar";const o=w("上传材质包",true),b=w("上移"),h=w("下移"),s=document.createElement("input");s.type="file",s.accept=".zip,application/zip",s.hidden=true,r.append(o,b,h,s),a.prepend(r);let n=-1;const d=()=>[...t.querySelectorAll(".k-schema-table tr")].filter(e=>!!e.querySelector("td")),l=()=>{const e=d();n>=e.length&&(n=e.length-1),e.forEach((c,u)=>c.classList.toggle("litematic-selected",u===n)),b.disabled=n<=0,h.disabled=n<0||n>=e.length-1};t.addEventListener("click",e=>{const c=e.target.closest(".k-schema-table tr");!c||!c.querySelector("td")||(n=d().indexOf(c),l())});const y=async e=>{var i;const c=d();if(n<0||n+e<0||n+e>=c.length)return;(i=c[n].querySelectorAll("td.k-schema-table-button")[e<0?0:1])==null||i.click(),n+=e,await E(),l()};b.addEventListener("click",()=>void y(-1)),h.addEventListener("click",()=>void y(1)),o.addEventListener("click",()=>s.click()),s.addEventListener("change",async()=>{var c,u;const e=(c=s.files)==null?void 0:c[0];if(s.value="",!!e){if(!/\.zip$/i.test(e.name))return m.error("只允许上传 ZIP 材质包");if(e.size>256*1024*1024)return m.error("材质包超过 256 MB 上传上限");o.disabled=true,o.textContent="上传中...";try{const i=await S("litematic/resource-pack-upload",e.name,await x(e)),k=[...a.querySelectorAll("button")].find(q=>/添加(行|项目)/.test(q.textContent??""));if(!k)throw new Error("无法找到资源包列表的添加按钮");k.click(),await E();const v=t.querySelectorAll(".k-schema-table input"),p=v[v.length-1];if(!p)throw new Error("无法写入资源包路径");const f=(u=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,"value"))==null?void 0:u.set;f==null||f.call(p,i),p.dispatchEvent(new Event("input",{bubbles:true})),p.dispatchEvent(new Event("change",{bubbles:true})),n=d().length-1,l(),m.success("材质包已上传并设为最高优先级,请保存配置")}catch(i){m.error(i instanceof Error?i.message:String(i))}finally{o.disabled=false,o.textContent="上传材质包"}}}),new MutationObserver(l).observe(t,{childList:true,subtree:true}),l()}function w(t,a=false){const r=document.createElement("button");return r.type="button",r.className=`el-button${a?" el-button--primary":""}`,r.textContent=t,r}function x(t){return new Promise((a,r)=>{const o=new FileReader;o.onerror=()=>r(o.error??new Error("读取文件失败")),o.onload=()=>a(String(o.result).split(",",2)[1]??""),o.readAsDataURL(t)})}function g(){L().forEach(P)}new MutationObserver(g).observe(document.body,{childList:true,subtree:true});g();const R=()=>{};export{R as default};
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ .litematic-resource-pack-toolbar{display:inline-flex;flex-wrap:wrap;gap:8px;margin-right:8px}.litematic-resource-pack-toolbar .el-button{margin-left:0}.k-schema-table tr.litematic-selected td{background:var(--k-button-hover-bg)}.k-schema-table tr.litematic-selected td:first-child{box-shadow:inset 3px 0 var(--k-color-active)}
@@ -0,0 +1,102 @@
1
+ import { EventEmitter } from "node:events";
2
+ export declare const GPU_PROTOCOL_VERSION = 2;
3
+ export declare const GPU_BINARY_HEADER_LIMIT: number;
4
+ export interface RenderView {
5
+ id: string;
6
+ name?: string;
7
+ yaw: number;
8
+ pitch: number;
9
+ zoom?: number;
10
+ autoFill?: boolean;
11
+ width: number;
12
+ height: number;
13
+ background: string;
14
+ transparentBackground: boolean;
15
+ supersampling: number;
16
+ }
17
+ export interface GpuRenderRequest {
18
+ version: 2;
19
+ id: string;
20
+ filename: string;
21
+ views: RenderView[];
22
+ resourcePackProfile?: string;
23
+ sourceGroup?: string;
24
+ sourceUser?: string;
25
+ }
26
+ export interface GpuAgentNodeConfig {
27
+ agentId: string;
28
+ sharedSecret: string;
29
+ enabled?: boolean;
30
+ }
31
+ export interface GpuAgentCapabilities {
32
+ rendererVersion?: string;
33
+ minecraftVersion?: string;
34
+ gpu?: string;
35
+ maxTextureSize?: number;
36
+ resourcePackFingerprint?: string;
37
+ }
38
+ export interface GpuRenderImage {
39
+ id: string;
40
+ name: string;
41
+ width: number;
42
+ height: number;
43
+ png: Buffer;
44
+ }
45
+ export interface GpuRenderResult {
46
+ agentId: string;
47
+ images: GpuRenderImage[];
48
+ elapsedMillis?: number;
49
+ cacheHit?: boolean;
50
+ gpu?: string;
51
+ }
52
+ interface BinaryHeader {
53
+ type: "input" | "image";
54
+ taskId: string;
55
+ attachmentId: string;
56
+ name?: string;
57
+ width?: number;
58
+ height?: number;
59
+ }
60
+ export interface GpuAgentHubOptions {
61
+ host: string;
62
+ port: number;
63
+ path: string;
64
+ nodes: GpuAgentNodeConfig[];
65
+ heartbeatTimeout: number;
66
+ logger: {
67
+ info(message: string): void;
68
+ warn(message: string): void;
69
+ debug(message: string): void;
70
+ };
71
+ }
72
+ export declare function gpuAgentAuthSignature(secret: string, challenge: string, agentId: string): string;
73
+ export declare function encodeGpuBinary(header: BinaryHeader, payload: Buffer): Buffer<ArrayBuffer>;
74
+ export declare function decodeGpuBinary(value: Buffer): {
75
+ header: BinaryHeader;
76
+ payload: Buffer;
77
+ };
78
+ export declare function selectGpuAgent<T extends {
79
+ authenticated: boolean;
80
+ busy: boolean;
81
+ queueLength: number;
82
+ lastAssigned: number;
83
+ lastSeen: number;
84
+ capabilities: GpuAgentCapabilities;
85
+ }>(agents: T[], views: RenderView[], now?: number, heartbeatTimeout?: number): T | undefined;
86
+ export declare class GpuAgentHub extends EventEmitter {
87
+ private readonly options;
88
+ private readonly states;
89
+ private readonly nodeConfigs;
90
+ private server?;
91
+ constructor(options: GpuAgentHubOptions);
92
+ start(): void;
93
+ close(): Promise<void>;
94
+ connectedAgentIds(): string[];
95
+ capabilityFingerprint(): string;
96
+ render(request: GpuRenderRequest, schematic: Buffer, timeout: number): Promise<GpuRenderResult>;
97
+ private accept;
98
+ private onMessage;
99
+ private onBinary;
100
+ private disconnect;
101
+ }
102
+ export {};
@@ -0,0 +1,257 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GpuAgentHub = exports.GPU_BINARY_HEADER_LIMIT = exports.GPU_PROTOCOL_VERSION = void 0;
4
+ exports.gpuAgentAuthSignature = gpuAgentAuthSignature;
5
+ exports.encodeGpuBinary = encodeGpuBinary;
6
+ exports.decodeGpuBinary = decodeGpuBinary;
7
+ exports.selectGpuAgent = selectGpuAgent;
8
+ const node_crypto_1 = require("node:crypto");
9
+ const node_events_1 = require("node:events");
10
+ const ws_1 = require("ws");
11
+ exports.GPU_PROTOCOL_VERSION = 2;
12
+ exports.GPU_BINARY_HEADER_LIMIT = 64 * 1024;
13
+ const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
14
+ function gpuAgentAuthSignature(secret, challenge, agentId) {
15
+ return (0, node_crypto_1.createHmac)("sha256", secret).update(challenge).update(".").update(agentId).digest("hex");
16
+ }
17
+ function encodeGpuBinary(header, payload) {
18
+ const encoded = Buffer.from(JSON.stringify(header), "utf8");
19
+ if (encoded.length > exports.GPU_BINARY_HEADER_LIMIT)
20
+ throw new Error("GPU binary header is too large");
21
+ const output = Buffer.allocUnsafe(4 + encoded.length + payload.length);
22
+ output.writeUInt32BE(encoded.length, 0);
23
+ encoded.copy(output, 4);
24
+ payload.copy(output, 4 + encoded.length);
25
+ return output;
26
+ }
27
+ function decodeGpuBinary(value) {
28
+ if (value.length < 5)
29
+ throw new Error("GPU binary frame is truncated");
30
+ const headerLength = value.readUInt32BE(0);
31
+ if (!headerLength || headerLength > exports.GPU_BINARY_HEADER_LIMIT || 4 + headerLength > value.length) {
32
+ throw new Error("GPU binary frame has an invalid header");
33
+ }
34
+ const header = JSON.parse(value.toString("utf8", 4, 4 + headerLength));
35
+ if (!header ||
36
+ !["input", "image"].includes(header.type) ||
37
+ !header.taskId ||
38
+ !header.attachmentId) {
39
+ throw new Error("GPU binary frame has an invalid envelope");
40
+ }
41
+ return { header, payload: value.subarray(4 + headerLength) };
42
+ }
43
+ function selectGpuAgent(agents, views, now = Date.now(), heartbeatTimeout = 30_000) {
44
+ const requiredTexture = Math.max(...views.map((view) => Math.max(view.width, view.height) * Math.max(1, view.supersampling)), 1);
45
+ return agents
46
+ .filter((agent) => agent.authenticated && !agent.busy && now - agent.lastSeen <= heartbeatTimeout)
47
+ .filter((agent) => !agent.capabilities.maxTextureSize || agent.capabilities.maxTextureSize >= requiredTexture)
48
+ .sort((left, right) => left.queueLength - right.queueLength || left.lastAssigned - right.lastAssigned)[0];
49
+ }
50
+ function secureHexEqual(left, right) {
51
+ if (typeof left !== "string" || !/^[0-9a-f]+$/i.test(left))
52
+ return false;
53
+ const first = Buffer.from(left, "hex");
54
+ const second = Buffer.from(right, "hex");
55
+ return first.length === second.length && first.length > 0 && (0, node_crypto_1.timingSafeEqual)(first, second);
56
+ }
57
+ class GpuAgentHub extends node_events_1.EventEmitter {
58
+ options;
59
+ states = new Map();
60
+ nodeConfigs;
61
+ server;
62
+ constructor(options) {
63
+ super();
64
+ this.options = options;
65
+ this.nodeConfigs = new Map(options.nodes.filter((node) => node.enabled !== false).map((node) => [node.agentId, node]));
66
+ }
67
+ start() {
68
+ if (this.server)
69
+ return;
70
+ this.server = new ws_1.WebSocketServer({
71
+ host: this.options.host,
72
+ port: this.options.port,
73
+ path: this.options.path,
74
+ });
75
+ this.server.on("connection", (socket) => this.accept(socket));
76
+ this.server.on("listening", () => this.options.logger.info(`GPU Agent WebSocket listening on ws://${this.options.host}:${this.options.port}${this.options.path}`));
77
+ this.server.on("error", (error) => this.options.logger.warn(`GPU Agent WebSocket error: ${error.message}`));
78
+ }
79
+ async close() {
80
+ for (const state of this.states.values())
81
+ this.disconnect(state, new Error("GPU Agent hub stopped"));
82
+ const server = this.server;
83
+ this.server = undefined;
84
+ if (server)
85
+ await new Promise((resolve) => server.close(() => resolve()));
86
+ }
87
+ connectedAgentIds() {
88
+ return [...this.states.values()]
89
+ .filter((state) => state.authenticated)
90
+ .map((state) => state.agentId);
91
+ }
92
+ capabilityFingerprint() {
93
+ return ([...this.states.values()]
94
+ .filter((state) => state.authenticated)
95
+ .map((state) => [
96
+ state.agentId,
97
+ state.capabilities.rendererVersion ?? "",
98
+ state.capabilities.minecraftVersion ?? "",
99
+ state.capabilities.resourcePackFingerprint ?? "",
100
+ ].join(":"))
101
+ .sort()
102
+ .join("|") || "offline");
103
+ }
104
+ async render(request, schematic, timeout) {
105
+ const state = selectGpuAgent([...this.states.values()], request.views, Date.now(), this.options.heartbeatTimeout);
106
+ if (!state)
107
+ throw new Error("没有可用且能力匹配的 GPU Agent");
108
+ const taskId = request.id || (0, node_crypto_1.randomUUID)();
109
+ state.busy = true;
110
+ state.queueLength++;
111
+ state.lastAssigned = Date.now();
112
+ try {
113
+ const promise = new Promise((resolve, reject) => {
114
+ const timer = setTimeout(() => {
115
+ state.pending.delete(taskId);
116
+ reject(new Error(`GPU Agent ${state.agentId} 渲染超时`));
117
+ }, timeout);
118
+ state.pending.set(taskId, { resolve, reject, images: [], timer });
119
+ });
120
+ state.socket.send(JSON.stringify({ type: "render", version: 2, task: { ...request, id: taskId } }));
121
+ state.socket.send(encodeGpuBinary({ type: "input", taskId, attachmentId: "litematic", name: request.filename }, schematic));
122
+ return await promise;
123
+ }
124
+ finally {
125
+ state.busy = false;
126
+ state.queueLength = Math.max(0, state.queueLength - 1);
127
+ }
128
+ }
129
+ accept(socket) {
130
+ const state = {
131
+ agentId: "",
132
+ socket,
133
+ authenticated: false,
134
+ challenge: (0, node_crypto_1.randomUUID)(),
135
+ queueLength: 0,
136
+ busy: false,
137
+ lastAssigned: 0,
138
+ lastSeen: Date.now(),
139
+ capabilities: {},
140
+ pending: new Map(),
141
+ };
142
+ this.states.set(socket, state);
143
+ socket.on("message", (data, isBinary) => {
144
+ try {
145
+ this.onMessage(state, Buffer.from(data), isBinary);
146
+ }
147
+ catch (error) {
148
+ this.options.logger.warn(`GPU Agent protocol error: ${error instanceof Error ? error.message : String(error)}`);
149
+ socket.close(1008, "protocol error");
150
+ }
151
+ });
152
+ socket.on("close", () => this.disconnect(state, new Error(`GPU Agent ${state.agentId || "unknown"} disconnected`)));
153
+ socket.on("error", (error) => this.options.logger.debug(`GPU Agent socket error: ${error.message}`));
154
+ }
155
+ onMessage(state, data, isBinary) {
156
+ state.lastSeen = Date.now();
157
+ if (isBinary)
158
+ return this.onBinary(state, data);
159
+ const message = JSON.parse(data.toString("utf8"));
160
+ if (message.type === "hello" && message.version === 2 && typeof message.agentId === "string") {
161
+ if (!this.nodeConfigs.has(message.agentId))
162
+ throw new Error(`unconfigured agentId: ${message.agentId}`);
163
+ state.agentId = message.agentId;
164
+ state.challenge = (0, node_crypto_1.randomUUID)();
165
+ state.socket.send(JSON.stringify({ type: "challenge", version: 2, challenge: state.challenge }));
166
+ return;
167
+ }
168
+ if (message.type === "auth" && state.agentId) {
169
+ const node = this.nodeConfigs.get(state.agentId);
170
+ const expected = gpuAgentAuthSignature(node.sharedSecret, state.challenge, state.agentId);
171
+ if (!secureHexEqual(message.signature, expected))
172
+ throw new Error("invalid agent signature");
173
+ state.authenticated = true;
174
+ state.capabilities = message.capabilities ?? {};
175
+ state.socket.send(JSON.stringify({ type: "authenticated", version: 2 }));
176
+ this.options.logger.info(`GPU Agent connected: ${state.agentId}${state.capabilities.gpu ? ` (${state.capabilities.gpu})` : ""}`);
177
+ return;
178
+ }
179
+ if (!state.authenticated)
180
+ throw new Error("agent is not authenticated");
181
+ if (message.type === "heartbeat") {
182
+ state.busy = Boolean(message.busy);
183
+ state.queueLength = Math.max(0, Number(message.queueLength) || 0);
184
+ if (message.capabilities)
185
+ state.capabilities = { ...state.capabilities, ...message.capabilities };
186
+ return;
187
+ }
188
+ if (message.type === "progress") {
189
+ this.emit("progress", {
190
+ agentId: state.agentId,
191
+ taskId: message.taskId,
192
+ progress: message.progress,
193
+ stage: message.stage,
194
+ });
195
+ return;
196
+ }
197
+ if (message.type === "result" || message.type === "error") {
198
+ const pending = state.pending.get(message.taskId);
199
+ if (!pending) {
200
+ this.options.logger.warn(`GPU Agent ${state.agentId} 回传了未知任务 ${message.taskId} 的结果(可能已超时)`);
201
+ return;
202
+ }
203
+ clearTimeout(pending.timer);
204
+ state.pending.delete(message.taskId);
205
+ if (message.type === "error")
206
+ pending.reject(new Error(`${message.code ?? "GPU_RENDER_FAILED"}: ${message.message ?? "GPU render failed"}`));
207
+ else {
208
+ this.options.logger.info(`GPU Agent ${state.agentId} 回传结果:任务 ${message.taskId},${pending.images.length} 张图`);
209
+ pending.resolve({
210
+ agentId: state.agentId,
211
+ images: pending.images,
212
+ elapsedMillis: message.elapsedMillis,
213
+ cacheHit: message.cacheHit,
214
+ gpu: state.capabilities.gpu,
215
+ });
216
+ }
217
+ }
218
+ }
219
+ onBinary(state, data) {
220
+ if (!state.authenticated)
221
+ throw new Error("agent is not authenticated");
222
+ const { header, payload } = decodeGpuBinary(data);
223
+ if (header.type !== "image")
224
+ throw new Error("server only accepts image binary frames");
225
+ if (payload.length < PNG_SIGNATURE.length ||
226
+ !payload.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) {
227
+ throw new Error("agent returned an invalid PNG attachment");
228
+ }
229
+ const pending = state.pending.get(header.taskId);
230
+ if (!pending)
231
+ return;
232
+ pending.images.push({
233
+ id: header.attachmentId,
234
+ name: header.name ?? `${header.attachmentId}.png`,
235
+ width: Number(header.width) || 0,
236
+ height: Number(header.height) || 0,
237
+ png: Buffer.from(payload),
238
+ });
239
+ }
240
+ disconnect(state, error) {
241
+ if (!this.states.delete(state.socket))
242
+ return;
243
+ for (const pending of state.pending.values()) {
244
+ clearTimeout(pending.timer);
245
+ pending.reject(error);
246
+ }
247
+ state.pending.clear();
248
+ // 必须真正关闭 socket:只删注册表会让连接变成僵尸,Agent 侧会一直认为仍在线而不再重连
249
+ try {
250
+ state.socket.close(1001, error.message.slice(0, 120));
251
+ }
252
+ catch (ignored) { }
253
+ if (state.authenticated)
254
+ this.options.logger.warn(error.message);
255
+ }
256
+ }
257
+ exports.GpuAgentHub = GpuAgentHub;
package/lib/index.d.ts CHANGED
@@ -1,4 +1,10 @@
1
1
  import { Context, h, Schema, Session } from "koishi";
2
+ import { GpuAgentNodeConfig, GpuRenderRequest } from "./gpu-agent-protocol";
3
+ declare module "@koishijs/console" {
4
+ interface Events {
5
+ "litematic/resource-pack-upload"(filename: unknown, base64: unknown): Promise<string>;
6
+ }
7
+ }
2
8
  /** Koishi 插件名称 */
3
9
  export declare const name = "litematic-renderer";
4
10
  /** 插件可选注入的 Koishi 服务 */
@@ -9,6 +15,26 @@ export declare const inject: {
9
15
  export interface Config {
10
16
  /** QQ 机器人的接入方式 */
11
17
  qqBotType: QqBotType;
18
+ /** 官方 QQ 的实验性固定出口方式;默认关闭,不改变既有网络行为 */
19
+ officialProxyMode: OfficialProxyMode;
20
+ /** 已有 HTTP(S) 或 SOCKS 代理地址,仅在 proxy 模式使用 */
21
+ officialProxyUrl: string;
22
+ /** SSH 固定出口使用的可执行文件;密钥模式不会把秘密写入命令行 */
23
+ sshProxyExecutable: string;
24
+ /** SSH 固定出口服务器的数字 IP */
25
+ sshProxyHost: string;
26
+ /** SSH 固定出口服务器端口 */
27
+ sshProxyPort: number;
28
+ /** SSH 固定出口登录用户 */
29
+ sshProxyUser: string;
30
+ /** SSH 私钥路径;设置后优先于密码 */
31
+ sshProxyPrivateKey: string;
32
+ /** SSH 密码;仅交给 ssh2,不输出到日志或命令行 */
33
+ sshProxyPassword: string;
34
+ /** SSH SOCKS5 代理仅在本机监听的端口 */
35
+ sshProxyLocalPort: number;
36
+ /** 是否启用实验性官方 QQ 静默断线自动重载;默认关闭 */
37
+ officialGatewayWatchdog: boolean;
12
38
  /** 独立渲染器使用的 Java 可执行文件;为空时自动查找 Java 21 或更高版本 */
13
39
  standaloneJavaCommand: string;
14
40
  /** Minecraft 客户端 JAR 或基础资源包路径 */
@@ -16,7 +42,7 @@ export interface Config {
16
42
  /** 自定义材质包路径,后面的材质包具有更高优先级 */
17
43
  resourcePackPaths: string[];
18
44
  /** 用于生成投影图的渲染后端 */
19
- renderEngine: "standalone" | "java" | "webgl" | "cpu";
45
+ renderEngine: "standalone" | "gpuAgent" | "java" | "webgl" | "cpu";
20
46
  /** 独立 Java 渲染器 JAR 的路径 */
21
47
  standaloneRendererJar: string;
22
48
  /** 独立 Java 渲染任务的超时时间,单位为毫秒 */
@@ -29,6 +55,8 @@ export interface Config {
29
55
  standaloneJavaMemoryRestartLimit: number;
30
56
  /** 自动处理的最大文件大小,单位为 KiB */
31
57
  maxFileSize: number;
58
+ /** 私聊自动处理的独立文件大小上限,单位为 KiB */
59
+ privateMaxFileSize: number;
32
60
  /** 投影声明方块数及所有区域总体积的上限 */
33
61
  maxBlocks: number;
34
62
  /** 六面图和 CPU 渲染结果的最长边像素上限 */
@@ -41,6 +69,8 @@ export interface Config {
41
69
  transparentBackground: boolean;
42
70
  /** 是否默认使用 QQ 合并转发发送渲染结果 */
43
71
  sendAsForward: boolean;
72
+ /** 是否允许私聊附件和私聊命令触发渲染;默认关闭以保持原行为 */
73
+ allowPrivateRender: boolean;
44
74
  /** 是否在每张渲染图前显示视图标题 */
45
75
  showViewTitles: boolean;
46
76
  /** 是否在结果消息中引用并提及发送者 */
@@ -51,6 +81,10 @@ export interface Config {
51
81
  sixFaceLayout: SixFaceLayout;
52
82
  /** 针对指定群聊覆盖的发送选项 */
53
83
  groupSendOptions: GroupSendOption[];
84
+ /** 非空时只允许列表中的群渲染 */
85
+ groupWhitelist: string[];
86
+ /** 始终禁止列表中的群渲染,优先级高于白名单 */
87
+ groupBlacklist: string[];
54
88
  /** 文件下载及 WebGL 页面操作的超时时间,单位为毫秒 */
55
89
  renderTimeout: number;
56
90
  /** 持久化渲染缓存的目录 */
@@ -87,6 +121,20 @@ export interface Config {
87
121
  isometricRotation: number;
88
122
  /** 正二轴测视图的俯仰角 */
89
123
  isometricSlant: number;
124
+ /** 是否启动 GPU Agent v2 WebSocket 服务;与选择渲染引擎分开控制 */
125
+ gpuAgentEnabled: boolean;
126
+ /** GPU Agent v2 服务监听地址 */
127
+ gpuAgentListenHost: string;
128
+ /** GPU Agent v2 服务监听端口 */
129
+ gpuAgentListenPort: number;
130
+ /** GPU Agent v2 WebSocket 路径 */
131
+ gpuAgentPath: string;
132
+ /** 允许连接的 GPU Agent 节点及其独立密钥 */
133
+ gpuAgentNodes: GpuAgentNodeConfig[];
134
+ /** 单个 GPU Agent 渲染任务的等待时间,单位为毫秒 */
135
+ gpuAgentTimeout: number;
136
+ /** GPU Agent 失败后是否回退到本地 standalone */
137
+ gpuAgentFallback: boolean;
90
138
  }
91
139
  /** 自建 QQ 中发送多张渲染图的方式 */
92
140
  export type SendMode = "forward" | "combined";
@@ -96,6 +144,8 @@ export type ReplyAndMentionOverride = "inherit" | "enabled" | "disabled";
96
144
  export type SixFaceLayout = "horizontal" | "vertical";
97
145
  /** QQ 机器人的接入方式 */
98
146
  export type QqBotType = "official" | "selfHosted";
147
+ /** 官方 QQ 实验性固定出口模式 */
148
+ export type OfficialProxyMode = "disabled" | "proxy" | "ssh";
99
149
  /** 指定群聊的消息发送覆盖项 */
100
150
  export interface GroupSendOption {
101
151
  /** 要应用覆盖设置的 QQ 群号 */
@@ -153,6 +203,14 @@ export interface LitematicMetadata {
153
203
  /** 由数据版本号推断出的 Minecraft 版本 */
154
204
  minecraftVersion?: string;
155
205
  }
206
+ /** 官方 QQ 固定出口逻辑所需的配置子集。 */
207
+ type OfficialProxyConfig = Pick<Config, "officialProxyMode" | "officialProxyUrl" | "sshProxyExecutable" | "sshProxyHost" | "sshProxyPort" | "sshProxyUser" | "sshProxyPrivateKey" | "sshProxyPassword" | "sshProxyLocalPort">;
208
+ /** 解析官方 QQ 应使用的出站代理地址。 */
209
+ export declare function resolveOfficialProxyUrl(config: OfficialProxyConfig): string | undefined;
210
+ /** 构造不包含密码的系统 SSH 动态转发参数。 */
211
+ export declare function buildSshProxyArguments(config: OfficialProxyConfig): string[];
212
+ /** 热重载官方 QQ 适配器;只由显式启用的实验性看门狗调用。 */
213
+ export declare function reloadOfficialQqAdapter(ctx: Context, logger: ReturnType<Context["logger"]>): Promise<boolean>;
156
214
  /**
157
215
  * 注册 Litematic 文件监听、中间件、命令、缓存和诊断端点
158
216
  *
@@ -161,6 +219,8 @@ export interface LitematicMetadata {
161
219
  * @returns 无返回值
162
220
  */
163
221
  export declare function apply(ctx: Context, config: Config): void;
222
+ /** 校验并按内容哈希保存 Koishi 控制台上传的 ZIP 材质包。 */
223
+ export declare function saveUploadedResourcePack(filename: unknown, base64: unknown, directory: string): Promise<string>;
164
224
  /**
165
225
  * 为影响渲染结果的配置生成稳定的短哈希
166
226
  *
@@ -215,6 +275,10 @@ type FileSession = Pick<Session, "elements" | "content"> & {
215
275
  data?: unknown;
216
276
  message?: unknown;
217
277
  };
278
+ /** 判断群聊、频道或显式启用的私聊是否允许进入渲染流程。 */
279
+ export declare function canRenderInSession(session: Pick<Session, "guildId" | "isDirect">, allowPrivateRender: boolean): boolean;
280
+ /** 群黑名单优先;白名单非空时只允许精确匹配的群 ID。 */
281
+ export declare function isGroupAllowed(config: Pick<Config, "groupWhitelist" | "groupBlacklist">, groupId?: string | null): boolean;
218
282
  /**
219
283
  * 在 Koishi 会话元素及常见适配器原始事件字段中查找 Litematic 文件
220
284
  *
@@ -244,6 +308,20 @@ export declare function standaloneJvmOptions(maxHeapMb: number): string[];
244
308
  * @returns 输出包含常见内存错误或退出码为 137 时返回 `true`
245
309
  */
246
310
  export declare function isJavaMemoryFailure(output: string, exitCode?: number | null): boolean;
311
+ /**
312
+ * 通过文件任务队列请求 Fabric 客户端渲染投影
313
+ *
314
+ * @param input - Litematic 输入文件路径
315
+ * @param output - 渲染输出目录
316
+ * @param config - Java 渲染桥相关配置
317
+ * @returns 渲染桥返回成功结果后解决的 Promise
318
+ * @throws 渲染桥未就绪、材质包不匹配、渲染失败或超时时抛出错误
319
+ */
320
+ /** 创建与 GPU Agent v2 协议兼容的双视图任务。 */
321
+ export declare function createGpuRenderRequest(filename: string, config: Pick<Config, "outputSize" | "background" | "transparentBackground" | "javaSupersampling" | "isometricRotation" | "isometricSlant" | "isometricFill">, source?: {
322
+ group?: string;
323
+ user?: string;
324
+ }): GpuRenderRequest;
247
325
  /**
248
326
  * 将单次独立渲染诊断合并到插件级诊断文件
249
327
  *
package/lib/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.Config=exports.inject=exports.name=void 0,exports.apply=apply,exports.hashRenderConfiguration=hashRenderConfiguration,exports.patchOneBotAdapterSource=patchOneBotAdapterSource,exports.cacheNameSegment=cacheNameSegment,exports.enforceCacheLimit=enforceCacheLimit,exports.findLitematicFile=findLitematicFile,exports.supportsStandaloneJavaVersion=supportsStandaloneJavaVersion,exports.standaloneJvmOptions=standaloneJvmOptions,exports.isJavaMemoryFailure=isJavaMemoryFailure,exports.mergeRenderDiagnostics=mergeRenderDiagnostics,exports.formatRenderError=formatRenderError,exports.resolveSendOptions=resolveSendOptions,exports.sendImages=sendImages,exports.composeQqOverview=composeQqOverview,exports.replyElements=replyElements,exports.parseLitematicMetadata=parseLitematicMetadata,exports.validateProjectionLimits=validateProjectionLimits,exports.formatLitematicMetadata=formatLitematicMetadata,exports.parseLitematic=parseLitematic,exports.renderSchematic=renderSchematic,exports.renderSixFaceOverview=renderSixFaceOverview;const koishi_1=require("koishi"),node_crypto_1=require("node:crypto"),node_fs_1=require("node:fs"),node_path_1=require("node:path"),node_url_1=require("node:url"),node_child_process_1=require("node:child_process"),node_zlib_1=require("node:zlib"),pngjs_1=require("pngjs"),Sentry=require("@sentry/node-core/light");exports.name="litematic-renderer",exports.inject={optional:["puppeteer","server"]};const CACHE_FORMAT_VERSION=14,packageVersion=require("../package.json").version,PLUGIN_VERSION="string"==typeof packageVersion?packageVersion:"unknown";exports.Config=koishi_1.Schema.intersect([koishi_1.Schema.object({qqBotType:koishi_1.Schema.union([koishi_1.Schema.const("official").description("QQ 官方机器人"),koishi_1.Schema.const("selfHosted").description("自建 QQ(OneBot / NapCat)")]).role("radio").default("selfHosted").description("机器人接入类型;决定图片、合并转发、引用和 @ 的发送方式"),patchOneBotGroupUpload:koishi_1.Schema.boolean().default(!0).description("自建 QQ:启动时修复 OneBot 群文件上传事件;官方机器人不使用此项")}).description("机器人接入"),koishi_1.Schema.object({renderEngine:koishi_1.Schema.union([koishi_1.Schema.const("standalone"),koishi_1.Schema.const("java"),koishi_1.Schema.const("webgl"),koishi_1.Schema.const("cpu")]).default("standalone").description("渲染引擎;standalone 为无需客户端的独立 Java 资源包渲染器"),maxFileSize:koishi_1.Schema.natural().min(1).default(1024).description("自动处理的最大文件大小(KB)"),maxBlocks:koishi_1.Schema.natural().min(1).max(2e9).default(5e6).description("投影区域体积及声明方块数上限;用于拒绝尺寸异常或资源消耗过大的投影"),outputSize:koishi_1.Schema.natural().min(128).max(4096).default(1024).description("六面图和 CPU 渲染最长边的最大像素数"),background:koishi_1.Schema.string().default("#000000").description("PNG 背景颜色"),transparentBackground:koishi_1.Schema.boolean().default(!1).description("输出透明背景"),isometricFill:koishi_1.Schema.percent().default(.78).description("正二轴测主体在画布中的最大占比"),isometricRotation:koishi_1.Schema.number().min(0).max(360).step(1).default(135).description("Isometric Renders 绕竖直轴的基准旋转角"),isometricSlant:koishi_1.Schema.number().min(-90).max(90).step(1).default(36).description("Isometric Renders 俯仰角;36° 为正二轴测预设")}).description("渲染设置"),koishi_1.Schema.object({sendAsForward:koishi_1.Schema.boolean().default(!1).description("仅自建 QQ:开启为合并转发,关闭为联合发送;官方 QQ 忽略此项"),showViewTitles:koishi_1.Schema.boolean().default(!1).description("仅自建 QQ:发送图片时显示视图标题"),replyAndMention:koishi_1.Schema.boolean().default(!1).description("自建 QQ 会引用并 @ 发送者;官方 QQ 仅引用,避免显示 OpenID"),sixFaceOverview:koishi_1.Schema.boolean().default(!0).description("合并转发时生成并附加上、下、东、南、西、北六面正交合成图"),sixFaceLayout:koishi_1.Schema.union([koishi_1.Schema.const("horizontal").description("横向 3×2"),koishi_1.Schema.const("vertical").description("纵向 2×3")]).default("horizontal").description("六面正交合成图布局;最长边由 outputSize 控制"),groupSendOptions:koishi_1.Schema.array(koishi_1.Schema.object({groupId:koishi_1.Schema.string().description("QQ群号"),sendMode:koishi_1.Schema.union([koishi_1.Schema.const("forward").description("合并转发"),koishi_1.Schema.const("combined").description("联合发送(一条普通消息)")]).default("forward"),replyAndMention:koishi_1.Schema.union([koishi_1.Schema.const("inherit").description("继承全局设置"),koishi_1.Schema.const("enabled").description("开启回复 @"),koishi_1.Schema.const("disabled").description("关闭回复 @")]).default("inherit")})).default([]).description("按群覆盖发送方式和回复设置;官方 QQ 只使用引用开关,自建 QQ 同时使用发送模式和 @")}).description("发送设置"),koishi_1.Schema.object({standaloneJavaCommand:koishi_1.Schema.string().default("").description("独立渲染器使用的 Java 可执行文件;推荐 Java 21+,留空自动查找"),minecraftJarPath:koishi_1.Schema.string().default("").description("可选的 Minecraft 客户端 JAR 或基础资源包;留空使用内置 26.2 原版资源"),resourcePackPaths:koishi_1.Schema.array(koishi_1.Schema.string()).default([]).description("自定义材质包路径;越靠后优先级越高"),standaloneRendererJar:koishi_1.Schema.string().default("").description("独立渲染器 JAR;留空使用插件内置版本"),standaloneRenderTimeout:koishi_1.Schema.natural().min(1e4).default(18e4).description("独立 Java 渲染超时(毫秒)"),standaloneJavaMaxHeapMb:koishi_1.Schema.natural().min(128).max(32768).step(8).default(200).description("首次独立渲染的最大堆内存(MiB)"),standaloneJavaRetryMaxHeapMb:koishi_1.Schema.natural().min(256).max(32768).step(128).default(2048).description("内存不足时新进程重试的最大堆内存(MiB)"),standaloneJavaMemoryRestartLimit:koishi_1.Schema.natural().max(3).default(1).description("检测到内存不足后启动全新进程重试的次数"),javaResolution:koishi_1.Schema.natural().min(256).max(4096).default(1024).description("Java 渲染最终输出边长"),javaSupersampling:koishi_1.Schema.natural().min(1).max(4).default(2).description("离屏超采样倍数")}).description("独立渲染器"),koishi_1.Schema.object({renderTimeout:koishi_1.Schema.natural().min(1e3).default(3e4).description("文件下载超时(毫秒)"),cacheDirectory:koishi_1.Schema.string().default("data/litematic-renderer-cache").description("持久缓存目录;按插件版本和投影 SHA-256 分区"),cacheMaxSizeGb:koishi_1.Schema.number().min(1).max(1024).step(1).default(20).description("所有版本缓存总上限(GiB),超出后按最久未使用清理"),diagnosticsFilePath:koishi_1.Schema.string().default("data/litematic-renderer-diagnostics.json").description("渲染诊断持久化文件路径;导出地址固定为 /litematic-renderer/diagnostics"),sentryDsn:koishi_1.Schema.string().default("").description("Sentry DSN,用于上报渲染错误;留空时读取 SENTRY_DSN 环境变量").experimental()}).description("缓存与诊断"),koishi_1.Schema.object({isometricCellSize:koishi_1.Schema.natural().min(2).max(32).default(7).description("CPU 正二轴测方块菱形半宽"),javaRenderTimeout:koishi_1.Schema.natural().min(1e4).default(18e4).description("等待 Minecraft Java 渲染桥的超时(毫秒)"),webglQuality:koishi_1.Schema.union([koishi_1.Schema.const("standard"),koishi_1.Schema.const("high"),koishi_1.Schema.const("ultra")]).default("high").description("WebGL 渲染质量"),webglWidth:koishi_1.Schema.natural().min(256).max(2048).default(800).description("WebGL 单张图宽度"),webglHeight:koishi_1.Schema.natural().min(256).max(2048).default(600).description("WebGL 单张图高度"),isometricSquare:koishi_1.Schema.boolean().default(!0).description("将 WebGL 正二轴测图输出为正方形"),javaBridgeDirectory:koishi_1.Schema.string().default("").description("renderEngine=java 时使用的 Fabric Java 渲染桥任务目录"),gpuRendererCommand:koishi_1.Schema.string().default("").description("可选外部 GPU 渲染器命令;非空时优先尝试,失败后回退到 renderEngine")}).description("高级设置")]);const activeStandaloneJavaProcesses=new Set;function apply(e,t){const i=e.logger(exports.name),a=1024*t.maxFileSize,n=t.sentryDsn||process.env.SENTRY_DSN||"";n&&Sentry.init({dsn:n,tracesSampleRate:1});const r=(e,i)=>{n&&Sentry.withScope(a=>{a.setFingerprint([i]),a.setTag("entrypoint",i),a.setContext("render",{engine:t.renderEngine}),Sentry.captureException(e)})};e.on("dispose",()=>terminateActiveStandaloneJavaProcesses(i)),"selfHosted"===t.qqBotType&&ensureOneBotGroupUploadCompatibility(i,t.patchOneBotGroupUpload);const o=(0,node_path_1.resolve)(stringOrDefault(t.diagnosticsFilePath,"data/litematic-renderer-diagnostics.json")),s=(0,node_path_1.join)((0,node_path_1.dirname)(o),"litematic-renderer-diagnostics-archive"),c=(0,node_path_1.join)((0,node_path_1.dirname)(o),"litematic-renderer-diagnostics-disabled");rotateDiagnosticsFile(o,s,i);const d=(0,node_path_1.resolve)(stringOrDefault(t.cacheDirectory,"data/litematic-renderer-cache")),l=(0,node_path_1.join)(d,`v${cachePathSegment(PLUGIN_VERSION)}`),m=Math.floor(t.cacheMaxSizeGb*1024**3),u=new Map;node_fs_1.promises.mkdir(l,{recursive:!0}).then(()=>enforceCacheLimit(d,m)).catch(e=>i.warn(`缓存初始化失败:${e instanceof Error?e.message:String(e)}`));const h=async(n,r="schematic.litematic",s,c=!1)=>{const h=s??await download(e,n,a,t.renderTimeout),f=parseLitematicMetadata(h);validateProjectionLimits(h,f,t.maxBlocks);const p=formatLitematicMetadata(f,r),g=projectionNameFromFilename(r),_=(0,node_crypto_1.createHash)("sha256").update(h).digest("hex"),w="standalone"===t.renderEngine?await resolveMinecraftResources(t):"",y="standalone"===t.renderEngine?await fingerprintFiles([w,...resourcePackPaths(t.resourcePackPaths)]):[],S=hashRenderConfiguration({version:CACHE_FORMAT_VERSION,outputSize:t.outputSize,isometricCellSize:t.isometricCellSize,background:t.background,transparentBackground:t.transparentBackground,gpuRendererCommand:t.gpuRendererCommand,renderEngine:t.renderEngine,standaloneRendererJar:t.standaloneRendererJar,resourceFingerprint:y,javaResolution:t.javaResolution,javaSupersampling:t.javaSupersampling,webglQuality:t.webglQuality,webglWidth:t.webglWidth,webglHeight:t.webglHeight,isometricSquare:t.isometricSquare,isometricFill:t.isometricFill,isometricRotation:t.isometricRotation,isometricSlant:t.isometricSlant,sixFaceLayout:t.sixFaceLayout}),v=cacheNameSegment(r),b=(0,node_path_1.join)(l,`${v}-${_}-${S}`),x=(0,node_path_1.join)(b,"projection.litematic");await node_fs_1.promises.mkdir(b,{recursive:!0}),await exists(x)||await node_fs_1.promises.writeFile(x,h),await ensureCacheMetadata(b,{pluginVersion:PLUGIN_VERSION,cacheFormatVersion:CACHE_FORMAT_VERSION,contentSha256:_,renderConfigSha256:S,sourceFilename:(0,node_path_1.basename)(r),createdAt:(new Date).toISOString()});const k=["isometric.png","isometric-reverse.png"].map(e=>(0,node_path_1.join)(b,e));if((await Promise.all(k.map(exists))).every(Boolean))i.debug(`命中投影缓存:${_}`);else{let a=u.get(b);a?i.debug(`等待相同投影的缓存任务:${_}`):(a=(async()=>{try{if((await Promise.all(k.map(exists))).every(Boolean))return;if(!Boolean(t.gpuRendererCommand&&await renderWithGpu(t.gpuRendererCommand,x,b,t.renderTimeout,i))||!(await Promise.all(k.map(exists))).every(Boolean))if("standalone"===t.renderEngine)await renderWithStandalone(x,b,w,t,i),await mergeRenderDiagnostics(b,o,i);else if("java"===t.renderEngine)await renderWithJavaBridge(x,b,t);else{const a=parseLitematic(h);let n;if("webgl"===t.renderEngine&&e.puppeteer)try{n=await renderWithWebgl(e,h,t)}catch(e){i.warn(`WebGL 渲染失败,回退 CPU:${e instanceof Error?e.message:String(e)}`),n=renderSchematic(a,t)}else n=renderSchematic(a,t);await Promise.all(n.map(e=>node_fs_1.promises.writeFile((0,node_path_1.join)(b,e.title),e.png)))}if(!(await Promise.all(k.map(exists))).every(Boolean))throw new Error("渲染器没有生成两张正二轴测 PNG")}catch(e){throw await appendRenderError(b,o,t.renderEngine,e,i),e}})().finally(()=>u.delete(b)),u.set(b,a)),await a}const M=k.map((e,t)=>({title:["正二轴测","反向正二轴测(旋转 180°)"][t],path:e}));if(c){const e=(0,node_path_1.join)(b,"six-faces.png");if(!await exists(e)){const a=`${b}:six-face`;let n=u.get(a);n||(n=(async()=>{try{if("standalone"===t.renderEngine)try{await renderWithStandalone(x,b,w,t,i,"six-face")}catch(e){i.warn(`真实材质六面图生成失败,回退方块颜色投影:${e instanceof Error?e.message:String(e)}`)}if(!await exists(e)){const i=parseLitematic(h);await node_fs_1.promises.writeFile(e,renderSixFaceOverview(i,t,t.sixFaceLayout))}}catch(e){i.warn(`六面正交图生成失败,继续发送轴测图:${e instanceof Error?e.message:String(e)}`)}})().finally(()=>u.delete(a)),u.set(a,n)),await n}await exists(e)&&M.push({title:"六面正投影",path:e})}return await touchCacheEntry(b),await enforceCacheLimit(d,m,b),{images:M,metadata:p,projectionName:g}};e.middleware(async(e,n)=>{if(!e.guildId)return n();const s=findLitematicFile(e);if(!s)return n();const c=await resolveFileUrl(e,s);if(!c)return i.warn(`检测到 Litematic 文件但无法获取下载地址:${fileName(s)??"unknown.litematic"};请检查 OneBot/NapCat 的 get_group_file_url 接口`),await e.send([...replyElements(e,t.qqBotType),(0,koishi_1.h)("text",{content:"检测到投影文件,但无法获取群文件下载地址,请检查机器人文件接口"})]),n();if(isOverLimit(s.size,a))return await appendGlobalRenderError(o,`input:${(0,node_path_1.basename)(fileName(s)??"unknown.litematic")}`,"input",`file size exceeds ${t.maxFileSize} KB`,i),await e.send([...replyElements(e,t.qqBotType),(0,koishi_1.h)("text",{content:`文件大小超过 ${(t.maxFileSize/1024).toFixed(2)} MB,无法渲染`})]),n();if(!isUnderLimit(s.size,a))return n();try{const i=resolveSendOptions(t,e.guildId),a=await h(c,fileName(s)??"schematic.litematic",void 0,i.sixFaceOverview);await sendImages(e,a.images,a.metadata,i,a.projectionName)}catch(a){return r(a,"group-file-upload"),i.warn(a),await e.send([...replyElements(e,t.qqBotType),(0,koishi_1.h)("text",{content:formatRenderError(a,t)})]),n()}return n()}),e.command("litematic.render <url:string>","渲染可直接下载的 .litematic 文件").action(async({session:e},i)=>{if(!i)return"请提供 .litematic 文件的直链";if(!e)return"此命令只能在消息会话中执行";try{const a=resolveSendOptions(t,e.guildId),n=await h(i,"schematic.litematic",void 0,a.sixFaceOverview);await sendImages(e,n.images,n.metadata,a,n.projectionName)}catch(e){return r(e,"litematic.render"),formatRenderError(e,t)}}),e.command("litematic.cache.clear","清理投影渲染缓存",{authority:3}).action(async()=>(await node_fs_1.promises.rm(d,{recursive:!0,force:!0}),await node_fs_1.promises.mkdir(l,{recursive:!0}),"投影渲染缓存已清理")),e.command("litematic.errors.export","export render diagnostics",{authority:3}).action(async()=>`Diagnostics file: ${o}`),e.command("litematic.errors.disable","disable current render diagnostics snapshot",{authority:3}).action(async()=>disableDiagnosticsSnapshot(o,c));const f=e.server;f?.get&&f.get("/litematic-renderer/diagnostics",async e=>{e.type="application/json",e.set("Content-Disposition",'attachment; filename="litematic-renderer-diagnostics.json"'),e.body=await node_fs_1.promises.readFile(o,"utf8").catch(()=>'{"format":2,"blocks":[],"errors":[]}')})}function hashRenderConfiguration(e){return(0,node_crypto_1.createHash)("sha256").update(JSON.stringify(e)).digest("hex").slice(0,16)}function patchOneBotAdapterSource(e){if(/^\s*case ["']group_upload["']:/m.test(e))return{source:e,changed:!1,alreadyCompatible:!0};const t=e.match(/\(0,\s*(import_[\w$]+)\.hyphenate\)\(data\.sub_type\)/)?.[1]??"import_koishi2",i=/ \/\/ https:\/\/github\.com\/koishijs\/koishi-plugin-adapter-onebot\/issues\/33\r?\n \/\/ case 'offline_file':\r?\n \/\/ session\.elements = \[h\('file', data\.file\)\]\r?\n \/\/ session\.type = 'message'\r?\n \/\/ session\.subtype = 'private'\r?\n \/\/ session\.isDirect = true\r?\n \/\/ session\.subsubtype = 'offline-file-added'\r?\n \/\/ break\r?\n \/\/ case 'group_upload':\r?\n \/\/ session\.elements = \[h\('file', data\.file\)\]\r?\n \/\/ session\.type = 'message'\r?\n \/\/ session\.subtype = 'group'\r?\n \/\/ session\.subsubtype = 'guild-file-added'\r?\n \/\/ break/;if(!i.test(e))return{source:e,changed:!1,alreadyCompatible:!1};const a=[" // Restored by koishi-plugin-litematic-renderer so NapCat group file uploads reach middleware",' case "offline_file":',` session.elements = [(0, ${t}.h)("file", data.file)];`,' session.type = "message";',' session.subtype = "private";'," session.isDirect = true;",' session.subsubtype = "offline-file-added";'," break;",' case "group_upload":',` session.elements = [(0, ${t}.h)("file", data.file)];`,' session.type = "message";',' session.subtype = "group";',' session.subsubtype = "guild-file-added";'," break;"].join("\n");return{source:e.replace(i,a),changed:!0,alreadyCompatible:!1}}async function ensureOneBotGroupUploadCompatibility(e,t){if(!t)return;let i;try{i=require.resolve("koishi-plugin-adapter-onebot/package.json",{paths:[process.cwd(),__dirname]})}catch{return void e.debug("未安装 koishi-plugin-adapter-onebot,跳过群文件上传事件兼容修复")}try{const t=(0,node_path_1.dirname)(i),a=JSON.parse(await node_fs_1.promises.readFile(i,"utf8")),n=(0,node_path_1.join)(t,"lib","index.js"),r=await node_fs_1.promises.readFile(n,"utf8"),o=patchOneBotAdapterSource(r);if(o.alreadyCompatible)return void e.debug(`koishi-plugin-adapter-onebot@${a.version??"unknown"} 已支持 group_upload`);if(!o.changed)return void e.warn(`koishi-plugin-adapter-onebot@${a.version??"unknown"} 未识别到可自动修复的 group_upload 代码段`);const s=`${n}.litematic-renderer.bak`;await exists(s)||await node_fs_1.promises.writeFile(s,r),await node_fs_1.promises.writeFile(n,o.source),e.warn(`已修复 koishi-plugin-adapter-onebot@${a.version??"unknown"} 的群文件上传事件映射;请重启 Koishi 让已加载的 OneBot 适配器生效`)}catch(t){e.warn(`修复 OneBot 群文件上传事件失败:${t instanceof Error?t.message:String(t)}`)}}function cachePathSegment(e){return e.replace(/[^a-zA-Z0-9._-]/g,"_")||"unknown"}function cacheNameSegment(e){return((0,node_path_1.basename)(e||"schematic.litematic",(0,node_path_1.extname)(e||"")).replace(/[<>:"/\\|?*\u0000-\u001f]/g,"_").replace(/[. ]+$/g,"").trim()||"schematic").slice(0,80)}function projectionNameFromFilename(e){return(0,node_path_1.basename)(e||"schematic.litematic").replace(/\.litematic$/i,"").trim()||"schematic"}async function ensureCacheMetadata(e,t){const i=(0,node_path_1.join)(e,"cache.json");await exists(i)||await node_fs_1.promises.writeFile(i,JSON.stringify(t,null,2)+"\n")}async function touchCacheEntry(e){const t=new Date;await node_fs_1.promises.utimes(e,t,t).catch(()=>{})}async function directorySize(e){let t=0;const i=await node_fs_1.promises.readdir(e,{withFileTypes:!0}).catch(e=>{if("ENOENT"===e?.code)return[];throw e});for(const a of i){const i=(0,node_path_1.join)(e,a.name);a.isDirectory()?t+=await directorySize(i):a.isFile()&&(t+=(await node_fs_1.promises.stat(i)).size)}return t}async function enforceCacheLimit(e,t,i){const a=[],n=await node_fs_1.promises.readdir(e,{withFileTypes:!0}).catch(e=>{if("ENOENT"===e?.code)return[];throw e});for(const t of n){if(!t.isDirectory())continue;const i=(0,node_path_1.join)(e,t.name),n=await node_fs_1.promises.readdir(i,{withFileTypes:!0});for(const e of n){if(!e.isDirectory())continue;const t=(0,node_path_1.join)(i,e.name),[n,r]=await Promise.all([directorySize(t),node_fs_1.promises.stat(t)]);a.push({path:t,size:n,lastUsed:r.mtimeMs})}}let r=a.reduce((e,t)=>e+t.size,0),o=0,s=0;const c=i&&(0,node_path_1.resolve)(i);a.sort((e,t)=>e.lastUsed-t.lastUsed);for(const e of a){if(r<=t)break;c&&(0,node_path_1.resolve)(e.path)===c||(await node_fs_1.promises.rm(e.path,{recursive:!0,force:!0}),r-=e.size,o+=e.size,s++)}return{totalBytes:r,removedBytes:o,removedEntries:s}}function fileElements(e){return[...e.elements??[],...koishi_1.h.parse(e.content??"")]}function findLitematicFile(e){const t=fileElements(e).filter(e=>"file"===e.type),i=t.find(e=>".litematic"===(0,node_path_1.extname)(fileName(e.attrs)??"").toLowerCase());if(i?.attrs)return i.attrs;for(const t of[e.event,e._data,e.data,e.message]){const e=findLitematicInFilePayload(t);if(e)return e}const a=inferLitematicName(e.content);return a&&t[0]?.attrs?{...t[0].attrs,name:a}:void 0}function findLitematicInFilePayload(e,t=new Set,i=0){if(!e||"object"!=typeof e||i>6||t.has(e))return;t.add(e);const a=e,n=a,r=n.file&&"object"==typeof n.file?n.file:void 0;if(r&&fileName(r)&&".litematic"===(0,node_path_1.extname)(fileName(r)).toLowerCase())return{...r,id:r.id??n.id,file_id:r.file_id??n.file_id,fileId:r.fileId??n.fileId,busid:r.busid??n.busid};if(fileName(n)&&".litematic"===(0,node_path_1.extname)(fileName(n)).toLowerCase())return n;for(const[e,n]of Object.entries(a))if(n&&"object"==typeof n&&/file|upload|message|data|event/i.test(e)){const e=findLitematicInFilePayload(n,t,i+1);if(e)return e}}function inferLitematicName(e){if(!e)return;const t=e.match(/(?:file|文件)\s+(.+?\.litematic)(?=$|[\]】\r\n])/i);return t?.[1]?t[1].trim():e.match(/([^\s\]】<>"']+\.litematic)(?=$|[\s\]】])/i)?.[1]?.trim()}function fileName(e){const t=e.file&&"object"==typeof e.file?e.file:void 0,i=e.name??e.filename??e.file_name??e.fileName??t?.name??t?.filename??t?.file_name??t?.fileName;return"string"==typeof i&&i.trim()?i.trim():void 0}async function resolveFileUrl(e,t){const i=t.file&&"object"==typeof t.file?t.file:void 0,a=t.url??t.src??("string"==typeof t.file?t.file:void 0)??i?.url??i?.src;if("string"==typeof a&&a)return a;const n=t.id??t.file_id??t.fileId??i?.id??i?.file_id??i?.fileId,r=e.bot.internal;if(!e.guildId||!n||"function"!=typeof r?.getGroupFileUrl)return;const o=await r.getGroupFileUrl(e.guildId,n,Number(t.busid??i?.busid??0));return"string"==typeof o?o:o?.url}function isUnderLimit(e,t){if(null==e||""===e)return!0;const i=Number(e);return Number.isFinite(i)&&i>0&&i<=t}async function download(e,t,i,a){const n=await e.http.get(t,{responseType:"arraybuffer",timeout:a}),r=Buffer.from(n);if(r.byteLength>i)throw new Error(`文件超过 ${(i/1024/1024).toFixed(2)} MB 限制`);return r}async function exists(e){try{return await node_fs_1.promises.access(e,node_fs_1.constants.R_OK),!0}catch{return!1}}function stringOrDefault(e,t){return"string"==typeof e&&e.trim()?e:t}function resourcePackPaths(e){return Array.isArray(e)?e.filter(e=>"string"==typeof e&&Boolean(e.trim())):[]}async function fingerprintFiles(e){return Promise.all(e.filter(e=>"string"==typeof e&&Boolean(e.trim())).map(async e=>{const t=(0,node_path_1.resolve)(e);try{const e=await node_fs_1.promises.stat(t);return{path:t,size:e.size,modified:e.mtimeMs}}catch{return{path:t,missing:!0}}}))}async function resolveStandaloneJavaCommand(e){const t=e.trim();if(t)return t;const i="win32"===process.platform?"java.exe":"java",a=new Set;if(process.env.JAVA_HOME&&a.add((0,node_path_1.join)(process.env.JAVA_HOME,"bin",i)),"win32"===process.platform){const e=new Set([(0,node_path_1.join)(process.env.ProgramFiles??"C:/Program Files","Java"),(0,node_path_1.join)(process.env.ProgramFiles??"C:/Program Files","Eclipse Adoptium"),(0,node_path_1.join)(process.env.ProgramFiles??"C:/Program Files","Microsoft","jdk")]);for(const t of e)try{const e=await node_fs_1.promises.readdir(t,{withFileTypes:!0});for(const n of e)n.isDirectory()&&/(?:^|[-_])21(?:[._-]|$)/.test(n.name)&&a.add((0,node_path_1.join)(t,n.name,"bin",i))}catch{}}a.add("java");for(const e of a)if(supportsStandaloneJavaVersion(await javaMajorVersion(e)))return e;throw new Error("未找到 Java 21+;请安装 Java 21 或更高版本,设置 JAVA_HOME,或填写 standaloneJavaCommand")}function isOverLimit(e,t){if(null==e||""===e)return!1;const i=Number(e);return Number.isFinite(i)&&i>t}async function javaMajorVersion(e){return new Promise(t=>{let i="",a=!1;const n=e=>{a||(a=!0,clearTimeout(s),t(e))},r=(0,node_child_process_1.spawn)(e,["-version"],{shell:!1,windowsHide:!0}),o=e=>{i=(i+String(e)).slice(-2e3)};r.stdout?.on("data",o),r.stderr?.on("data",o),r.once("error",()=>n()),r.once("close",()=>{const e=i.match(/(?:java|openjdk) version "(\d+)/i);n(e?Number(e[1]):void 0)});const s=setTimeout(()=>{r.kill(),n()},5e3)})}function supportsStandaloneJavaVersion(e){return void 0!==e&&e>=21}function standaloneJvmOptions(e){return["-Xms128m",`-Xmx${Math.max(128,Math.min(32768,Math.floor(e)))}m`,"-XX:+UseG1GC","-XX:+UseStringDeduplication","-XX:+ExitOnOutOfMemoryError"]}function isJavaMemoryFailure(e,t){return 137===t||/outofmemoryerror|java heap space|gc overhead limit|cannot reserve enough space|native memory allocation|insufficient memory|malloc failed|os::commit_memory/i.test(e)}async function resolveMinecraftResources(e){const t="string"==typeof e.minecraftJarPath?e.minecraftJarPath.trim():"";if(t&&await exists((0,node_path_1.resolve)(t)))return(0,node_path_1.resolve)(t);const i=(0,node_path_1.resolve)((0,node_path_1.join)(__dirname,"../assets/vanilla-resources/Minecraft-26.2-Vanilla-Resources.zip"));if(!await exists(i))throw new Error(`插件内置的 Minecraft 26.2 原版资源包不存在:${i}`);return i}async function renderWithStandalone(e,t,i,a,n,r="isometric"){const o=(0,node_path_1.resolve)(a.standaloneRendererJar||(0,node_path_1.join)(__dirname,"../assets/standalone-renderer/litematic-standalone-renderer-0.2.7-patch.1.jar"));if(!await exists(o))throw new Error(`独立 Java 渲染器不存在:${o}`);if(!await exists(i))throw new Error(`Minecraft 资源 JAR 不存在:${i}`);for(const e of resourcePackPaths(a.resourcePackPaths))if(!await exists((0,node_path_1.resolve)(e)))throw new Error(`材质包不存在:${e}`);const s=["-Djava.awt.headless=true","-jar",o,"--input",(0,node_path_1.resolve)(e),"--output",(0,node_path_1.resolve)(t),"--minecraft-jar",i,"--resolution",String(a.javaResolution),"--supersampling",String(a.javaSupersampling),"--rotation",String(a.isometricRotation),"--slant",String(a.isometricSlant),"--fill",String(a.isometricFill),"--background",a.background];"six-face"===r&&s.push("--six-face-only","--six-face-resolution",String(a.outputSize),"--six-face-layout",a.sixFaceLayout),a.transparentBackground&&s.push("--transparent-background");for(const e of resourcePackPaths(a.resourcePackPaths))s.push("--resource-pack",(0,node_path_1.resolve)(e));const c=await resolveStandaloneJavaCommand(a.standaloneJavaCommand);for(let e=0;e<=a.standaloneJavaMemoryRestartLimit;e++){const i=0===e?a.standaloneJavaMaxHeapMb:a.standaloneJavaRetryMaxHeapMb,o=await runStandaloneJava(c,[...standaloneJvmOptions(i),...s],a.standaloneRenderTimeout);if(0===o.code)return;const d=isJavaMemoryFailure(o.stderr,o.code);if(d&&e<a.standaloneJavaMemoryRestartLimit){n.warn(`独立 Java 渲染超出 ${a.standaloneJavaMaxHeapMb} MiB 堆内存限制;正在启动全新进程重试(${e+1}/${a.standaloneJavaMemoryRestartLimit})`);const i="six-face"===r?["six-faces.png"]:["isometric.png","isometric-reverse.png"];await Promise.all(i.map(e=>node_fs_1.promises.rm((0,node_path_1.join)(t,e),{force:!0})));continue}const l=d?`;Java 内存不足(最大堆 ${a.standaloneJavaMaxHeapMb} MiB,已重启 ${e} 次)`:"";throw new Error(`独立 Java 渲染器退出码 ${o.code}${l}${o.stderr?`:${o.stderr.trim()}`:""}`)}}async function runStandaloneJava(e,t,i){return new Promise((a,n)=>{const r=(0,node_child_process_1.spawn)(e,t,{shell:!1,windowsHide:!0});activeStandaloneJavaProcesses.add(r);let o="",s=!1;const c=()=>activeStandaloneJavaProcesses.delete(r);r.stderr?.on("data",e=>{o=(o+String(e)).slice(-6e3)});const d=setTimeout(()=>{s||(s=!0,(async()=>{await terminateStandaloneJavaProcess(r),c(),n(new Error("Standalone Java render timed out; process terminated and memory reclaimed"))})())},i);r.once("error",e=>{s||(s=!0,clearTimeout(d),terminateStandaloneJavaProcess(r),c(),n(e))}),r.once("close",e=>{s||(s=!0,clearTimeout(d),c(),a({code:e,stderr:o}))})})}async function terminateStandaloneJavaProcess(e){const t=e.pid;t&&("win32"===process.platform&&await new Promise(e=>{const i=(0,node_child_process_1.spawn)("taskkill.exe",["/PID",String(t),"/T","/F"],{windowsHide:!0});i.once("close",()=>e()),i.once("error",()=>e())}),null!=e.exitCode||e.killed||e.kill())}async function terminateActiveStandaloneJavaProcesses(e){const t=[...activeStandaloneJavaProcesses];t.length&&(await Promise.all(t.map(e=>terminateStandaloneJavaProcess(e))),e.info(`已终止 ${t.length} 个独立渲染 Java 进程并回收其子进程`))}async function renderWithJavaBridge(e,t,i){if(!i.javaBridgeDirectory.trim())throw new Error("Fabric Java 渲染桥任务目录未配置;使用 java 后端时请填写 javaBridgeDirectory");const a=(0,node_path_1.resolve)(i.javaBridgeDirectory),n=await readJson((0,node_path_1.join)(a,"status.json"));if(!n?.timestamp||Date.now()-n.timestamp>5e3)throw new Error("Minecraft Java 渲染桥未运行;请启动 1.21.1-Fabric 客户端并进入一个世界");if(!n.inWorld)throw new Error("Minecraft Java 渲染桥已启动,但客户端尚未进入世界");const r=n.resourcePacks??[];if(![["XeKr","3.6forMC1.20.2~1.21.5.zip"],["XKRDA","1.19.4~1.21snapshot.zip"]].every(e=>r.some(t=>e.every(e=>t.includes(e)))))throw new Error("Minecraft 客户端没有同时启用 XeKr 红显基础包和 XKRDA 附加包");const o=(0,node_crypto_1.randomUUID)(),s=(0,node_path_1.join)(a,"jobs"),c=(0,node_path_1.join)(a,"results",`${o}.result.json`),d=(0,node_path_1.join)(s,`${o}.job.json`),l=`${d}.tmp`;await node_fs_1.promises.mkdir(s,{recursive:!0}),await node_fs_1.promises.rm(c,{force:!0}),await node_fs_1.promises.writeFile(l,JSON.stringify({id:o,input:(0,node_path_1.resolve)(e),outputDirectory:(0,node_path_1.resolve)(t),resolution:i.javaResolution,supersampling:i.javaSupersampling,rotation:i.isometricRotation,slant:i.isometricSlant,fill:i.isometricFill,background:i.background,transparentBackground:i.transparentBackground})),await node_fs_1.promises.rename(l,d);const m=Date.now()+i.javaRenderTimeout;for(;Date.now()<m;){const e=await readJson(c);if(e){if(await node_fs_1.promises.rm(c,{force:!0}),!e.success)throw new Error(`Minecraft Java 渲染失败:${e.error??"未知错误"}`);return}await new Promise(e=>setTimeout(e,200))}throw new Error(`Minecraft Java 渲染超过 ${Math.round(i.javaRenderTimeout/1e3)} 秒`)}async function readJson(e){try{return JSON.parse(await node_fs_1.promises.readFile(e,"utf8"))}catch(e){if("ENOENT"===e?.code||e instanceof SyntaxError)return;throw e}}async function mergeRenderDiagnostics(e,t,i){const a=(0,node_path_1.join)(e,"render-diagnostics.json"),n=await readJson(a);if(!n)return;const r=Array.isArray(n.blocks)?n.blocks:[],o=Array.isArray(n.errors)?n.errors:[];if(!r.length&&!o.length)return;const s=(0,node_path_1.resolve)(t),c=await readJson(s)??{format:2,generatedAt:(new Date).toISOString(),blocks:[],errors:[]},d=new Map;for(const e of c.blocks??[])d.set(`${e.state}\0${e.reason}`,e);for(const e of r){const t=`${e.state}\0${e.reason}`,i=d.get(t);i?(i.count=Number(i.count??0)+Number(e.count??0),i.samples=[...i.samples??[],...e.samples??[]].slice(0,20)):d.set(t,{...e,samples:e.samples??[]})}const l=new Map;for(const e of c.errors??[])l.set(`${e.renderId}\0${e.engine}\0${e.message}`,e);for(const e of o){const t=`${e.renderId}\0${e.engine}\0${e.message}`,i=l.get(t);i?i.count=Number(i.count??0)+Number(e.count??0):l.set(t,{...e})}const m={format:2,pluginVersion:PLUGIN_VERSION,generatedAt:(new Date).toISOString(),blocks:[...d.values()],errors:[...l.values()]};await node_fs_1.promises.mkdir((0,node_path_1.resolve)(t,".."),{recursive:!0}).catch(()=>{}),await node_fs_1.promises.writeFile(s,JSON.stringify(m,null,2)+"\n"),r.length&&i.warn(`记录 ${r.length} 类无法正常渲染的方块;诊断已保存到 ${s}`),o.length&&i.warn(`记录 ${o.length} 类渲染错误;诊断已保存到 ${s}`)}async function rotateDiagnosticsFile(e,t,i){const a=await readJson(e);if(!a||a.pluginVersion===PLUGIN_VERSION)return;await node_fs_1.promises.mkdir(t,{recursive:!0});const n=cachePathSegment("string"==typeof a.pluginVersion?a.pluginVersion:"legacy"),r=formatDiagnosticsTimestamp(new Date),o=(0,node_path_1.join)(t,`litematic-renderer-diagnostics-v${n}-${r}.json`);await node_fs_1.promises.rename(e,o),i.info(`已将旧版诊断汇总归档到 ${o}`)}async function disableDiagnosticsSnapshot(e,t){if(!await readJson(e))return"当前没有可禁用的诊断汇总";await node_fs_1.promises.mkdir(t,{recursive:!0});const i=(0,node_path_1.join)(t,`litematic-renderer-diagnostics-disabled-${formatDiagnosticsTimestamp(new Date)}.json`);return await node_fs_1.promises.rename(e,i),await node_fs_1.promises.writeFile(e,JSON.stringify({format:2,pluginVersion:PLUGIN_VERSION,generatedAt:(new Date).toISOString(),blocks:[],errors:[]},null,2)+"\n"),`当前诊断已移入禁用文件夹:${i}`}async function appendGlobalRenderError(e,t,i,a,n){const r=(0,node_path_1.resolve)(e),o=await readJson(r)??{format:2,pluginVersion:PLUGIN_VERSION,generatedAt:(new Date).toISOString(),blocks:[],errors:[]};o.format=2,o.pluginVersion=PLUGIN_VERSION,o.errors??=[];const s=o.errors.find(e=>e.renderId===t&&e.engine===i&&e.message===a);s?s.count=Number(s.count??0)+1:o.errors.push({renderId:t,engine:i,message:a,count:1,generatedAt:(new Date).toISOString()}),await node_fs_1.promises.mkdir((0,node_path_1.dirname)(r),{recursive:!0}),await node_fs_1.promises.writeFile(r,JSON.stringify(o,null,2)+"\n"),n.warn(`渲染问题已记录到 ${r}`)}function formatDiagnosticsTimestamp(e){const t=e=>String(e).padStart(2,"0");return`${e.getFullYear()}${t(e.getMonth()+1)}${t(e.getDate())}-${t(e.getHours())}${t(e.getMinutes())}${t(e.getSeconds())}`}async function appendRenderError(e,t,i,a,n){const r=(0,node_path_1.join)(e,"render-diagnostics.json"),o=await readJson(r)??{format:2,generatedAt:(new Date).toISOString(),blocks:[],errors:[]};o.format=2,o.errors??=[];const s=a instanceof Error?a.message:String(a),c=(0,node_path_1.basename)(e),d=o.errors.find(e=>e.renderId===c&&e.engine===i&&e.message===s);d?d.count=Number(d.count??0)+1:o.errors.push({renderId:c,engine:i,message:s,count:1,generatedAt:(new Date).toISOString()}),await node_fs_1.promises.writeFile(r,JSON.stringify(o,null,2)+"\n"),await mergeRenderDiagnostics(e,t,n)}function formatRenderError(e,t){const i=e instanceof Error?e.message:String(e);return/投影方块数或区域体积超过/i.test(i)?`${i},不渲染`:/文件超过|file size|file.*limit/i.test(i)?`文件大小超过 ${(t.maxFileSize/1024).toFixed(2)} MB,不渲染`:/java 21\+|未找到.*java|java.*not found/i.test(i)?"未找到 Java 21+,请在插件配置中检查 Java 可执行文件设置":/standalone renderer|独立.*渲染器.*不存在|渲染器.*not exist/i.test(i)?"独立渲染器不可用,请重装插件或检查 Java 渲染配置":/minecraft.*resource|resource pack|原版资源|材质包.*不存在/i.test(i)?"Minecraft 原版资源包不可用,请检查资源包配置":"投影渲染失败,请检查渲染器配置或导出诊断文件"}async function renderWithGpu(e,t,i,a,n){try{return await new Promise((n,r)=>{const o=(0,node_child_process_1.spawn)(e,["--input",t,"--output",i,"--views","isometric,isometric-reverse"],{shell:!1,windowsHide:!0}),s=setTimeout(()=>{o.kill(),r(new Error("GPU 渲染超时"))},a);o.once("error",r),o.once("exit",e=>0===e?n():r(new Error(`GPU 渲染器退出码:${e}`))),o.once("close",()=>clearTimeout(s))}),!0}catch(e){return n.warn(`GPU 渲染器不可用,回退 CPU:${e instanceof Error?e.message:String(e)}`),!1}}async function renderWithWebgl(e,t,i){const a=await e.puppeteer.page();try{const e=(0,node_url_1.pathToFileURL)((0,node_path_1.resolve)(__dirname,"../assets/threeviews/index.html")).href;await a.goto(e,{waitUntil:"networkidle0",timeout:i.renderTimeout}),await a.waitForFunction(()=>Boolean(window.litematicViewerAPI&&window.deepslateResources),{timeout:i.renderTimeout});const n=await a.evaluate(async({base64:e,width:t,height:i,quality:a,isometricSquare:n,isometricFill:r,isometricRotation:o,isometricSlant:s})=>{const c=atob(e),d=new Uint8Array(c.length);for(let e=0;e<c.length;e++)d[e]=c.charCodeAt(e);const l=new File([d],"schematic.litematic",{type:"application/octet-stream"});return await window.litematicViewerAPI.loadFromFile(l),window.litematicViewerAPI.renderIsometricViews({width:t,height:i,quality:a,isometricSquare:n,isometricFill:r,isometricRotation:o,isometricSlant:s})},{base64:t.toString("base64"),width:i.webglWidth,height:i.webglHeight,quality:i.webglQuality,isometricSquare:i.isometricSquare,isometricFill:i.isometricFill,isometricRotation:i.isometricRotation,isometricSlant:i.isometricSlant});return[{title:"isometric.png",png:decodeDataUrl(n.isometricView)},{title:"isometric-reverse.png",png:decodeDataUrl(n.reverseIsometricView)}]}finally{await a.close().catch(()=>{})}}function decodeDataUrl(e){if("string"!=typeof e||!e.startsWith("data:image/"))throw new Error("WebGL 渲染器没有返回 PNG 数据");const t=e.indexOf(",");if(t<0)throw new Error("WebGL 渲染器返回了无效图像数据");return Buffer.from(e.slice(t+1),"base64")}function resolveSendOptions(e,t){const i=t?[...e.groupSendOptions].reverse().find(e=>e.groupId.trim()===t):void 0,a="enabled"===i?.replyAndMention||"disabled"!==i?.replyAndMention&&e.replyAndMention,n=i?.sendMode??(e.sendAsForward?"forward":"combined"),r=e.qqBotType??"selfHosted";return{qqBotType:r,sendMode:n,replyAndMention:a,showViewTitles:e.showViewTitles??!1,sixFaceOverview:("official"===r||"forward"===n)&&(e.sixFaceOverview??!0)}}async function sendImages(e,t,i,a,n="schematic"){if("official"===a.qqBotType){const r=await composeQqOverview(t),o=[...a.replyAndMention&&e.messageId?[(0,koishi_1.h)("quote",{id:e.messageId})]:[],koishi_1.h.image((0,node_url_1.pathToFileURL)(r).href),(0,koishi_1.h)("text",{content:`${i}\n${n} 已渲染成功,结果如上`})];return void await e.send(o)}const r=await Promise.all(t.map(async({title:e,path:t})=>({title:e,source:`base64://${(await node_fs_1.promises.readFile(t)).toString("base64")}`}))),o=r.map(({title:t,source:i})=>(0,koishi_1.h)("message",{userId:e.selfId,nickname:"投影渲染"},[...a.showViewTitles?[(0,koishi_1.h)("text",{content:t})]:[],(0,koishi_1.h)("img",{src:i})])),s=(0,koishi_1.h)("message",{userId:e.selfId,nickname:"投影信息"},[(0,koishi_1.h)("text",{content:i})]);if("forward"===a.sendMode)return void await e.send((0,koishi_1.h)("figure",{},[...o,s]));const c=a.replyAndMention?replyElements(e,a.qqBotType):[],d=r.flatMap(({title:e,source:t},i)=>[...a.showViewTitles?[(0,koishi_1.h)("text",{content:`${i?"\n":""}${e}\n`})]:[],(0,koishi_1.h)("img",{src:t})]);await e.send([...c,...d,(0,koishi_1.h)("text",{content:`\n${i}\n${n} 已渲染成功,结果如上`})])}async function composeQqOverview(e){if(!e.length)throw new Error("没有可合并的渲染图片");const t=await Promise.all(e.map(async e=>pngjs_1.PNG.sync.read(await node_fs_1.promises.readFile((0,node_path_1.resolve)(e.path))))),i=Math.max(...t.map(e=>e.width)),a=Math.max(4,Math.round(.01*i)),n=[];let r=0;const o=t.slice(0,Math.min(2,t.length)),s=1===o.length?i:Math.floor((i-a)/2),c=o.map(e=>Math.max(1,Math.round(e.height*s/e.width))),d=Math.max(...c);for(let e=0;e<o.length;e++)n.push({image:o[e],x:e*(s+a),y:Math.floor((d-c[e])/2),width:s,height:c[e]});r+=d;for(const e of t.slice(2)){r+=a;const t=Math.max(1,Math.round(e.height*i/e.width));n.push({image:e,x:0,y:r,width:i,height:t}),r+=t}const l=new pngjs_1.PNG({width:i,height:r});for(const e of n)drawScaledPng(l,e);const m=(0,node_path_1.resolve)((0,node_path_1.dirname)(e[0].path),"qq-overview.png");return await node_fs_1.promises.writeFile(m,pngjs_1.PNG.sync.write(l)),m}function drawScaledPng(e,t){const{image:i,x:a,y:n,width:r,height:o}=t;for(let t=0;t<o;t++){const s=Math.min(i.height-1,Math.floor((t+.5)*i.height/o));for(let o=0;o<r;o++){const c=Math.min(i.width-1,Math.floor((o+.5)*i.width/r)),d=4*(s*i.width+c),l=4*((n+t)*e.width+a+o);i.data.copy(e.data,l,d,d+4)}}}function replyElements(e,t=("qq"===e.platform?"official":"selfHosted")){const i=[];return e.messageId&&i.push((0,koishi_1.h)("quote",{id:e.messageId})),"selfHosted"===t&&e.userId&&(i.push((0,koishi_1.h)("at",{id:e.userId})),i.push((0,koishi_1.h)("text",{content:"\n"}))),i}class NbtReader{data;offset=0;constructor(e){this.data=e}byte(){return this.data.readInt8(this.offset++)}ubyte(){return this.data.readUInt8(this.offset++)}short(){const e=this.data.readInt16BE(this.offset);return this.offset+=2,e}int(){const e=this.data.readInt32BE(this.offset);return this.offset+=4,e}float(){const e=this.data.readFloatBE(this.offset);return this.offset+=4,e}double(){const e=this.data.readDoubleBE(this.offset);return this.offset+=8,e}long(){const e=this.data.readBigInt64BE(this.offset);return this.offset+=8,e}text(){const e=this.data.readUInt16BE(this.offset);this.offset+=2;const t=this.data.toString("utf8",this.offset,this.offset+e);return this.offset+=e,t}readNamedRoot(){if(10!==this.ubyte())throw new Error("不是 NBT Compound 数据");return this.text(),this.value(10)}value(e){switch(e){case 0:return null;case 1:return this.byte();case 2:return this.short();case 3:return this.int();case 4:return this.long();case 5:return this.float();case 6:return this.double();case 7:{const e=this.int(),t=this.data.subarray(this.offset,this.offset+e);return this.offset+=e,t}case 8:return this.text();case 9:{const e=this.ubyte(),t=this.int();return Array.from({length:t},()=>this.value(e))}case 10:{const e={};for(;;){const t=this.ubyte();if(!t)return e;e[this.text()]=this.value(t)}}case 11:{const e=this.int();return Array.from({length:e},()=>this.int())}case 12:{const e=this.int();return Array.from({length:e},()=>this.long())}default:throw new Error(`不支持的 NBT 标签类型:${e}`)}}}const MINECRAFT_DATA_VERSIONS=new Map([[4903,"26.2"],[4790,"26.1.2"],[4671,"1.21.11"],[4557,"1.21.10"],[4555,"1.21.9"],[4440,"1.21.8"],[4438,"1.21.7"],[4435,"1.21.6"],[4325,"1.21.5"],[4189,"1.21.4"],[4082,"1.21.3"],[4080,"1.21.2"],[3955,"1.21.1"],[3953,"1.21"],[3839,"1.20.6"],[3837,"1.20.5"],[3700,"1.20.4"],[3698,"1.20.3"],[3578,"1.20.2"],[3465,"1.20.1"],[3463,"1.20"],[3337,"1.19.4"],[3218,"1.19.3"],[3120,"1.19.2"],[3117,"1.19.1"],[3105,"1.19"],[2975,"1.18.2"]]);function readLitematicRoot(e){let t;try{t=(0,node_zlib_1.gunzipSync)(e)}catch{t=e}return new NbtReader(t).readNamedRoot()}function compoundValue(e){return!e||"object"!=typeof e||Array.isArray(e)||Buffer.isBuffer(e)?void 0:e}function numericValue(e){if("number"==typeof e&&Number.isFinite(e))return e;if("bigint"==typeof e){const t=Number(e);if(Number.isSafeInteger(t))return t}}function readMetadataSize(e,t){const i=compoundValue(t.EnclosingSize),a=i&&[numericValue(i.x),numericValue(i.y),numericValue(i.z)];if(a?.every(e=>null!=e))return a.map(e=>Math.abs(e));const n=compoundValue(e.Regions);if(!n)return;let r=1/0,o=1/0,s=1/0,c=-1/0,d=-1/0,l=-1/0;for(const e of Object.values(n)){const t=compoundValue(e),i=compoundValue(t?.Position),a=compoundValue(t?.Size),n=numericValue(i?.x),m=numericValue(i?.y),u=numericValue(i?.z),h=numericValue(a?.x),f=numericValue(a?.y),p=numericValue(a?.z);if([n,m,u,h,f,p].some(e=>null==e)||0===h||0===f||0===p)continue;const g=n+h-Math.sign(h),_=m+f-Math.sign(f),w=u+p-Math.sign(p);r=Math.min(r,n,g),o=Math.min(o,m,_),s=Math.min(s,u,w),c=Math.max(c,n,g),d=Math.max(d,m,_),l=Math.max(l,u,w)}return Number.isFinite(r)?[c-r+1,d-o+1,l-s+1]:void 0}function formatCreatedAt(e){if(null==e||e<=0)return"未知";const t=new Date(e<1e10?1e3*e:e);if(Number.isNaN(t.getTime()))return"未知";const i=new Intl.DateTimeFormat("zh-CN",{timeZone:"Asia/Shanghai",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hourCycle:"h23"}).formatToParts(t),a=Object.fromEntries(i.map(e=>[e.type,e.value]));return`${a.year}-${a.month}-${a.day} ${a.hour}:${a.minute}:${a.second}`}function parseLitematicMetadata(e){const t=readLitematicRoot(e),i=compoundValue(t.Metadata)??{},a=numericValue(t.MinecraftDataVersion);return{author:"string"==typeof i.Author&&i.Author.trim()?i.Author.trim():"未知",createdAt:formatCreatedAt(numericValue(i.TimeCreated)),totalBlocks:numericValue(i.TotalBlocks),totalVolume:numericValue(i.TotalVolume),size:readMetadataSize(t,i),litematicVersion:numericValue(t.Version),minecraftDataVersion:a,minecraftVersion:null==a?void 0:MINECRAFT_DATA_VERSIONS.get(a)}}function validateProjectionLimits(e,t,i){const a=Number.isSafeInteger(i)&&i>0?i:5e6,n=()=>{throw new Error(`投影方块数或区域体积超过 ${a.toLocaleString("en-US")} 限制`)};(Math.abs(t.totalBlocks??0)>a||Math.abs(t.totalVolume??0)>a)&&n();const r=compoundValue(readLitematicRoot(e).Regions);if(!r)return;let o=0;for(const e of Object.values(r)){const t=compoundValue(compoundValue(e)?.Size),i=[numericValue(t?.x),numericValue(t?.y),numericValue(t?.z)];if(i.some(e=>null==e))continue;const r=i.reduce((e,t)=>e*Math.abs(t),1);(!Number.isSafeInteger(r)||r>a-o)&&n(),o+=r}}function formatLitematicMetadata(e,t){const i=null==e.totalBlocks?"未知":String(e.totalBlocks),a=null==e.totalVolume?"未知":String(e.totalVolume),n=e.size?.join(" × ")??"未知",r=null==e.litematicVersion?"未知":String(e.litematicVersion),o=e.minecraftVersion??"未知",s=null==e.minecraftDataVersion?"":`(数据版本:${e.minecraftDataVersion})`,c="string"==typeof t&&t.trim()?(0,node_path_1.basename)(t).replace(/\.litematic$/i,""):"";return[...c?[`投影名称:${c}`]:[],`保存者游戏 ID:${e.author}`,`创建时间:${e.createdAt}`,`方块数/体积:${i}/${a}`,`尺寸:${n}`,`Litematic 版本:${r}`,`游戏版本:${o}${s}`].join("\n")}function parseLitematic(e){const t=readLitematicRoot(e).Regions;if(!t||"object"!=typeof t)throw new Error("文件不含 Litematica Regions 数据");const i=[];for(const e of Object.values(t))parseRegion(e,i);if(!i.length)throw new Error("投影中没有非空气方块");return i}function parseRegion(e,t){const i=e.Position,a=e.Size,n=e.BlockStatePalette,r=e.BlockStates;if(!(i&&a&&Array.isArray(n)&&Array.isArray(r)))return;const o=Math.abs(a.x),s=Math.abs(a.y),c=Math.abs(a.z),d=o*s*c;if(!d)return;const l=Math.max(2,Math.ceil(Math.log2(n.length))),m=a.x<0?-1:1,u=a.y<0?-1:1,h=a.z<0?-1:1;for(let e=0;e<d;e++){const a=unpackState(r,e,l),s=n[a]?.Name;if(!s||"minecraft:air"===s||"minecraft:cave_air"===s||"minecraft:void_air"===s)continue;const d=e%o,f=Math.floor(e/o)%c,p=Math.floor(e/(o*c));t.push({x:i.x+d*m,y:i.y+p*u,z:i.z+f*h,name:s})}}function unpackState(e,t,i){const a=BigInt(t*i),n=Number(a/64n),r=a%64n;if(n>=e.length)return 0;const o=(1n<<BigInt(i))-1n;let s=BigInt.asUintN(64,e[n])>>r&o;return r+BigInt(i)>64n&&n+1<e.length&&(s|=BigInt.asUintN(64,e[n+1])<<64n-r&o),Number(s)}function renderSchematic(e,t){const i=getBounds(e);return[{title:"isometric.png",png:renderIsometric(e,i,t,!1)},{title:"isometric-reverse.png",png:renderIsometric(e,i,t,!0)}]}function getBounds(e){return e.reduce((e,t)=>({minX:Math.min(e.minX,t.x),minY:Math.min(e.minY,t.y),minZ:Math.min(e.minZ,t.z),maxX:Math.max(e.maxX,t.x),maxY:Math.max(e.maxY,t.y),maxZ:Math.max(e.maxZ,t.z)}),{minX:1/0,minY:1/0,minZ:1/0,maxX:-1/0,maxY:-1/0,maxZ:-1/0})}const SIX_FACE_ORDER=["up","down","east","south","west","north"],SIX_FACE_LABELS={up:["00000100000","00000100000","00000100000","00000100000","00000111110","00000100000","00000100000","00000100000","00000100000","00000100000","11111111111"],down:["00000000000","11111111111","00000100000","00000100000","00000110000","00000101000","00000100100","00000100100","00000100000","00000100000","00000100000"],east:["00000100000","00000100000","01111111111","00001010000","00010010000","00111111110","00000010000","00010010100","00100010010","01000010001","00001110000"],south:["00000100000","11111111111","00000100000","01111111110","01010001010","01001010010","01111111110","01000100010","01111111110","01000100010","01000100110"],west:["00000000000","11111111111","00001010000","01111111110","01001010010","01001010010","01010010010","01100001110","01000000010","01111111110","01000000010"],north:["00010010000","00010010000","00010010000","11110010011","00010011100","00010010000","00010010000","00110010000","01010010001","10010010001","00010001111"]};function renderSixFaceOverview(e,t,i=t.sixFaceLayout??"horizontal"){if(!e.length)throw new Error("投影中没有可生成六面图的方块");const a=Math.max(128,Math.floor(t.outputSize)),n=Math.max(4,Math.round(.01*a)),r="vertical"===i?2:3,o="vertical"===i?3:2,s=Math.floor((a-n*("vertical"===i?o+1:r+1))/("vertical"===i?o:r)),c=new Raster("vertical"===i?r*s+n*(r+1):a,"vertical"===i?a:o*s+n*(o+1),t),d=getBounds(e);for(const[t,i]of SIX_FACE_ORDER.entries()){const a=n+t%r*(s+n),o=n+Math.floor(t/r)*(s+n);drawOrthographicFace(c,e,d,i,a,o,s,n),drawFaceLabel(c,i,a,o,s,n)}return c.png()}function drawOrthographicFace(e,t,i,a,n,r,o,s){const c="east"===a||"west"===a?i.maxZ-i.minZ+1:i.maxX-i.minX+1,d="up"===a||"down"===a?i.maxZ-i.minZ+1:i.maxY-i.minY+1,l=new Map;for(const e of t){const t=orthographicCoordinates(e,i,a),n=`${t.a},${t.b}`,r=l.get(n);(!r||t.depth>r.depth)&&l.set(n,{block:e,...t})}const m=Math.max(2,Math.floor(s/2)),u=Math.max(1,o-2*m),h=Math.min(u/c,u/d),f=n+m+(u-c*h)/2,p=r+m+(u-d*h)/2;for(const{block:t,a:i,b:a}of[...l.values()].sort((e,t)=>e.b-t.b||e.a-t.a)){const n=Math.floor(f+i*h),r=Math.floor(p+a*h),o=Math.max(n+1,Math.ceil(f+(i+1)*h)),s=Math.max(r+1,Math.ceil(p+(a+1)*h));e.rect(n,r,o-n,s-r,colorFor(t.name))}}function orthographicCoordinates(e,t,i){switch(i){case"up":return{a:e.x-t.minX,b:e.z-t.minZ,depth:e.y};case"down":return{a:t.maxX-e.x,b:e.z-t.minZ,depth:-e.y};case"east":return{a:t.maxZ-e.z,b:t.maxY-e.y,depth:e.x};case"south":return{a:e.x-t.minX,b:t.maxY-e.y,depth:e.z};case"west":return{a:e.z-t.minZ,b:t.maxY-e.y,depth:-e.x};case"north":return{a:t.maxX-e.x,b:t.maxY-e.y,depth:-e.z}}}function drawFaceLabel(e,t,i,a,n,r){const o=SIX_FACE_LABELS[t],s=Math.max(1,Math.min(6,Math.floor(n/80))),c=Math.max(1,Math.floor(s/2)),d=i+Math.max(2,Math.floor(r/2)),l=a+Math.max(2,Math.floor(r/2));e.rect(d,l,o[0].length*s+2*c,o.length*s+2*c,"#000000");for(let t=0;t<o.length;t++)for(let i=0;i<o[t].length;i++)"1"===o[t][i]&&e.rect(d+c+i*s,l+c+t*s,s,s,"#ffffff")}function renderIsometric(e,t,i,a){const n=i.isometricCellSize,r=Math.ceil(n/2),o=(t.maxX-t.minX+t.maxZ-t.minZ+2)*n+4,s=(t.maxX-t.minX+t.maxZ-t.minZ+2)*r+(t.maxY-t.minY+2)*n+4,c=Math.max(1,Math.min(1,i.outputSize/Math.max(o,s))),d=new Raster(Math.ceil(o*c),Math.ceil(s*c),i),l=e=>({x:a?t.maxX-e.x:e.x-t.minX,z:a?t.maxZ-e.z:e.z-t.minZ}),m=[...e].sort((e,t)=>{const i=l(e),a=l(t);return i.x+i.z+e.y-(a.x+a.z+t.y)});for(const e of m){const i=l(e),a=(i.x-i.z)*n+o/2,s=(i.x+i.z)*r+(t.maxY-e.y+1)*n;d.voxel(Math.round(a*c),Math.round(s*c),Math.max(2,Math.round(n*c)),colorFor(e.name))}return d.png()}const COLORS={stone:"#777777",cobblestone:"#707070",deepslate:"#4b4b50",dirt:"#855b37",grass_block:"#63913d",sand:"#dbca85",glass:"#a7d9df",water:"#3f76e4",lava:"#e05b22",oak_planks:"#b9915a",spruce_planks:"#765033",birch_planks:"#d9c485",iron_block:"#d7d7d7",gold_block:"#f6c543",diamond_block:"#5ed6c7",redstone_block:"#b21d1d",netherrack:"#823332",quartz_block:"#e9e4dc",obsidian:"#211d2d",white_wool:"#e9e9e9",black_wool:"#1d1d1d",red_wool:"#a12722",blue_wool:"#3549a7",green_wool:"#4c702c",yellow_wool:"#e5c440"};function colorFor(e){const t=e.replace(/^minecraft:/,"");if(COLORS[t])return COLORS[t];for(const[e,i]of Object.entries(COLORS))if(t.includes(e))return i;const i=(0,node_crypto_1.createHash)("md5").update(t).digest();return`#${(90+i[0]%120).toString(16).padStart(2,"0")}${(90+i[1]%120).toString(16).padStart(2,"0")}${(90+i[2]%120).toString(16).padStart(2,"0")}`}class Raster{width;height;data;background;constructor(e,t,i){this.width=e,this.height=t,this.background=parseColor(i.background,i.transparentBackground?0:255),this.data=Buffer.alloc(e*t*4);for(let e=0;e<this.data.length;e+=4)this.data.set(this.background,e)}rect(e,t,i,a,n){for(let r=Math.max(0,t);r<Math.min(this.height,t+a);r++)for(let t=Math.max(0,e);t<Math.min(this.width,e+i);t++)this.pixel(t,r,n)}pixel(e,t,i){e<0||t<0||e>=this.width||t>=this.height||this.data.set(parseColor(i),4*(t*this.width+e))}voxel(e,t,i,a){const n=adjust(a,1.15),r=adjust(a,.78),o=adjust(a,.6),s=Math.ceil(i/2);for(let a=0;a<s;a++)for(let r=-i+a;r<=i-a;r++)this.pixel(e+r,t-s+a,n);for(let a=0;a<i;a++)for(let n=-i+Math.floor(a/2);n<=0;n++)this.pixel(e+n,t+a,r);for(let a=0;a<i;a++)for(let n=0;n<=i-Math.floor(a/2);n++)this.pixel(e+n,t+a,o)}png(){const e=Buffer.alloc((4*this.width+1)*this.height);for(let t=0;t<this.height;t++)e[t*(4*this.width+1)]=0,this.data.copy(e,t*(4*this.width+1)+1,t*this.width*4,(t+1)*this.width*4);const t=Buffer.alloc(13);return t.writeUInt32BE(this.width,0),t.writeUInt32BE(this.height,4),t.set([8,6,0,0,0],8),Buffer.concat([Buffer.from("‰PNG\r\n\n","binary"),chunk("IHDR",t),chunk("IDAT",(0,node_zlib_1.deflateSync)(e)),chunk("IEND",Buffer.alloc(0))])}}function parseColor(e,t=255){const i=/^#?([0-9a-f]{6})$/i.exec(e)?.[1]??"182026";return[parseInt(i.slice(0,2),16),parseInt(i.slice(2,4),16),parseInt(i.slice(4,6),16),t]}function adjust(e,t){const[i,a,n]=parseColor(e);return`#${[i,a,n].map(e=>Math.round(Math.min(255,e*t)).toString(16).padStart(2,"0")).join("")}`}function chunk(e,t){const i=Buffer.from(e),a=Buffer.alloc(4);a.writeUInt32BE(t.length);const n=Buffer.alloc(4);return n.writeUInt32BE(crc32(Buffer.concat([i,t]))>>>0),Buffer.concat([a,i,t,n])}const CRC_TABLE=Array.from({length:256},(e,t)=>{let i=t;for(let e=0;e<8;e++)i=1&i?3988292384^i>>>1:i>>>1;return i>>>0});function crc32(e){let t=4294967295;for(const i of e)t=CRC_TABLE[255&(t^i)]^t>>>8;return(4294967295^t)>>>0}
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.Config=exports.inject=exports.name=void 0,exports.resolveOfficialProxyUrl=resolveOfficialProxyUrl,exports.buildSshProxyArguments=buildSshProxyArguments,exports.reloadOfficialQqAdapter=reloadOfficialQqAdapter,exports.apply=apply,exports.saveUploadedResourcePack=saveUploadedResourcePack,exports.hashRenderConfiguration=hashRenderConfiguration,exports.patchOneBotAdapterSource=patchOneBotAdapterSource,exports.cacheNameSegment=cacheNameSegment,exports.enforceCacheLimit=enforceCacheLimit,exports.canRenderInSession=canRenderInSession,exports.isGroupAllowed=isGroupAllowed,exports.findLitematicFile=findLitematicFile,exports.supportsStandaloneJavaVersion=supportsStandaloneJavaVersion,exports.standaloneJvmOptions=standaloneJvmOptions,exports.isJavaMemoryFailure=isJavaMemoryFailure,exports.createGpuRenderRequest=createGpuRenderRequest,exports.mergeRenderDiagnostics=mergeRenderDiagnostics,exports.formatRenderError=formatRenderError,exports.resolveSendOptions=resolveSendOptions,exports.sendImages=sendImages,exports.composeQqOverview=composeQqOverview,exports.replyElements=replyElements,exports.parseLitematicMetadata=parseLitematicMetadata,exports.validateProjectionLimits=validateProjectionLimits,exports.formatLitematicMetadata=formatLitematicMetadata,exports.parseLitematic=parseLitematic,exports.renderSchematic=renderSchematic,exports.renderSixFaceOverview=renderSixFaceOverview;const koishi_1=require("koishi"),node_crypto_1=require("node:crypto"),node_fs_1=require("node:fs"),node_path_1=require("node:path"),node_url_1=require("node:url"),node_child_process_1=require("node:child_process"),node_net_1=require("node:net"),node_zlib_1=require("node:zlib"),pngjs_1=require("pngjs"),ssh2_1=require("ssh2"),gpu_agent_protocol_1=require("./gpu-agent-protocol"),Sentry=require("@sentry/node-core/light");exports.name="litematic-renderer",exports.inject={optional:["puppeteer","server","console"]};const CACHE_FORMAT_VERSION=15,RESOURCE_PACK_UPLOAD_LIMIT=268435456,packageVersion=require("../package.json").version,PLUGIN_VERSION="string"==typeof packageVersion?packageVersion:"unknown";exports.Config=koishi_1.Schema.intersect([koishi_1.Schema.object({qqBotType:koishi_1.Schema.union([koishi_1.Schema.const("official").description("QQ 官方机器人"),koishi_1.Schema.const("selfHosted").description("自建 QQ(OneBot / NapCat)")]).role("radio").default("selfHosted").description("机器人接入类型;决定图片、合并转发、引用和 @ 的发送方式"),patchOneBotGroupUpload:koishi_1.Schema.boolean().default(!0).description("自建 QQ:启动时修复 OneBot 群文件上传事件;官方机器人不使用此项"),officialProxyMode:koishi_1.Schema.union([koishi_1.Schema.const("disabled").description("关闭中转"),koishi_1.Schema.const("proxy").description("使用已有代理"),koishi_1.Schema.const("ssh").description("自动建立 SSH 中转")]).role("radio").default("disabled").description("实验性:仅官方 QQ 使用固定公网出口;默认关闭,不改变现有网络行为").experimental(),officialProxyUrl:koishi_1.Schema.string().default("").description("实验性:已有 socks5h、socks5、http 或 https 代理地址").experimental(),sshProxyExecutable:koishi_1.Schema.string().default("ssh").description("实验性:密钥模式使用的 SSH 可执行文件").experimental(),sshProxyHost:koishi_1.Schema.string().default("").description("实验性:SSH 中转服务器的数字 IP,例如 203.0.113.10").experimental(),sshProxyPort:koishi_1.Schema.natural().min(1).max(65535).default(22).description("实验性:SSH 中转服务器端口").experimental(),sshProxyUser:koishi_1.Schema.string().default("root").description("实验性:SSH 登录用户名").experimental(),sshProxyPrivateKey:koishi_1.Schema.path({filters:["file"]}).default("").description("实验性:SSH 私钥路径;填写后优先使用密钥登录").experimental(),sshProxyPassword:koishi_1.Schema.string().role("secret").default("").description("实验性:SSH 登录密码;不会进入命令行或日志").experimental(),sshProxyLocalPort:koishi_1.Schema.natural().min(1).max(65535).default(1080).description("实验性:仅监听 127.0.0.1 的 SOCKS5 端口").experimental(),officialGatewayWatchdog:koishi_1.Schema.boolean().default(!1).description("实验性:8 分钟无入站消息时热重载 QQ 适配器;默认关闭").experimental()}).description("机器人接入"),koishi_1.Schema.object({renderEngine:koishi_1.Schema.union([koishi_1.Schema.const("standalone"),koishi_1.Schema.const("gpuAgent").description("独立 GPU Agent v2(主动连接)"),koishi_1.Schema.const("java"),koishi_1.Schema.const("webgl"),koishi_1.Schema.const("cpu")]).default("standalone").description("渲染引擎;保留本地全部引擎,并新增主动连接的 GPU Agent v2"),maxFileSize:koishi_1.Schema.natural().min(1).default(1024).description("群聊自动处理的最大文件大小(KB)"),privateMaxFileSize:koishi_1.Schema.natural().min(1).default(1024).description("私聊自动处理的独立文件大小(KB);不改变群聊上限"),maxBlocks:koishi_1.Schema.natural().min(1).max(2e9).default(5e6).description("投影区域体积及声明方块数上限;用于拒绝尺寸异常或资源消耗过大的投影"),outputSize:koishi_1.Schema.natural().min(128).max(4096).default(1024).description("六面图和 CPU 渲染最长边的最大像素数"),background:koishi_1.Schema.string().default("#000000").description("PNG 背景颜色"),transparentBackground:koishi_1.Schema.boolean().default(!1).description("输出透明背景"),isometricFill:koishi_1.Schema.percent().default(.78).description("正二轴测主体在画布中的最大占比"),isometricRotation:koishi_1.Schema.number().min(0).max(360).step(1).default(135).description("Isometric Renders 绕竖直轴的基准旋转角"),isometricSlant:koishi_1.Schema.number().min(-90).max(90).step(1).default(36).description("Isometric Renders 俯仰角;36° 为正二轴测预设")}).description("渲染设置"),koishi_1.Schema.object({allowPrivateRender:koishi_1.Schema.boolean().default(!1).description("允许私聊附件及私聊命令触发渲染;默认关闭以保持现有行为"),sendAsForward:koishi_1.Schema.boolean().default(!1).description("仅自建 QQ:开启为合并转发,关闭为联合发送;官方 QQ 忽略此项"),showViewTitles:koishi_1.Schema.boolean().default(!1).description("仅自建 QQ:发送图片时显示视图标题"),replyAndMention:koishi_1.Schema.boolean().default(!1).description("自建 QQ 会引用并 @ 发送者;官方 QQ 仅引用,避免显示 OpenID"),sixFaceOverview:koishi_1.Schema.boolean().default(!0).description("合并转发时生成并附加上、下、东、南、西、北六面正交合成图"),sixFaceLayout:koishi_1.Schema.union([koishi_1.Schema.const("horizontal").description("横向 3×2"),koishi_1.Schema.const("vertical").description("纵向 2×3")]).default("horizontal").description("六面正交合成图布局;最长边由 outputSize 控制"),groupSendOptions:koishi_1.Schema.array(koishi_1.Schema.object({groupId:koishi_1.Schema.string().description("QQ群号"),sendMode:koishi_1.Schema.union([koishi_1.Schema.const("forward").description("合并转发"),koishi_1.Schema.const("combined").description("联合发送(一条普通消息)")]).default("forward"),replyAndMention:koishi_1.Schema.union([koishi_1.Schema.const("inherit").description("继承全局设置"),koishi_1.Schema.const("enabled").description("开启回复 @"),koishi_1.Schema.const("disabled").description("关闭回复 @")]).default("inherit")})).default([]).description("按群覆盖发送方式和回复设置;官方 QQ 只使用引用开关,自建 QQ 同时使用发送模式和 @"),groupWhitelist:koishi_1.Schema.array(koishi_1.Schema.string()).default([]).description("群白名单:非空时只有列表内群可渲染;私聊不参与判定"),groupBlacklist:koishi_1.Schema.array(koishi_1.Schema.string()).default([]).description("群黑名单:列表内群始终不渲染,优先级高于白名单")}).description("发送设置"),koishi_1.Schema.object({standaloneJavaCommand:koishi_1.Schema.string().default("").description("独立渲染器使用的 Java 可执行文件;推荐 Java 21+,留空自动查找"),minecraftJarPath:koishi_1.Schema.string().default("").description("可选的 Minecraft 客户端 JAR 或基础资源包;留空使用内置 26.2 原版资源"),resourcePackPaths:koishi_1.Schema.array(koishi_1.Schema.string()).role("table").default([]).description("自定义材质包路径;控制台可上传 ZIP,越靠后优先级越高"),standaloneRendererJar:koishi_1.Schema.string().default("").description("独立渲染器 JAR;留空使用插件内置版本"),standaloneRenderTimeout:koishi_1.Schema.natural().min(1e4).default(18e4).description("独立 Java 渲染超时(毫秒)"),standaloneJavaMaxHeapMb:koishi_1.Schema.natural().min(128).max(32768).step(8).default(200).description("首次独立渲染的最大堆内存(MiB)"),standaloneJavaRetryMaxHeapMb:koishi_1.Schema.natural().min(256).max(32768).step(128).default(2048).description("内存不足时新进程重试的最大堆内存(MiB)"),standaloneJavaMemoryRestartLimit:koishi_1.Schema.natural().max(3).default(1).description("检测到内存不足后启动全新进程重试的次数"),javaResolution:koishi_1.Schema.natural().min(256).max(4096).default(1024).description("Java 渲染最终输出边长"),javaSupersampling:koishi_1.Schema.natural().min(1).max(4).default(2).description("离屏超采样倍数")}).description("独立渲染器"),koishi_1.Schema.object({renderTimeout:koishi_1.Schema.natural().min(1e3).default(3e4).description("文件下载超时(毫秒)"),cacheDirectory:koishi_1.Schema.string().default("data/litematic-renderer-cache").description("持久缓存目录;按插件版本和投影 SHA-256 分区"),cacheMaxSizeGb:koishi_1.Schema.number().min(1).max(1024).step(1).default(20).description("所有版本缓存总上限(GiB),超出后按最久未使用清理"),diagnosticsFilePath:koishi_1.Schema.string().default("data/litematic-renderer-diagnostics.json").description("渲染诊断持久化文件路径;导出地址固定为 /litematic-renderer/diagnostics"),sentryDsn:koishi_1.Schema.string().default("").description("Sentry DSN,用于上报渲染错误;留空时读取 SENTRY_DSN 环境变量").experimental()}).description("缓存与诊断"),koishi_1.Schema.object({isometricCellSize:koishi_1.Schema.natural().min(2).max(32).default(7).description("CPU 正二轴测方块菱形半宽"),javaRenderTimeout:koishi_1.Schema.natural().min(1e4).default(18e4).description("等待 Minecraft Java 渲染桥的超时(毫秒)"),webglQuality:koishi_1.Schema.union([koishi_1.Schema.const("standard"),koishi_1.Schema.const("high"),koishi_1.Schema.const("ultra")]).default("high").description("WebGL 渲染质量"),webglWidth:koishi_1.Schema.natural().min(256).max(2048).default(800).description("WebGL 单张图宽度"),webglHeight:koishi_1.Schema.natural().min(256).max(2048).default(600).description("WebGL 单张图高度"),isometricSquare:koishi_1.Schema.boolean().default(!0).description("将 WebGL 正二轴测图输出为正方形"),javaBridgeDirectory:koishi_1.Schema.string().default("").description("renderEngine=java 时使用的 Fabric Java 渲染桥任务目录"),gpuRendererCommand:koishi_1.Schema.string().default("").description("可选外部 GPU 渲染器命令;非空时优先尝试,失败后回退到 renderEngine")}).description("高级设置"),koishi_1.Schema.object({gpuAgentEnabled:koishi_1.Schema.boolean().default(!1).description("启动 GPU Agent v2 WebSocket 服务;默认关闭,不占用监听端口"),gpuAgentListenHost:koishi_1.Schema.string().default("0.0.0.0").description("GPU Agent v2 监听地址"),gpuAgentListenPort:koishi_1.Schema.natural().min(1).max(65535).default(39180).description("GPU Agent v2 监听端口"),gpuAgentPath:koishi_1.Schema.string().default("/litematic-renderer/agent/v2").description("GPU Agent v2 WebSocket 路径"),gpuAgentNodes:koishi_1.Schema.array(koishi_1.Schema.object({agentId:koishi_1.Schema.string().description("GPU 节点唯一 ID"),sharedSecret:koishi_1.Schema.string().role("secret").description("该节点的 HMAC 共享密钥"),enabled:koishi_1.Schema.boolean().default(!0).description("允许此节点连接")})).default([]).description("允许连接的 GPU 节点;每个节点使用独立密钥"),gpuAgentTimeout:koishi_1.Schema.natural().min(1e4).max(36e5).default(24e4).description("等待 GPU Agent 完成任务的超时(毫秒)"),gpuAgentFallback:koishi_1.Schema.boolean().default(!0).description("GPU Agent 不可用或失败时回退到本地 standalone")}).description("GPU Agent v2")]);const activeStandaloneJavaProcesses=new Set;let standaloneRenderQueue=Promise.resolve();async function acquireStandaloneRenderSlot(){const e=standaloneRenderQueue;let t;return standaloneRenderQueue=new Promise(e=>{t=e}),await e.catch(()=>{}),t}const SUPPORTED_PROXY_PROTOCOLS=new Set(["http:","https:","socks:","socks5:","socks5h:"]);function resolveOfficialProxyUrl(e){if("disabled"===e.officialProxyMode)return;const t="ssh"===e.officialProxyMode?`socks5h://127.0.0.1:${e.sshProxyLocalPort}`:e.officialProxyUrl.trim();if(!t)throw new Error("已开启官方 QQ 中转,但代理地址为空");let i;try{i=new URL(t)}catch{throw new Error(`代理地址格式无效:${t}`)}if(!SUPPORTED_PROXY_PROTOCOLS.has(i.protocol))throw new Error(`不支持的代理协议:${i.protocol}`);return i.href}function buildSshProxyArguments(e){const t=e.sshProxyHost.trim(),i=e.sshProxyUser.trim(),r=e.sshProxyPrivateKey.trim();if(!t)throw new Error("SSH 中转服务器 IP 不能为空");if(!i)throw new Error("SSH 登录用户名不能为空");if(!r&&!e.sshProxyPassword)throw new Error("SSH 私钥和密码至少填写一项");const n=["-NT","-D",`127.0.0.1:${e.sshProxyLocalPort}`,"-p",String(e.sshProxyPort),"-o","BatchMode=yes","-o","ExitOnForwardFailure=yes","-o","ServerAliveInterval=30","-o","ServerAliveCountMax=3","-o","StrictHostKeyChecking=accept-new"];return r&&n.push("-i",(0,node_path_1.resolve)(r)),n.push(`${i}@${t}`),n}function isTcpPortOpen(e,t,i=1e3){return new Promise(r=>{const n=new node_net_1.Socket;let a=!1;const o=e=>{a||(a=!0,n.destroy(),r(e))};n.setTimeout(i),n.once("connect",()=>o(!0)),n.once("timeout",()=>o(!1)),n.once("error",()=>o(!1)),n.connect(t,e)})}function startPasswordSshProxyTunnel(e,t){let i,r,n,a=!1;const o=new Set,s=()=>{a||(i=new ssh2_1.Client,i.once("ready",()=>{!a&&i&&(r=(0,node_net_1.createServer)(e=>{o.add(e),e.once("close",()=>o.delete(e)),handleSocks5Connection(e,i)}),r.once("error",e=>{t.warn(`本地 SOCKS5 监听失败:${e.message}`),i?.end()}),r.listen(e.sshProxyLocalPort,"127.0.0.1",()=>{t.info(`官方 QQ 中转已就绪:socks5h://127.0.0.1:${e.sshProxyLocalPort}`)}))}),i.once("error",e=>t.warn(`SSH 密码登录失败:${e.message}`)),i.once("close",()=>{i=void 0,r?.close(),r=void 0;for(const e of o)e.destroy();a||a||n||(n=setTimeout(()=>{n=void 0,s()},5e3))}),t.info(`正在使用密码建立官方 QQ SSH 中转:${e.sshProxyUser}@${e.sshProxyHost}`),i.connect({host:e.sshProxyHost.trim(),port:e.sshProxyPort,username:e.sshProxyUser.trim(),password:e.sshProxyPassword,keepaliveInterval:3e4,keepaliveCountMax:3,readyTimeout:15e3}))};return s(),()=>{a=!0,n&&clearTimeout(n),r?.close(),i?.end();for(const e of o)e.destroy()}}function handleSocks5Connection(e,t){let i=Buffer.alloc(0),r="greeting";const n=(t=1)=>{"request"===r&&e.write(Buffer.from([5,t,0,1,0,0,0,0,0,0])),e.destroy()};e.on("data",a=>{if(i=Buffer.concat([i,a]),"greeting"===r){if(i.length<2)return;const t=2+i[1];if(i.length<t)return;if(5!==i[0]||!i.subarray(2,t).includes(0))return n();i=i.subarray(t),r="request",e.write(Buffer.from([5,0]))}if("request"!==r||i.length<4)return;if(5!==i[0]||1!==i[1])return n(7);const o=i[3];let s,c;if(1===o){if(i.length<10)return;s=[...i.subarray(4,8)].join("."),c=8}else if(3===o){const e=i[4];if(i.length<7+e)return;s=i.subarray(5,5+e).toString("utf8"),c=5+e}else{if(4!==o)return n(8);{if(i.length<22)return;const e=[];for(let t=4;t<20;t+=2)e.push(i.readUInt16BE(t).toString(16));s=e.join(":"),c=20}}const d=i.readUInt16BE(c);e.pause(),e.removeAllListeners("data"),t.forwardOut("127.0.0.1",0,s,d,(t,i)=>{if(t)return n(5);e.write(Buffer.from([5,0,0,1,0,0,0,0,0,0])),e.pipe(i).pipe(e),e.resume()})}),e.once("error",()=>e.destroy())}function startSshProxyTunnel(e,t){if(buildSshProxyArguments(e),!e.sshProxyPrivateKey.trim())return startPasswordSshProxyTunnel(e,t);const i=e.sshProxyExecutable.trim()||"ssh",r=buildSshProxyArguments(e);let n,a,o=!1;const s=()=>{o||a||(a=setTimeout(()=>{a=void 0,c()},5e3))},c=async()=>{if(o)return;if(await isTcpPortOpen("127.0.0.1",e.sshProxyLocalPort))return void t.info(`官方 QQ 中转已就绪:socks5h://127.0.0.1:${e.sshProxyLocalPort}`);try{await node_fs_1.promises.access((0,node_path_1.resolve)(e.sshProxyPrivateKey),node_fs_1.constants.R_OK)}catch{return t.warn(`SSH 私钥不可读:${(0,node_path_1.resolve)(e.sshProxyPrivateKey)}`),void s()}n=(0,node_child_process_1.spawn)(i,r,{stdio:["ignore","ignore","pipe"],windowsHide:!0});let a="";n.stderr?.on("data",e=>{a=`${a}${e.toString()}`.slice(-2e3)}),n.once("spawn",()=>t.info(`正在建立官方 QQ SSH 中转:${e.sshProxyUser}@${e.sshProxyHost}`)),n.once("error",e=>{t.warn(`SSH 中转启动失败:${e.message}`),s()}),n.once("close",e=>{if(n=void 0,o)return;const i=a.trim().split(/\r?\n/).at(-1);t.warn(`SSH 中转已断开(退出码 ${e??"unknown"})${i?`:${i}`:""}`),s()});for(let i=0;i<20&&n&&!o;i++){if(await isTcpPortOpen("127.0.0.1",e.sshProxyLocalPort,300)){t.info(`官方 QQ 中转已就绪:socks5h://127.0.0.1:${e.sshProxyLocalPort}`);break}await new Promise(e=>setTimeout(e,250))}};return c(),()=>{o=!0,a&&clearTimeout(a),n?.kill()}}async function reloadOfficialQqAdapter(e,t){try{const i=e.loader;if(!i?.entries)return!1;for(const e of i.entries.values())if(e?.config&&"secret"in e.config&&"intents"in e.config&&String(e.name??"").includes("adapter-qq"))return t.warn("官方 QQ 网关看门狗:8 分钟无消息,正在热重载适配器"),await i.reload(e),!0}catch(e){t.warn(`官方 QQ 网关看门狗热重载失败:${e instanceof Error?e.message:String(e)}`)}return!1}function apply(e,t){const i=e.logger(exports.name),r=t.gpuAgentEnabled?new gpu_agent_protocol_1.GpuAgentHub({host:t.gpuAgentListenHost,port:t.gpuAgentListenPort,path:t.gpuAgentPath.startsWith("/")?t.gpuAgentPath:`/${t.gpuAgentPath}`,nodes:t.gpuAgentNodes,heartbeatTimeout:3e4,logger:i}):void 0;r?.start(),r&&e.on("dispose",()=>{r.close()});let n=Promise.resolve();if("official"===t.qqBotType){const r=resolveOfficialProxyUrl(t);if(r){const n=e.root.on("http/config",e=>{e.proxyAgent=r}),a="ssh"===t.officialProxyMode?startSshProxyTunnel(t,i):()=>{};i.info(`官方 QQ 实验性出站代理已启用:${r}`);const o=setInterval(async()=>{try{await e.http.get("https://api.sgroup.qq.com/gateway",{timeout:1e4,responseType:"json"})}catch{}},6e4);let s=Date.now();t.officialGatewayWatchdog&&e.on("message",()=>{s=Date.now()});const c=t.officialGatewayWatchdog?setInterval(()=>{Date.now()-s<=48e4||reloadOfficialQqAdapter(e,i).then(e=>{e&&(s=Date.now())})},6e4):void 0;e.on("dispose",()=>{clearInterval(o),c&&clearInterval(c),n(),a()})}}e.inject(["console"],e=>{const t=(0,node_path_1.resolve)(__dirname,"..");e.console.addEntry({dev:(0,node_path_1.join)(t,"client/index.ts"),prod:(0,node_path_1.join)(t,"dist")}),e.console.addListener("litematic/resource-pack-upload",async(t,i)=>saveUploadedResourcePack(t,i,(0,node_path_1.resolve)(e.baseDir,"data/litematic-resource-packs")),{authority:4})});const a=1024*t.maxFileSize,o=t.sentryDsn||process.env.SENTRY_DSN||"";o&&Sentry.init({dsn:o,tracesSampleRate:1});const s=(e,i)=>{o&&Sentry.withScope(r=>{r.setFingerprint([i]),r.setTag("entrypoint",i),r.setContext("render",{engine:t.renderEngine}),Sentry.captureException(e)})};e.on("dispose",()=>terminateActiveStandaloneJavaProcesses(i)),"selfHosted"===t.qqBotType&&ensureOneBotGroupUploadCompatibility(i,t.patchOneBotGroupUpload);const c=(0,node_path_1.resolve)(stringOrDefault(t.diagnosticsFilePath,"data/litematic-renderer-diagnostics.json")),d=(0,node_path_1.join)((0,node_path_1.dirname)(c),"litematic-renderer-diagnostics-archive"),l=(0,node_path_1.join)((0,node_path_1.dirname)(c),"litematic-renderer-diagnostics-disabled");rotateDiagnosticsFile(c,d,i);const u=(0,node_path_1.resolve)(stringOrDefault(t.cacheDirectory,"data/litematic-renderer-cache")),m=(0,node_path_1.join)(u,`v${cachePathSegment(PLUGIN_VERSION)}`),h=Math.floor(t.cacheMaxSizeGb*1024**3),p=new Map;node_fs_1.promises.mkdir(m,{recursive:!0}).then(()=>enforceCacheLimit(u,h)).catch(e=>i.warn(`缓存初始化失败:${e instanceof Error?e.message:String(e)}`));const f=async(o,s="schematic.litematic",d,l=!1,f,g=a)=>{const _=d??await download(e,o,g,t.renderTimeout),w=parseLitematicMetadata(_);validateProjectionLimits(_,w,t.maxBlocks);const S=formatLitematicMetadata(w,s),y=projectionNameFromFilename(s),v=(0,node_crypto_1.createHash)("sha256").update(_).digest("hex"),x="standalone"===t.renderEngine||"gpuAgent"===t.renderEngine&&t.gpuAgentFallback,b=x?await resolveMinecraftResources(t):"",k=x?await fingerprintFiles([b,...resourcePackPaths(t.resourcePackPaths)]):[],P=hashRenderConfiguration({version:CACHE_FORMAT_VERSION,outputSize:t.outputSize,isometricCellSize:t.isometricCellSize,background:t.background,transparentBackground:t.transparentBackground,gpuRendererCommand:t.gpuRendererCommand,renderEngine:t.renderEngine,standaloneRendererJar:t.standaloneRendererJar,resourceFingerprint:k,javaResolution:t.javaResolution,javaSupersampling:t.javaSupersampling,webglQuality:t.webglQuality,webglWidth:t.webglWidth,webglHeight:t.webglHeight,isometricSquare:t.isometricSquare,isometricFill:t.isometricFill,isometricRotation:t.isometricRotation,isometricSlant:t.isometricSlant,sixFaceLayout:t.sixFaceLayout,gpuAgentFingerprint:"gpuAgent"===t.renderEngine?r?.capabilityFingerprint()??"disabled":void 0}),M=cacheNameSegment(s),E=(0,node_path_1.join)(m,`${M}-${v}-${P}`),R=(0,node_path_1.join)(E,"projection.litematic");await node_fs_1.promises.mkdir(E,{recursive:!0}),await exists(R)||await node_fs_1.promises.writeFile(R,_),await ensureCacheMetadata(E,{pluginVersion:PLUGIN_VERSION,cacheFormatVersion:CACHE_FORMAT_VERSION,contentSha256:v,renderConfigSha256:P,sourceFilename:(0,node_path_1.basename)(s),createdAt:(new Date).toISOString()});const I=["isometric.png","isometric-reverse.png"].map(e=>(0,node_path_1.join)(E,e)),$=async()=>{const e=await Promise.all(I.map(exists));return e.every(Boolean)||"gpuAgent"===t.renderEngine&&e[0]};if(await $())i.debug(`命中投影缓存:${v}`);else{let a=p.get(E);a?i.debug(`等待相同投影的缓存任务:${v}`):(a=(async()=>{try{if(await $())return;if(!Boolean(t.gpuRendererCommand&&await renderWithGpu(t.gpuRendererCommand,R,E,t.renderTimeout,i))||!(await Promise.all(I.map(exists))).every(Boolean))if("standalone"===t.renderEngine)await renderWithStandalone(R,E,b,t,i),await mergeRenderDiagnostics(E,c,i);else if("gpuAgent"===t.renderEngine)try{if(!r)throw new Error("GPU Agent v2 服务未启用");const e=await(e=>{const t=n.then(e,e);return n=t.catch(()=>{}),t})(()=>r.render(createGpuRenderRequest(s,t,f),_,t.gpuAgentTimeout));for(const t of e.images){if("merged"===t.id||"merged.png"===t.name){await node_fs_1.promises.writeFile(I[0],t.png),await node_fs_1.promises.rm(I[1],{force:!0});break}"isometric"===t.id||"isometric.png"===t.name?await node_fs_1.promises.writeFile(I[0],t.png):"isometric-reverse"!==t.id&&"isometric-reverse.png"!==t.name||await node_fs_1.promises.writeFile(I[1],t.png)}}catch(e){if(!t.gpuAgentFallback)throw e;i.warn(`GPU Agent 渲染失败,回退独立 Java:${e instanceof Error?e.message:String(e)}`),await renderWithStandalone(R,E,b,t,i),await mergeRenderDiagnostics(E,c,i)}else if("java"===t.renderEngine)await renderWithJavaBridge(R,E,t);else{const r=parseLitematic(_);let n;if("webgl"===t.renderEngine&&e.puppeteer)try{n=await renderWithWebgl(e,_,t)}catch(e){i.warn(`WebGL 渲染失败,回退 CPU:${e instanceof Error?e.message:String(e)}`),n=renderSchematic(r,t)}else n=renderSchematic(r,t);await Promise.all(n.map(e=>node_fs_1.promises.writeFile((0,node_path_1.join)(E,e.title),e.png)))}if(!await $())throw new Error("渲染器没有生成两张正二轴测 PNG")}catch(e){throw await appendRenderError(E,c,t.renderEngine,e,i),e}})().finally(()=>p.delete(E)),p.set(E,a)),await a}const A=await exists(I[1])?I.map((e,t)=>({title:["正二轴测","反向正二轴测(旋转 180°)"][t],path:e})):[{title:"正二轴测",path:I[0]}];if(l){const e=(0,node_path_1.join)(E,"six-faces.png");if(!await exists(e)){const r=`${E}:six-face`;let n=p.get(r);n||(n=(async()=>{try{if("standalone"===t.renderEngine)try{await renderWithStandalone(R,E,b,t,i,"six-face")}catch(e){i.warn(`真实材质六面图生成失败,回退方块颜色投影:${e instanceof Error?e.message:String(e)}`)}if(!await exists(e)){const i=parseLitematic(_);await node_fs_1.promises.writeFile(e,renderSixFaceOverview(i,t,t.sixFaceLayout))}}catch(e){i.warn(`六面正交图生成失败,继续发送轴测图:${e instanceof Error?e.message:String(e)}`)}})().finally(()=>p.delete(r)),p.set(r,n)),await n}await exists(e)&&A.push({title:"六面正投影",path:e})}return await touchCacheEntry(E),await enforceCacheLimit(u,h,E),{images:A,metadata:S,projectionName:y}};e.middleware(async(e,r)=>{if(!canRenderInSession(e,t.allowPrivateRender))return r();const n=findLitematicFile(e);if(!n)return r();if(!isGroupAllowed(t,e.guildId))return i.info(`群 ${e.guildId} 不在渲染白名单内或已被拉黑,忽略其投影文件`),await e.send([...replyElements(e,t.qqBotType),(0,koishi_1.h)("text",{content:`本群未开启投影渲染。如需开启,请将群 ID ${e.guildId} 加入插件白名单`})]),r();const a=await resolveFileUrl(e,n);if(!a)return i.warn(`检测到 Litematic 文件但无法获取下载地址:${fileName(n)??"unknown.litematic"};请检查 OneBot/NapCat 的 get_group_file_url 接口`),await e.send([...replyElements(e,t.qqBotType),(0,koishi_1.h)("text",{content:"检测到投影文件,但无法获取群文件下载地址,请检查机器人文件接口"})]),r();const o=Boolean(e.isDirect),d=o?t.privateMaxFileSize:t.maxFileSize,l=1024*d;if(isOverLimit(n.size,l))return await appendGlobalRenderError(c,`input:${(0,node_path_1.basename)(fileName(n)??"unknown.litematic")}`,"input",`file size exceeds ${d} KB`,i),await e.send([...replyElements(e,t.qqBotType),(0,koishi_1.h)("text",{content:`文件大小超过 ${(d/1024).toFixed(2)} MB,无法渲染`})]),r();if(!isUnderLimit(n.size,l))return r();try{const i=resolveSendOptions(t,e.guildId),r=await f(a,fileName(n)??"schematic.litematic",void 0,i.sixFaceOverview,{group:e.guildId??void 0,user:e.userId??void 0},l);await sendImages(e,r.images,r.metadata,i,r.projectionName)}catch(n){return s(n,o?"private-file-upload":"group-file-upload"),i.warn(n),await e.send([...replyElements(e,t.qqBotType),(0,koishi_1.h)("text",{content:formatRenderError(n,{maxFileSize:d})})]),r()}return r()}),e.command("litematic.render <url:string>","渲染可直接下载的 .litematic 文件").action(async({session:e},i)=>{if(!i)return"请提供 .litematic 文件的直链";if(!e)return"此命令只能在消息会话中执行";if(!canRenderInSession(e,t.allowPrivateRender))return"单人对话渲染未开启,请在插件发送设置中启用";if(!isGroupAllowed(t,e.guildId))return`本群未开启投影渲染。如需开启,请将群 ID ${e.guildId} 加入插件白名单`;const r=e.isDirect?t.privateMaxFileSize:t.maxFileSize,n=1024*r;try{const r=resolveSendOptions(t,e.guildId),a=await f(i,"schematic.litematic",void 0,r.sixFaceOverview,{group:e.guildId??void 0,user:e.userId??void 0},n);await sendImages(e,a.images,a.metadata,r,a.projectionName)}catch(e){return s(e,"litematic.render"),formatRenderError(e,{maxFileSize:r})}}),e.command("litematic.cache.clear","清理投影渲染缓存",{authority:3}).action(async()=>(await node_fs_1.promises.rm(u,{recursive:!0,force:!0}),await node_fs_1.promises.mkdir(m,{recursive:!0}),"投影渲染缓存已清理")),e.command("litematic.errors.export","export render diagnostics",{authority:3}).action(async()=>`Diagnostics file: ${c}`),e.command("litematic.errors.disable","disable current render diagnostics snapshot",{authority:3}).action(async()=>disableDiagnosticsSnapshot(c,l)),e.command("litematic.proxy.check","检查 Koishi 当前公网出口 IP",{authority:3}).action(async()=>{try{const t=await e.http.get("https://ifconfig.me/ip",{responseType:"text",timeout:1e4,headers:{"User-Agent":"koishi-plugin-litematic-renderer"}});return`当前 Koishi 出口 IP:${String(t).trim()}`}catch(e){return`出口 IP 检查失败:${e instanceof Error?e.message:String(e)}`}});const g=e.server;g?.get&&g.get("/litematic-renderer/diagnostics",async e=>{e.type="application/json",e.set("Content-Disposition",'attachment; filename="litematic-renderer-diagnostics.json"'),e.body=await node_fs_1.promises.readFile(c,"utf8").catch(()=>'{"format":2,"blocks":[],"errors":[]}')})}async function saveUploadedResourcePack(e,t,i){if("string"!=typeof e||!/\.zip$/i.test(e))throw new Error("只允许上传 ZIP 材质包");if("string"!=typeof t||!t.length)throw new Error("材质包内容为空");const r=Buffer.from(t,"base64");if(!r.length)throw new Error("材质包内容为空");if(r.length>RESOURCE_PACK_UPLOAD_LIMIT)throw new Error("材质包超过 256 MB 上传上限");const n=r.readUInt32LE(0);if(![67324752,101010256,134695760].includes(n))throw new Error("文件不是有效的 ZIP 材质包");const a=(0,node_path_1.basename)(e).replace(/\.zip$/i,"").replace(/[<>:"/\\|?*\x00-\x1f]/g,"_").replace(/[. ]+$/g,"").trim()||"resource-pack",o=(0,node_crypto_1.createHash)("sha256").update(r).digest("hex").slice(0,12),s=(0,node_path_1.join)(i,`${a}-${o}.zip`);return await node_fs_1.promises.mkdir(i,{recursive:!0}),await node_fs_1.promises.writeFile(s,r),s.replace(/\\/g,"/")}function hashRenderConfiguration(e){return(0,node_crypto_1.createHash)("sha256").update(JSON.stringify(e)).digest("hex").slice(0,16)}function patchOneBotAdapterSource(e){if(/^\s*case ["']group_upload["']:/m.test(e))return{source:e,changed:!1,alreadyCompatible:!0};const t=e.match(/\(0,\s*(import_[\w$]+)\.hyphenate\)\(data\.sub_type\)/)?.[1]??"import_koishi2",i=/ \/\/ https:\/\/github\.com\/koishijs\/koishi-plugin-adapter-onebot\/issues\/33\r?\n \/\/ case 'offline_file':\r?\n \/\/ session\.elements = \[h\('file', data\.file\)\]\r?\n \/\/ session\.type = 'message'\r?\n \/\/ session\.subtype = 'private'\r?\n \/\/ session\.isDirect = true\r?\n \/\/ session\.subsubtype = 'offline-file-added'\r?\n \/\/ break\r?\n \/\/ case 'group_upload':\r?\n \/\/ session\.elements = \[h\('file', data\.file\)\]\r?\n \/\/ session\.type = 'message'\r?\n \/\/ session\.subtype = 'group'\r?\n \/\/ session\.subsubtype = 'guild-file-added'\r?\n \/\/ break/;if(!i.test(e))return{source:e,changed:!1,alreadyCompatible:!1};const r=[" // Restored by koishi-plugin-litematic-renderer so NapCat group file uploads reach middleware",' case "offline_file":',` session.elements = [(0, ${t}.h)("file", data.file)];`,' session.type = "message";',' session.subtype = "private";'," session.isDirect = true;",' session.subsubtype = "offline-file-added";'," break;",' case "group_upload":',` session.elements = [(0, ${t}.h)("file", data.file)];`,' session.type = "message";',' session.subtype = "group";',' session.subsubtype = "guild-file-added";'," break;"].join("\n");return{source:e.replace(i,r),changed:!0,alreadyCompatible:!1}}async function ensureOneBotGroupUploadCompatibility(e,t){if(!t)return;let i;try{i=require.resolve("koishi-plugin-adapter-onebot/package.json",{paths:[process.cwd(),__dirname]})}catch{return void e.debug("未安装 koishi-plugin-adapter-onebot,跳过群文件上传事件兼容修复")}try{const t=(0,node_path_1.dirname)(i),r=JSON.parse(await node_fs_1.promises.readFile(i,"utf8")),n=(0,node_path_1.join)(t,"lib","index.js"),a=await node_fs_1.promises.readFile(n,"utf8"),o=patchOneBotAdapterSource(a);if(o.alreadyCompatible)return void e.debug(`koishi-plugin-adapter-onebot@${r.version??"unknown"} 已支持 group_upload`);if(!o.changed)return void e.warn(`koishi-plugin-adapter-onebot@${r.version??"unknown"} 未识别到可自动修复的 group_upload 代码段`);const s=`${n}.litematic-renderer.bak`;await exists(s)||await node_fs_1.promises.writeFile(s,a),await node_fs_1.promises.writeFile(n,o.source),e.warn(`已修复 koishi-plugin-adapter-onebot@${r.version??"unknown"} 的群文件上传事件映射;请重启 Koishi 让已加载的 OneBot 适配器生效`)}catch(t){e.warn(`修复 OneBot 群文件上传事件失败:${t instanceof Error?t.message:String(t)}`)}}function cachePathSegment(e){return e.replace(/[^a-zA-Z0-9._-]/g,"_")||"unknown"}function cacheNameSegment(e){return((0,node_path_1.basename)(e||"schematic.litematic",(0,node_path_1.extname)(e||"")).replace(/[<>:"/\\|?*\u0000-\u001f]/g,"_").replace(/[. ]+$/g,"").trim()||"schematic").slice(0,80)}function projectionNameFromFilename(e){return(0,node_path_1.basename)(e||"schematic.litematic").replace(/\.litematic$/i,"").trim()||"schematic"}async function ensureCacheMetadata(e,t){const i=(0,node_path_1.join)(e,"cache.json");await exists(i)||await node_fs_1.promises.writeFile(i,JSON.stringify(t,null,2)+"\n")}async function touchCacheEntry(e){const t=new Date;await node_fs_1.promises.utimes(e,t,t).catch(()=>{})}async function directorySize(e){let t=0;const i=await node_fs_1.promises.readdir(e,{withFileTypes:!0}).catch(e=>{if("ENOENT"===e?.code)return[];throw e});for(const r of i){const i=(0,node_path_1.join)(e,r.name);r.isDirectory()?t+=await directorySize(i):r.isFile()&&(t+=(await node_fs_1.promises.stat(i)).size)}return t}async function enforceCacheLimit(e,t,i){const r=[],n=await node_fs_1.promises.readdir(e,{withFileTypes:!0}).catch(e=>{if("ENOENT"===e?.code)return[];throw e});for(const t of n){if(!t.isDirectory())continue;const i=(0,node_path_1.join)(e,t.name),n=await node_fs_1.promises.readdir(i,{withFileTypes:!0});for(const e of n){if(!e.isDirectory())continue;const t=(0,node_path_1.join)(i,e.name),[n,a]=await Promise.all([directorySize(t),node_fs_1.promises.stat(t)]);r.push({path:t,size:n,lastUsed:a.mtimeMs})}}let a=r.reduce((e,t)=>e+t.size,0),o=0,s=0;const c=i&&(0,node_path_1.resolve)(i);r.sort((e,t)=>e.lastUsed-t.lastUsed);for(const e of r){if(a<=t)break;c&&(0,node_path_1.resolve)(e.path)===c||(await node_fs_1.promises.rm(e.path,{recursive:!0,force:!0}),a-=e.size,o+=e.size,s++)}return{totalBytes:a,removedBytes:o,removedEntries:s}}function fileElements(e){return[...e.elements??[],...koishi_1.h.parse(e.content??"")]}function canRenderInSession(e,t){return Boolean(e.guildId)||Boolean(e.isDirect)&&t}function isGroupAllowed(e,t){if(!t)return!0;const i=t.trim();return!i||!e.groupBlacklist.some(e=>e.trim()===i)&&!(e.groupWhitelist.length>0&&!e.groupWhitelist.some(e=>e.trim()===i))}function findLitematicFile(e){const t=fileElements(e).filter(e=>"file"===e.type),i=t.find(e=>".litematic"===(0,node_path_1.extname)(fileName(e.attrs)??"").toLowerCase());if(i?.attrs)return i.attrs;for(const t of[e.event,e._data,e.data,e.message]){const e=findLitematicInFilePayload(t);if(e)return e}const r=inferLitematicName(e.content);return r&&t[0]?.attrs?{...t[0].attrs,name:r}:void 0}function findLitematicInFilePayload(e,t=new Set,i=0){if(!e||"object"!=typeof e||i>6||t.has(e))return;t.add(e);const r=e,n=r,a=n.file&&"object"==typeof n.file?n.file:void 0;if(a&&fileName(a)&&".litematic"===(0,node_path_1.extname)(fileName(a)).toLowerCase())return{...a,id:a.id??n.id,file_id:a.file_id??n.file_id,fileId:a.fileId??n.fileId,busid:a.busid??n.busid};if(fileName(n)&&".litematic"===(0,node_path_1.extname)(fileName(n)).toLowerCase())return n;for(const[e,n]of Object.entries(r))if(n&&"object"==typeof n&&/file|upload|message|data|event/i.test(e)){const e=findLitematicInFilePayload(n,t,i+1);if(e)return e}}function inferLitematicName(e){if(!e)return;const t=e.match(/(?:file|文件)\s+(.+?\.litematic)(?=$|[\]】\r\n])/i);return t?.[1]?t[1].trim():e.match(/([^\s\]】<>"']+\.litematic)(?=$|[\s\]】])/i)?.[1]?.trim()}function fileName(e){const t=e.file&&"object"==typeof e.file?e.file:void 0,i=e.name??e.filename??e.file_name??e.fileName??t?.name??t?.filename??t?.file_name??t?.fileName;return"string"==typeof i&&i.trim()?i.trim():void 0}async function resolveFileUrl(e,t){const i=t.file&&"object"==typeof t.file?t.file:void 0,r=t.url??t.src??("string"==typeof t.file?t.file:void 0)??i?.url??i?.src;if("string"==typeof r&&r)return r;const n=t.id??t.file_id??t.fileId??i?.id??i?.file_id??i?.fileId,a=e.bot.internal;if(!e.guildId||!n||"function"!=typeof a?.getGroupFileUrl)return;const o=await a.getGroupFileUrl(e.guildId,n,Number(t.busid??i?.busid??0));return"string"==typeof o?o:o?.url}function isUnderLimit(e,t){if(null==e||""===e)return!0;const i=Number(e);return Number.isFinite(i)&&i>0&&i<=t}async function download(e,t,i,r){const n=await e.http.get(t,{responseType:"arraybuffer",timeout:r}),a=Buffer.from(n);if(a.byteLength>i)throw new Error(`文件超过 ${(i/1024/1024).toFixed(2)} MB 限制`);return a}async function exists(e){try{return await node_fs_1.promises.access(e,node_fs_1.constants.R_OK),!0}catch{return!1}}function stringOrDefault(e,t){return"string"==typeof e&&e.trim()?e:t}function resourcePackPaths(e){return Array.isArray(e)?e.filter(e=>"string"==typeof e&&Boolean(e.trim())):[]}async function fingerprintFiles(e){return Promise.all(e.filter(e=>"string"==typeof e&&Boolean(e.trim())).map(async e=>{const t=(0,node_path_1.resolve)(e);try{const e=await node_fs_1.promises.stat(t);return{path:t,size:e.size,modified:e.mtimeMs}}catch{return{path:t,missing:!0}}}))}async function resolveStandaloneJavaCommand(e){const t=e.trim();if(t)return t;const i="win32"===process.platform?"java.exe":"java",r=new Set;if(process.env.JAVA_HOME&&r.add((0,node_path_1.join)(process.env.JAVA_HOME,"bin",i)),"win32"===process.platform){const e=new Set([(0,node_path_1.join)(process.env.ProgramFiles??"C:/Program Files","Java"),(0,node_path_1.join)(process.env.ProgramFiles??"C:/Program Files","Eclipse Adoptium"),(0,node_path_1.join)(process.env.ProgramFiles??"C:/Program Files","Microsoft","jdk")]);for(const t of e)try{const e=await node_fs_1.promises.readdir(t,{withFileTypes:!0});for(const n of e)n.isDirectory()&&/(?:^|[-_])21(?:[._-]|$)/.test(n.name)&&r.add((0,node_path_1.join)(t,n.name,"bin",i))}catch{}}r.add("java");for(const e of r)if(supportsStandaloneJavaVersion(await javaMajorVersion(e)))return e;throw new Error("未找到 Java 21+;请安装 Java 21 或更高版本,设置 JAVA_HOME,或填写 standaloneJavaCommand")}function isOverLimit(e,t){if(null==e||""===e)return!1;const i=Number(e);return Number.isFinite(i)&&i>t}async function javaMajorVersion(e){return new Promise(t=>{let i="",r=!1;const n=e=>{r||(r=!0,clearTimeout(s),t(e))},a=(0,node_child_process_1.spawn)(e,["-version"],{shell:!1,windowsHide:!0}),o=e=>{i=(i+String(e)).slice(-2e3)};a.stdout?.on("data",o),a.stderr?.on("data",o),a.once("error",()=>n()),a.once("close",()=>{const e=i.match(/(?:java|openjdk) version "(\d+)/i);n(e?Number(e[1]):void 0)});const s=setTimeout(()=>{a.kill(),n()},5e3)})}function supportsStandaloneJavaVersion(e){return void 0!==e&&e>=21}function standaloneJvmOptions(e){return["-Xms128m",`-Xmx${Math.max(128,Math.min(32768,Math.floor(e)))}m`,"-XX:+UseG1GC","-XX:+UseStringDeduplication","-XX:+ExitOnOutOfMemoryError"]}function isJavaMemoryFailure(e,t){return 137===t||/outofmemoryerror|java heap space|gc overhead limit|cannot reserve enough space|native memory allocation|insufficient memory|malloc failed|os::commit_memory/i.test(e)}async function resolveMinecraftResources(e){const t="string"==typeof e.minecraftJarPath?e.minecraftJarPath.trim():"";if(t&&await exists((0,node_path_1.resolve)(t)))return(0,node_path_1.resolve)(t);const i=(0,node_path_1.resolve)((0,node_path_1.join)(__dirname,"../assets/vanilla-resources/Minecraft-26.2-Vanilla-Resources.zip"));if(!await exists(i))throw new Error(`插件内置的 Minecraft 26.2 原版资源包不存在:${i}`);return i}async function renderWithStandalone(e,t,i,r,n,a="isometric"){const o=await acquireStandaloneRenderSlot();try{const o=(0,node_path_1.resolve)(r.standaloneRendererJar||(0,node_path_1.join)(__dirname,"../assets/standalone-renderer/litematic-standalone-renderer-0.2.8-patch.1.jar"));if(!await exists(o))throw new Error(`独立 Java 渲染器不存在:${o}`);if(!await exists(i))throw new Error(`Minecraft 资源 JAR 不存在:${i}`);for(const e of resourcePackPaths(r.resourcePackPaths))if(!await exists((0,node_path_1.resolve)(e)))throw new Error(`材质包不存在:${e}`);const s=["-Djava.awt.headless=true","-jar",o,"--input",(0,node_path_1.resolve)(e),"--output",(0,node_path_1.resolve)(t),"--minecraft-jar",i,"--resolution",String(r.javaResolution),"--supersampling",String(r.javaSupersampling),"--rotation",String(r.isometricRotation),"--slant",String(r.isometricSlant),"--fill",String(r.isometricFill),"--background",r.background];"six-face"===a&&s.push("--six-face-only","--six-face-resolution",String(r.outputSize),"--six-face-layout",r.sixFaceLayout),r.transparentBackground&&s.push("--transparent-background");for(const e of resourcePackPaths(r.resourcePackPaths))s.push("--resource-pack",(0,node_path_1.resolve)(e));const c=await resolveStandaloneJavaCommand(r.standaloneJavaCommand);for(let e=0;e<=r.standaloneJavaMemoryRestartLimit;e++){const i=0===e?r.standaloneJavaMaxHeapMb:r.standaloneJavaRetryMaxHeapMb,o=await runStandaloneJava(c,[...standaloneJvmOptions(i),...s],r.standaloneRenderTimeout);if(0===o.code)return;const d=isJavaMemoryFailure(o.stderr,o.code);if(d&&e<r.standaloneJavaMemoryRestartLimit){n.warn(`独立 Java 渲染超出 ${r.standaloneJavaMaxHeapMb} MiB 堆内存限制;正在启动全新进程重试(${e+1}/${r.standaloneJavaMemoryRestartLimit})`);const i="six-face"===a?["six-faces.png"]:["isometric.png","isometric-reverse.png"];await Promise.all(i.map(e=>node_fs_1.promises.rm((0,node_path_1.join)(t,e),{force:!0})));continue}const l=d?`;Java 内存不足(最大堆 ${r.standaloneJavaMaxHeapMb} MiB,已重启 ${e} 次)`:"";throw new Error(`独立 Java 渲染器退出码 ${o.code}${l}${o.stderr?`:${o.stderr.trim()}`:""}`)}}finally{o()}}async function runStandaloneJava(e,t,i){return new Promise((r,n)=>{const a=(0,node_child_process_1.spawn)(e,t,{shell:!1,windowsHide:!0});activeStandaloneJavaProcesses.add(a);let o="",s=!1;const c=()=>activeStandaloneJavaProcesses.delete(a);a.stderr?.on("data",e=>{o=(o+String(e)).slice(-6e3)});const d=setTimeout(()=>{s||(s=!0,(async()=>{await terminateStandaloneJavaProcess(a),c(),n(new Error("Standalone Java render timed out; process terminated and memory reclaimed"))})())},i);a.once("error",e=>{s||(s=!0,clearTimeout(d),terminateStandaloneJavaProcess(a),c(),n(e))}),a.once("close",e=>{s||(s=!0,clearTimeout(d),c(),r({code:e,stderr:o}))})})}async function terminateStandaloneJavaProcess(e){const t=e.pid;t&&("win32"===process.platform&&await new Promise(e=>{const i=(0,node_child_process_1.spawn)("taskkill.exe",["/PID",String(t),"/T","/F"],{windowsHide:!0});i.once("close",()=>e()),i.once("error",()=>e())}),null!=e.exitCode||e.killed||e.kill())}async function terminateActiveStandaloneJavaProcesses(e){const t=[...activeStandaloneJavaProcesses];t.length&&(await Promise.all(t.map(e=>terminateStandaloneJavaProcess(e))),e.info(`已终止 ${t.length} 个独立渲染 Java 进程并回收其子进程`))}function createGpuRenderRequest(e,t,i){const r=Math.max(128,Math.min(4096,Math.floor(t.outputSize))),n=(e,i,n)=>({id:e,name:i,yaw:n,pitch:t.isometricSlant,zoom:Math.max(.05,t.isometricFill/.95),autoFill:!0,width:r,height:r,background:t.background,transparentBackground:t.transparentBackground,supersampling:Math.max(1,Math.min(4,Math.floor(t.javaSupersampling)))});return{version:2,id:(0,node_crypto_1.randomUUID)(),filename:(0,node_path_1.basename)(e),views:[n("isometric","正二轴测",t.isometricRotation),n("isometric-reverse","反向正二轴测(旋转 180°)",t.isometricRotation+180)],sourceGroup:i?.group,sourceUser:i?.user}}async function renderWithJavaBridge(e,t,i){if(!i.javaBridgeDirectory.trim())throw new Error("Fabric Java 渲染桥任务目录未配置;使用 java 后端时请填写 javaBridgeDirectory");const r=(0,node_path_1.resolve)(i.javaBridgeDirectory),n=await readJson((0,node_path_1.join)(r,"status.json"));if(!n?.timestamp||Date.now()-n.timestamp>5e3)throw new Error("Minecraft Java 渲染桥未运行;请启动 1.21.1-Fabric 客户端并进入一个世界");if(!n.inWorld)throw new Error("Minecraft Java 渲染桥已启动,但客户端尚未进入世界");const a=n.resourcePacks??[];if(![["XeKr","3.6forMC1.20.2~1.21.5.zip"],["XKRDA","1.19.4~1.21snapshot.zip"]].every(e=>a.some(t=>e.every(e=>t.includes(e)))))throw new Error("Minecraft 客户端没有同时启用 XeKr 红显基础包和 XKRDA 附加包");const o=(0,node_crypto_1.randomUUID)(),s=(0,node_path_1.join)(r,"jobs"),c=(0,node_path_1.join)(r,"results",`${o}.result.json`),d=(0,node_path_1.join)(s,`${o}.job.json`),l=`${d}.tmp`;await node_fs_1.promises.mkdir(s,{recursive:!0}),await node_fs_1.promises.rm(c,{force:!0}),await node_fs_1.promises.writeFile(l,JSON.stringify({id:o,input:(0,node_path_1.resolve)(e),outputDirectory:(0,node_path_1.resolve)(t),resolution:i.javaResolution,supersampling:i.javaSupersampling,rotation:i.isometricRotation,slant:i.isometricSlant,fill:i.isometricFill,background:i.background,transparentBackground:i.transparentBackground})),await node_fs_1.promises.rename(l,d);const u=Date.now()+i.javaRenderTimeout;for(;Date.now()<u;){const e=await readJson(c);if(e){if(await node_fs_1.promises.rm(c,{force:!0}),!e.success)throw new Error(`Minecraft Java 渲染失败:${e.error??"未知错误"}`);return}await new Promise(e=>setTimeout(e,200))}throw new Error(`Minecraft Java 渲染超过 ${Math.round(i.javaRenderTimeout/1e3)} 秒`)}async function readJson(e){try{return JSON.parse(await node_fs_1.promises.readFile(e,"utf8"))}catch(e){if("ENOENT"===e?.code||e instanceof SyntaxError)return;throw e}}async function mergeRenderDiagnostics(e,t,i){const r=(0,node_path_1.join)(e,"render-diagnostics.json"),n=await readJson(r);if(!n)return;const a=Array.isArray(n.blocks)?n.blocks:[],o=Array.isArray(n.errors)?n.errors:[];if(!a.length&&!o.length)return;const s=(0,node_path_1.resolve)(t),c=await readJson(s)??{format:2,generatedAt:(new Date).toISOString(),blocks:[],errors:[]},d=new Map;for(const e of c.blocks??[])d.set(`${e.state}\0${e.reason}`,e);for(const e of a){const t=`${e.state}\0${e.reason}`,i=d.get(t);i?(i.count=Number(i.count??0)+Number(e.count??0),i.samples=[...i.samples??[],...e.samples??[]].slice(0,20)):d.set(t,{...e,samples:e.samples??[]})}const l=new Map;for(const e of c.errors??[])l.set(`${e.renderId}\0${e.engine}\0${e.message}`,e);for(const e of o){const t=`${e.renderId}\0${e.engine}\0${e.message}`,i=l.get(t);i?i.count=Number(i.count??0)+Number(e.count??0):l.set(t,{...e})}const u={format:2,pluginVersion:PLUGIN_VERSION,generatedAt:(new Date).toISOString(),blocks:[...d.values()],errors:[...l.values()]};await node_fs_1.promises.mkdir((0,node_path_1.resolve)(t,".."),{recursive:!0}).catch(()=>{}),await node_fs_1.promises.writeFile(s,JSON.stringify(u,null,2)+"\n"),a.length&&i.warn(`记录 ${a.length} 类无法正常渲染的方块;诊断已保存到 ${s}`),o.length&&i.warn(`记录 ${o.length} 类渲染错误;诊断已保存到 ${s}`)}async function rotateDiagnosticsFile(e,t,i){const r=await readJson(e);if(!r||r.pluginVersion===PLUGIN_VERSION)return;await node_fs_1.promises.mkdir(t,{recursive:!0});const n=cachePathSegment("string"==typeof r.pluginVersion?r.pluginVersion:"legacy"),a=formatDiagnosticsTimestamp(new Date),o=(0,node_path_1.join)(t,`litematic-renderer-diagnostics-v${n}-${a}.json`);await node_fs_1.promises.rename(e,o),i.info(`已将旧版诊断汇总归档到 ${o}`)}async function disableDiagnosticsSnapshot(e,t){if(!await readJson(e))return"当前没有可禁用的诊断汇总";await node_fs_1.promises.mkdir(t,{recursive:!0});const i=(0,node_path_1.join)(t,`litematic-renderer-diagnostics-disabled-${formatDiagnosticsTimestamp(new Date)}.json`);return await node_fs_1.promises.rename(e,i),await node_fs_1.promises.writeFile(e,JSON.stringify({format:2,pluginVersion:PLUGIN_VERSION,generatedAt:(new Date).toISOString(),blocks:[],errors:[]},null,2)+"\n"),`当前诊断已移入禁用文件夹:${i}`}async function appendGlobalRenderError(e,t,i,r,n){const a=(0,node_path_1.resolve)(e),o=await readJson(a)??{format:2,pluginVersion:PLUGIN_VERSION,generatedAt:(new Date).toISOString(),blocks:[],errors:[]};o.format=2,o.pluginVersion=PLUGIN_VERSION,o.errors??=[];const s=o.errors.find(e=>e.renderId===t&&e.engine===i&&e.message===r);s?s.count=Number(s.count??0)+1:o.errors.push({renderId:t,engine:i,message:r,count:1,generatedAt:(new Date).toISOString()}),await node_fs_1.promises.mkdir((0,node_path_1.dirname)(a),{recursive:!0}),await node_fs_1.promises.writeFile(a,JSON.stringify(o,null,2)+"\n"),n.warn(`渲染问题已记录到 ${a}`)}function formatDiagnosticsTimestamp(e){const t=e=>String(e).padStart(2,"0");return`${e.getFullYear()}${t(e.getMonth()+1)}${t(e.getDate())}-${t(e.getHours())}${t(e.getMinutes())}${t(e.getSeconds())}`}async function appendRenderError(e,t,i,r,n){const a=(0,node_path_1.join)(e,"render-diagnostics.json"),o=await readJson(a)??{format:2,generatedAt:(new Date).toISOString(),blocks:[],errors:[]};o.format=2,o.errors??=[];const s=r instanceof Error?r.message:String(r),c=(0,node_path_1.basename)(e),d=o.errors.find(e=>e.renderId===c&&e.engine===i&&e.message===s);d?d.count=Number(d.count??0)+1:o.errors.push({renderId:c,engine:i,message:s,count:1,generatedAt:(new Date).toISOString()}),await node_fs_1.promises.writeFile(a,JSON.stringify(o,null,2)+"\n"),await mergeRenderDiagnostics(e,t,n)}function formatRenderError(e,t){const i=e instanceof Error?e.message:String(e);return/投影方块数或区域体积超过/i.test(i)?`${i},不渲染`:/文件超过|file size|file.*limit/i.test(i)?`文件大小超过 ${(t.maxFileSize/1024).toFixed(2)} MB,不渲染`:/java 21\+|未找到.*java|java.*not found/i.test(i)?"未找到 Java 21+,请在插件配置中检查 Java 可执行文件设置":/standalone renderer|独立.*渲染器.*不存在|渲染器.*not exist/i.test(i)?"独立渲染器不可用,请重装插件或检查 Java 渲染配置":/minecraft.*resource|resource pack|原版资源|材质包.*不存在/i.test(i)?"Minecraft 原版资源包不可用,请检查资源包配置":"投影渲染失败,请检查渲染器配置或导出诊断文件"}async function renderWithGpu(e,t,i,r,n){try{return await new Promise((n,a)=>{const o=(0,node_child_process_1.spawn)(e,["--input",t,"--output",i,"--views","isometric,isometric-reverse"],{shell:!1,windowsHide:!0}),s=setTimeout(()=>{o.kill(),a(new Error("GPU 渲染超时"))},r);o.once("error",a),o.once("exit",e=>0===e?n():a(new Error(`GPU 渲染器退出码:${e}`))),o.once("close",()=>clearTimeout(s))}),!0}catch(e){return n.warn(`GPU 渲染器不可用,回退 CPU:${e instanceof Error?e.message:String(e)}`),!1}}async function renderWithWebgl(e,t,i){const r=await e.puppeteer.page();try{const e=(0,node_url_1.pathToFileURL)((0,node_path_1.resolve)(__dirname,"../assets/threeviews/index.html")).href;await r.goto(e,{waitUntil:"networkidle0",timeout:i.renderTimeout}),await r.waitForFunction(()=>Boolean(window.litematicViewerAPI&&window.deepslateResources),{timeout:i.renderTimeout});const n=await r.evaluate(async({base64:e,width:t,height:i,quality:r,isometricSquare:n,isometricFill:a,isometricRotation:o,isometricSlant:s})=>{const c=atob(e),d=new Uint8Array(c.length);for(let e=0;e<c.length;e++)d[e]=c.charCodeAt(e);const l=new File([d],"schematic.litematic",{type:"application/octet-stream"});return await window.litematicViewerAPI.loadFromFile(l),window.litematicViewerAPI.renderIsometricViews({width:t,height:i,quality:r,isometricSquare:n,isometricFill:a,isometricRotation:o,isometricSlant:s})},{base64:t.toString("base64"),width:i.webglWidth,height:i.webglHeight,quality:i.webglQuality,isometricSquare:i.isometricSquare,isometricFill:i.isometricFill,isometricRotation:i.isometricRotation,isometricSlant:i.isometricSlant});return[{title:"isometric.png",png:decodeDataUrl(n.isometricView)},{title:"isometric-reverse.png",png:decodeDataUrl(n.reverseIsometricView)}]}finally{await r.close().catch(()=>{})}}function decodeDataUrl(e){if("string"!=typeof e||!e.startsWith("data:image/"))throw new Error("WebGL 渲染器没有返回 PNG 数据");const t=e.indexOf(",");if(t<0)throw new Error("WebGL 渲染器返回了无效图像数据");return Buffer.from(e.slice(t+1),"base64")}function resolveSendOptions(e,t){const i=t?[...e.groupSendOptions].reverse().find(e=>e.groupId.trim()===t):void 0,r="enabled"===i?.replyAndMention||"disabled"!==i?.replyAndMention&&e.replyAndMention,n=i?.sendMode??(e.sendAsForward?"forward":"combined"),a=e.qqBotType??"selfHosted";return{qqBotType:a,sendMode:n,replyAndMention:r,showViewTitles:e.showViewTitles??!1,sixFaceOverview:("official"===a||"forward"===n)&&(e.sixFaceOverview??!0)}}async function sendImages(e,t,i,r,n="schematic"){if("official"===r.qqBotType){const a=await composeQqOverview(t),o=[...r.replyAndMention&&e.messageId?[(0,koishi_1.h)("quote",{id:e.messageId})]:[],koishi_1.h.image((0,node_url_1.pathToFileURL)(a).href),(0,koishi_1.h)("text",{content:`${i}\n${n} 已渲染成功,结果如上`})];return void await e.send(o)}const a=await Promise.all(t.map(async({title:e,path:t})=>({title:e,source:`base64://${(await node_fs_1.promises.readFile(t)).toString("base64")}`}))),o=a.map(({title:t,source:i})=>(0,koishi_1.h)("message",{userId:e.selfId,nickname:"投影渲染"},[...r.showViewTitles?[(0,koishi_1.h)("text",{content:t})]:[],(0,koishi_1.h)("img",{src:i})])),s=(0,koishi_1.h)("message",{userId:e.selfId,nickname:"投影信息"},[(0,koishi_1.h)("text",{content:i})]);if("forward"===r.sendMode)return void await e.send((0,koishi_1.h)("figure",{},[...o,s]));const c=r.replyAndMention?replyElements(e,r.qqBotType):[],d=a.flatMap(({title:e,source:t},i)=>[...r.showViewTitles?[(0,koishi_1.h)("text",{content:`${i?"\n":""}${e}\n`})]:[],(0,koishi_1.h)("img",{src:t})]);await e.send([...c,...d,(0,koishi_1.h)("text",{content:`\n${i}\n${n} 已渲染成功,结果如上`})])}async function composeQqOverview(e){if(!e.length)throw new Error("没有可合并的渲染图片");const t=await Promise.all(e.map(async e=>pngjs_1.PNG.sync.read(await node_fs_1.promises.readFile((0,node_path_1.resolve)(e.path))))),i=Math.max(...t.map(e=>e.width)),r=Math.max(4,Math.round(.01*i)),n=[];let a=0;const o=t.slice(0,Math.min(2,t.length)),s=1===o.length?i:Math.floor((i-r)/2),c=o.map(e=>Math.max(1,Math.round(e.height*s/e.width))),d=Math.max(...c);for(let e=0;e<o.length;e++)n.push({image:o[e],x:e*(s+r),y:Math.floor((d-c[e])/2),width:s,height:c[e]});a+=d;for(const e of t.slice(2)){a+=r;const t=Math.max(1,Math.round(e.height*i/e.width));n.push({image:e,x:0,y:a,width:i,height:t}),a+=t}const l=new pngjs_1.PNG({width:i,height:a});for(const e of n)drawScaledPng(l,e);const u=(0,node_path_1.resolve)((0,node_path_1.dirname)(e[0].path),"qq-overview.png");return await node_fs_1.promises.writeFile(u,pngjs_1.PNG.sync.write(l)),u}function drawScaledPng(e,t){const{image:i,x:r,y:n,width:a,height:o}=t;for(let t=0;t<o;t++){const s=Math.min(i.height-1,Math.floor((t+.5)*i.height/o));for(let o=0;o<a;o++){const c=Math.min(i.width-1,Math.floor((o+.5)*i.width/a)),d=4*(s*i.width+c),l=4*((n+t)*e.width+r+o);i.data.copy(e.data,l,d,d+4)}}}function replyElements(e,t=("qq"===e.platform?"official":"selfHosted")){const i=[];return e.messageId&&i.push((0,koishi_1.h)("quote",{id:e.messageId})),"selfHosted"===t&&e.userId&&(i.push((0,koishi_1.h)("at",{id:e.userId})),i.push((0,koishi_1.h)("text",{content:"\n"}))),i}class NbtReader{data;offset=0;constructor(e){this.data=e}byte(){return this.data.readInt8(this.offset++)}ubyte(){return this.data.readUInt8(this.offset++)}short(){const e=this.data.readInt16BE(this.offset);return this.offset+=2,e}int(){const e=this.data.readInt32BE(this.offset);return this.offset+=4,e}float(){const e=this.data.readFloatBE(this.offset);return this.offset+=4,e}double(){const e=this.data.readDoubleBE(this.offset);return this.offset+=8,e}long(){const e=this.data.readBigInt64BE(this.offset);return this.offset+=8,e}text(){const e=this.data.readUInt16BE(this.offset);this.offset+=2;const t=this.data.toString("utf8",this.offset,this.offset+e);return this.offset+=e,t}readNamedRoot(){if(10!==this.ubyte())throw new Error("不是 NBT Compound 数据");return this.text(),this.value(10)}value(e){switch(e){case 0:return null;case 1:return this.byte();case 2:return this.short();case 3:return this.int();case 4:return this.long();case 5:return this.float();case 6:return this.double();case 7:{const e=this.int(),t=this.data.subarray(this.offset,this.offset+e);return this.offset+=e,t}case 8:return this.text();case 9:{const e=this.ubyte(),t=this.int();return Array.from({length:t},()=>this.value(e))}case 10:{const e={};for(;;){const t=this.ubyte();if(!t)return e;e[this.text()]=this.value(t)}}case 11:{const e=this.int();return Array.from({length:e},()=>this.int())}case 12:{const e=this.int();return Array.from({length:e},()=>this.long())}default:throw new Error(`不支持的 NBT 标签类型:${e}`)}}}const MINECRAFT_DATA_VERSIONS=new Map([[4903,"26.2"],[4790,"26.1.2"],[4671,"1.21.11"],[4557,"1.21.10"],[4555,"1.21.9"],[4440,"1.21.8"],[4438,"1.21.7"],[4435,"1.21.6"],[4325,"1.21.5"],[4189,"1.21.4"],[4082,"1.21.3"],[4080,"1.21.2"],[3955,"1.21.1"],[3953,"1.21"],[3839,"1.20.6"],[3837,"1.20.5"],[3700,"1.20.4"],[3698,"1.20.3"],[3578,"1.20.2"],[3465,"1.20.1"],[3463,"1.20"],[3337,"1.19.4"],[3218,"1.19.3"],[3120,"1.19.2"],[3117,"1.19.1"],[3105,"1.19"],[2975,"1.18.2"]]);function readLitematicRoot(e){let t;try{t=(0,node_zlib_1.gunzipSync)(e)}catch{t=e}return new NbtReader(t).readNamedRoot()}function compoundValue(e){return!e||"object"!=typeof e||Array.isArray(e)||Buffer.isBuffer(e)?void 0:e}function numericValue(e){if("number"==typeof e&&Number.isFinite(e))return e;if("bigint"==typeof e){const t=Number(e);if(Number.isSafeInteger(t))return t}}function readMetadataSize(e,t){const i=compoundValue(t.EnclosingSize),r=i&&[numericValue(i.x),numericValue(i.y),numericValue(i.z)];if(r?.every(e=>null!=e))return r.map(e=>Math.abs(e));const n=compoundValue(e.Regions);if(!n)return;let a=1/0,o=1/0,s=1/0,c=-1/0,d=-1/0,l=-1/0;for(const e of Object.values(n)){const t=compoundValue(e),i=compoundValue(t?.Position),r=compoundValue(t?.Size),n=numericValue(i?.x),u=numericValue(i?.y),m=numericValue(i?.z),h=numericValue(r?.x),p=numericValue(r?.y),f=numericValue(r?.z);if([n,u,m,h,p,f].some(e=>null==e)||0===h||0===p||0===f)continue;const g=n+h-Math.sign(h),_=u+p-Math.sign(p),w=m+f-Math.sign(f);a=Math.min(a,n,g),o=Math.min(o,u,_),s=Math.min(s,m,w),c=Math.max(c,n,g),d=Math.max(d,u,_),l=Math.max(l,m,w)}return Number.isFinite(a)?[c-a+1,d-o+1,l-s+1]:void 0}function formatCreatedAt(e){if(null==e||e<=0)return"未知";const t=new Date(e<1e10?1e3*e:e);if(Number.isNaN(t.getTime()))return"未知";const i=new Intl.DateTimeFormat("zh-CN",{timeZone:"Asia/Shanghai",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hourCycle:"h23"}).formatToParts(t),r=Object.fromEntries(i.map(e=>[e.type,e.value]));return`${r.year}-${r.month}-${r.day} ${r.hour}:${r.minute}:${r.second}`}function parseLitematicMetadata(e){const t=readLitematicRoot(e),i=compoundValue(t.Metadata)??{},r=numericValue(t.MinecraftDataVersion);return{author:"string"==typeof i.Author&&i.Author.trim()?i.Author.trim():"未知",createdAt:formatCreatedAt(numericValue(i.TimeCreated)),totalBlocks:numericValue(i.TotalBlocks),totalVolume:numericValue(i.TotalVolume),size:readMetadataSize(t,i),litematicVersion:numericValue(t.Version),minecraftDataVersion:r,minecraftVersion:null==r?void 0:MINECRAFT_DATA_VERSIONS.get(r)}}function validateProjectionLimits(e,t,i){const r=Number.isSafeInteger(i)&&i>0?i:5e6,n=()=>{throw new Error(`投影方块数或区域体积超过 ${r.toLocaleString("en-US")} 限制`)};(Math.abs(t.totalBlocks??0)>r||Math.abs(t.totalVolume??0)>r)&&n();const a=compoundValue(readLitematicRoot(e).Regions);if(!a)return;let o=0;for(const e of Object.values(a)){const t=compoundValue(compoundValue(e)?.Size),i=[numericValue(t?.x),numericValue(t?.y),numericValue(t?.z)];if(i.some(e=>null==e))continue;const a=i.reduce((e,t)=>e*Math.abs(t),1);(!Number.isSafeInteger(a)||a>r-o)&&n(),o+=a}}function formatLitematicMetadata(e,t){const i=null==e.totalBlocks?"未知":String(e.totalBlocks),r=null==e.totalVolume?"未知":String(e.totalVolume),n=e.size?.join(" × ")??"未知",a=null==e.litematicVersion?"未知":String(e.litematicVersion),o=e.minecraftVersion??"未知",s=null==e.minecraftDataVersion?"":`(数据版本:${e.minecraftDataVersion})`,c="string"==typeof t&&t.trim()?(0,node_path_1.basename)(t).replace(/\.litematic$/i,""):"";return[...c?[`投影名称:${c}`]:[],`保存者游戏 ID:${e.author}`,`创建时间:${e.createdAt}`,`方块数/体积:${i}/${r}`,`尺寸:${n}`,`Litematic 版本:${a}`,`游戏版本:${o}${s}`].join("\n")}function parseLitematic(e){const t=readLitematicRoot(e).Regions;if(!t||"object"!=typeof t)throw new Error("文件不含 Litematica Regions 数据");const i=[];for(const e of Object.values(t))parseRegion(e,i);if(!i.length)throw new Error("投影中没有非空气方块");return i}function parseRegion(e,t){const i=e.Position,r=e.Size,n=e.BlockStatePalette,a=e.BlockStates;if(!(i&&r&&Array.isArray(n)&&Array.isArray(a)))return;const o=Math.abs(r.x),s=Math.abs(r.y),c=Math.abs(r.z),d=o*s*c;if(!d)return;const l=Math.max(2,Math.ceil(Math.log2(n.length))),u=r.x<0?-1:1,m=r.y<0?-1:1,h=r.z<0?-1:1;for(let e=0;e<d;e++){const r=unpackState(a,e,l),s=n[r]?.Name;if(!s||"minecraft:air"===s||"minecraft:cave_air"===s||"minecraft:void_air"===s)continue;const d=e%o,p=Math.floor(e/o)%c,f=Math.floor(e/(o*c));t.push({x:i.x+d*u,y:i.y+f*m,z:i.z+p*h,name:s})}}function unpackState(e,t,i){const r=BigInt(t*i),n=Number(r/64n),a=r%64n;if(n>=e.length)return 0;const o=(1n<<BigInt(i))-1n;let s=BigInt.asUintN(64,e[n])>>a&o;return a+BigInt(i)>64n&&n+1<e.length&&(s|=BigInt.asUintN(64,e[n+1])<<64n-a&o),Number(s)}function renderSchematic(e,t){const i=getBounds(e);return[{title:"isometric.png",png:renderIsometric(e,i,t,!1)},{title:"isometric-reverse.png",png:renderIsometric(e,i,t,!0)}]}function getBounds(e){return e.reduce((e,t)=>({minX:Math.min(e.minX,t.x),minY:Math.min(e.minY,t.y),minZ:Math.min(e.minZ,t.z),maxX:Math.max(e.maxX,t.x),maxY:Math.max(e.maxY,t.y),maxZ:Math.max(e.maxZ,t.z)}),{minX:1/0,minY:1/0,minZ:1/0,maxX:-1/0,maxY:-1/0,maxZ:-1/0})}const SIX_FACE_ORDER=["up","down","east","south","west","north"],SIX_FACE_LABELS={up:["00000100000","00000100000","00000100000","00000100000","00000111110","00000100000","00000100000","00000100000","00000100000","00000100000","11111111111"],down:["00000000000","11111111111","00000100000","00000100000","00000110000","00000101000","00000100100","00000100100","00000100000","00000100000","00000100000"],east:["00000100000","00000100000","01111111111","00001010000","00010010000","00111111110","00000010000","00010010100","00100010010","01000010001","00001110000"],south:["00000100000","11111111111","00000100000","01111111110","01010001010","01001010010","01111111110","01000100010","01111111110","01000100010","01000100110"],west:["00000000000","11111111111","00001010000","01111111110","01001010010","01001010010","01010010010","01100001110","01000000010","01111111110","01000000010"],north:["00010010000","00010010000","00010010000","11110010011","00010011100","00010010000","00010010000","00110010000","01010010001","10010010001","00010001111"]};function renderSixFaceOverview(e,t,i=t.sixFaceLayout??"horizontal"){if(!e.length)throw new Error("投影中没有可生成六面图的方块");const r=Math.max(128,Math.floor(t.outputSize)),n=Math.max(4,Math.round(.01*r)),a="vertical"===i?2:3,o="vertical"===i?3:2,s=Math.floor((r-n*("vertical"===i?o+1:a+1))/("vertical"===i?o:a)),c=new Raster("vertical"===i?a*s+n*(a+1):r,"vertical"===i?r:o*s+n*(o+1),t),d=getBounds(e);for(const[t,i]of SIX_FACE_ORDER.entries()){const r=n+t%a*(s+n),o=n+Math.floor(t/a)*(s+n);drawOrthographicFace(c,e,d,i,r,o,s,n),drawFaceLabel(c,i,r,o,s,n)}return c.png()}function drawOrthographicFace(e,t,i,r,n,a,o,s){const c="east"===r||"west"===r?i.maxZ-i.minZ+1:i.maxX-i.minX+1,d="up"===r||"down"===r?i.maxZ-i.minZ+1:i.maxY-i.minY+1,l=new Map;for(const e of t){const t=orthographicCoordinates(e,i,r),n=`${t.a},${t.b}`,a=l.get(n);(!a||t.depth>a.depth)&&l.set(n,{block:e,...t})}const u=Math.max(2,Math.floor(s/2)),m=Math.max(1,o-2*u),h=Math.min(m/c,m/d),p=n+u+(m-c*h)/2,f=a+u+(m-d*h)/2;for(const{block:t,a:i,b:r}of[...l.values()].sort((e,t)=>e.b-t.b||e.a-t.a)){const n=Math.floor(p+i*h),a=Math.floor(f+r*h),o=Math.max(n+1,Math.ceil(p+(i+1)*h)),s=Math.max(a+1,Math.ceil(f+(r+1)*h));e.rect(n,a,o-n,s-a,colorFor(t.name))}}function orthographicCoordinates(e,t,i){switch(i){case"up":return{a:e.x-t.minX,b:e.z-t.minZ,depth:e.y};case"down":return{a:t.maxX-e.x,b:e.z-t.minZ,depth:-e.y};case"east":return{a:t.maxZ-e.z,b:t.maxY-e.y,depth:e.x};case"south":return{a:e.x-t.minX,b:t.maxY-e.y,depth:e.z};case"west":return{a:e.z-t.minZ,b:t.maxY-e.y,depth:-e.x};case"north":return{a:t.maxX-e.x,b:t.maxY-e.y,depth:-e.z}}}function drawFaceLabel(e,t,i,r,n,a){const o=SIX_FACE_LABELS[t],s=Math.max(1,Math.min(6,Math.floor(n/80))),c=Math.max(1,Math.floor(s/2)),d=i+Math.max(2,Math.floor(a/2)),l=r+Math.max(2,Math.floor(a/2));e.rect(d,l,o[0].length*s+2*c,o.length*s+2*c,"#000000");for(let t=0;t<o.length;t++)for(let i=0;i<o[t].length;i++)"1"===o[t][i]&&e.rect(d+c+i*s,l+c+t*s,s,s,"#ffffff")}function renderIsometric(e,t,i,r){const n=i.isometricCellSize,a=Math.ceil(n/2),o=(t.maxX-t.minX+t.maxZ-t.minZ+2)*n+4,s=(t.maxX-t.minX+t.maxZ-t.minZ+2)*a+(t.maxY-t.minY+2)*n+4,c=Math.max(1,Math.min(1,i.outputSize/Math.max(o,s))),d=new Raster(Math.ceil(o*c),Math.ceil(s*c),i),l=e=>({x:r?t.maxX-e.x:e.x-t.minX,z:r?t.maxZ-e.z:e.z-t.minZ}),u=[...e].sort((e,t)=>{const i=l(e),r=l(t);return i.x+i.z+e.y-(r.x+r.z+t.y)});for(const e of u){const i=l(e),r=(i.x-i.z)*n+o/2,s=(i.x+i.z)*a+(t.maxY-e.y+1)*n;d.voxel(Math.round(r*c),Math.round(s*c),Math.max(2,Math.round(n*c)),colorFor(e.name))}return d.png()}const COLORS={stone:"#777777",cobblestone:"#707070",deepslate:"#4b4b50",dirt:"#855b37",grass_block:"#63913d",sand:"#dbca85",glass:"#a7d9df",water:"#3f76e4",lava:"#e05b22",oak_planks:"#b9915a",spruce_planks:"#765033",birch_planks:"#d9c485",iron_block:"#d7d7d7",gold_block:"#f6c543",diamond_block:"#5ed6c7",redstone_block:"#b21d1d",netherrack:"#823332",quartz_block:"#e9e4dc",obsidian:"#211d2d",white_wool:"#e9e9e9",black_wool:"#1d1d1d",red_wool:"#a12722",blue_wool:"#3549a7",green_wool:"#4c702c",yellow_wool:"#e5c440"};function colorFor(e){const t=e.replace(/^minecraft:/,"");if(COLORS[t])return COLORS[t];for(const[e,i]of Object.entries(COLORS))if(t.includes(e))return i;const i=(0,node_crypto_1.createHash)("md5").update(t).digest();return`#${(90+i[0]%120).toString(16).padStart(2,"0")}${(90+i[1]%120).toString(16).padStart(2,"0")}${(90+i[2]%120).toString(16).padStart(2,"0")}`}class Raster{width;height;data;background;constructor(e,t,i){this.width=e,this.height=t,this.background=parseColor(i.background,i.transparentBackground?0:255),this.data=Buffer.alloc(e*t*4);for(let e=0;e<this.data.length;e+=4)this.data.set(this.background,e)}rect(e,t,i,r,n){for(let a=Math.max(0,t);a<Math.min(this.height,t+r);a++)for(let t=Math.max(0,e);t<Math.min(this.width,e+i);t++)this.pixel(t,a,n)}pixel(e,t,i){e<0||t<0||e>=this.width||t>=this.height||this.data.set(parseColor(i),4*(t*this.width+e))}voxel(e,t,i,r){const n=adjust(r,1.15),a=adjust(r,.78),o=adjust(r,.6),s=Math.ceil(i/2);for(let r=0;r<s;r++)for(let a=-i+r;a<=i-r;a++)this.pixel(e+a,t-s+r,n);for(let r=0;r<i;r++)for(let n=-i+Math.floor(r/2);n<=0;n++)this.pixel(e+n,t+r,a);for(let r=0;r<i;r++)for(let n=0;n<=i-Math.floor(r/2);n++)this.pixel(e+n,t+r,o)}png(){const e=Buffer.alloc((4*this.width+1)*this.height);for(let t=0;t<this.height;t++)e[t*(4*this.width+1)]=0,this.data.copy(e,t*(4*this.width+1)+1,t*this.width*4,(t+1)*this.width*4);const t=Buffer.alloc(13);return t.writeUInt32BE(this.width,0),t.writeUInt32BE(this.height,4),t.set([8,6,0,0,0],8),Buffer.concat([Buffer.from("‰PNG\r\n\n","binary"),chunk("IHDR",t),chunk("IDAT",(0,node_zlib_1.deflateSync)(e)),chunk("IEND",Buffer.alloc(0))])}}function parseColor(e,t=255){const i=/^#?([0-9a-f]{6})$/i.exec(e)?.[1]??"182026";return[parseInt(i.slice(0,2),16),parseInt(i.slice(2,4),16),parseInt(i.slice(4,6),16),t]}function adjust(e,t){const[i,r,n]=parseColor(e);return`#${[i,r,n].map(e=>Math.round(Math.min(255,e*t)).toString(16).padStart(2,"0")).join("")}`}function chunk(e,t){const i=Buffer.from(e),r=Buffer.alloc(4);r.writeUInt32BE(t.length);const n=Buffer.alloc(4);return n.writeUInt32BE(crc32(Buffer.concat([i,t]))>>>0),Buffer.concat([r,i,t,n])}const CRC_TABLE=Array.from({length:256},(e,t)=>{let i=t;for(let e=0;e<8;e++)i=1&i?3988292384^i>>>1:i>>>1;return i>>>0});function crc32(e){let t=4294967295;for(const i of e)t=CRC_TABLE[255&(t^i)]^t>>>8;return(4294967295^t)>>>0}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiaozhu2007/koishi-plugin-litematic-renderer",
3
- "version": "0.5.5",
3
+ "version": "0.6.0",
4
4
  "description": "Render small Litematica schematics sent in QQ groups",
5
5
  "keywords": [
6
6
  "koishi",
@@ -20,9 +20,11 @@
20
20
  },
21
21
  "files": [
22
22
  "lib",
23
+ "client",
24
+ "dist",
23
25
  "assets/threeviews",
24
26
  "assets/java-bridge",
25
- "assets/standalone-renderer/litematic-standalone-renderer-0.2.7-patch.1.jar",
27
+ "assets/standalone-renderer/litematic-standalone-renderer-0.2.8-patch.1.jar",
26
28
  "assets/vanilla-resources",
27
29
  "README.md"
28
30
  ],
@@ -32,20 +34,26 @@
32
34
  "access": "public"
33
35
  },
34
36
  "scripts": {
35
- "build": "tsc -p tsconfig.json",
37
+ "build": "tsc -p tsconfig.json && koishi-console build",
38
+ "postbuild": "terser lib/index.js --compress --mangle --output lib/index.js",
36
39
  "format": "oxfmt .",
37
40
  "format:check": "oxfmt --check .",
38
- "postbuild": "terser lib/index.js --compress --mangle --output lib/index.js",
39
41
  "test": "node --test test/*.test.mjs"
40
42
  },
41
43
  "dependencies": {
42
44
  "@sentry/node-core": "^10.71.0",
43
- "pngjs": "^7.0.0"
45
+ "pngjs": "^7.0.0",
46
+ "ssh2": "^1.17.0",
47
+ "ws": "^8.18.3"
44
48
  },
45
49
  "devDependencies": {
50
+ "@koishijs/client": "^5.30.11",
46
51
  "@koishijs/plugin-adapter-onebot": "^6.0.2",
52
+ "@koishijs/plugin-console": "^5.30.11",
47
53
  "@types/node": "^22.0.0",
48
54
  "@types/pngjs": "^6.0.5",
55
+ "@types/ssh2": "^1.15.5",
56
+ "@types/ws": "^8.5.13",
49
57
  "koishi": "^4.18.0",
50
58
  "oxfmt": "^0.65.0",
51
59
  "terser": "^5.51.2",