|
14 | 14 |
|
15 | 15 | import fs from "node:fs/promises" |
16 | 16 | import path from "node:path" |
| 17 | +import { createRequire } from "node:module" |
17 | 18 | import { CF_DATA_ROOT } from "../config.js" |
18 | 19 |
|
| 20 | +const require = createRequire(import.meta.url) |
| 21 | + |
19 | 22 | /** 平台生成器 Skill 存储根目录 */ |
20 | 23 | export const CF_PLATFORM_ROOT = |
21 | 24 | process.env.CF_PLATFORM_ROOT ?? |
@@ -147,34 +150,60 @@ export class InvalidSkillPackageError extends Error { |
147 | 150 | } |
148 | 151 |
|
149 | 152 | /** |
150 | | - * 解压 zip Buffer,返回文件内容 Map<相对路径, Buffer>。 |
151 | | - * 校验根目录必须包含 SKILL.md,否则抛 InvalidSkillPackageError。 |
152 | | - * |
153 | | - * 依赖:unzipper(需在 package.json 中声明) |
| 153 | + * 纯 Node.js zip 解析(无外部依赖,兼容 Node v23)。 |
| 154 | + * 支持 STORED (0) 和 DEFLATED (8) 压缩方式。 |
154 | 155 | */ |
155 | 156 | export async function extractAndValidateZip( |
156 | 157 | zipBuffer: Buffer |
157 | 158 | ): Promise<Map<string, Buffer>> { |
158 | | - const unzipper = await import("unzipper").catch(() => { |
159 | | - throw new Error( |
160 | | - "unzipper is not installed. Run: npm install unzipper @types/unzipper" |
161 | | - ) |
162 | | - }) |
163 | | - |
164 | | - const directory = await unzipper.Open.buffer(zipBuffer) |
| 159 | + const { inflateRawSync } = require("node:zlib") as typeof import("node:zlib") |
165 | 160 | const files = new Map<string, Buffer>() |
166 | 161 |
|
167 | | - // 收集所有文件 |
168 | | - for (const file of directory.files) { |
169 | | - if (file.type === "Directory") continue |
170 | | - const content = await file.buffer() |
171 | | - // 去除 zip 根目录前缀(如 my-skill/SKILL.md → SKILL.md) |
172 | | - const parts = file.path.split("/") |
173 | | - const relPath = parts.length > 1 ? parts.slice(1).join("/") : parts[0] |
174 | | - if (relPath) files.set(relPath, content) |
| 162 | + // 强制拷贝一份独立 Buffer,彻底避免内存池引用问题 |
| 163 | + const buf = Buffer.allocUnsafe(zipBuffer.length) |
| 164 | + zipBuffer.copy(buf) |
| 165 | + |
| 166 | + process.stderr.write(`[zip] parse start, len=${buf.length}, sig=${buf.readUInt32LE(0).toString(16)}\n`) |
| 167 | + |
| 168 | + let offset = 0 |
| 169 | + while (offset < buf.length - 4) { |
| 170 | + const sig = buf.readUInt32LE(offset) |
| 171 | + if (sig !== 0x04034b50) break // Local file header signature |
| 172 | + |
| 173 | + const compression = buf.readUInt16LE(offset + 8) |
| 174 | + const compSize = buf.readUInt32LE(offset + 18) |
| 175 | + const nameLen = buf.readUInt16LE(offset + 26) |
| 176 | + const extraLen = buf.readUInt16LE(offset + 28) |
| 177 | + const name = buf.slice(offset + 30, offset + 30 + nameLen).toString("utf-8") |
| 178 | + const dataOffset = offset + 30 + nameLen + extraLen |
| 179 | + const compData = buf.slice(dataOffset, dataOffset + compSize) |
| 180 | + |
| 181 | + process.stderr.write(`[zip] entry name=${name} comp=${compression} compSize=${compSize} dataOffset=${dataOffset} compDataLen=${compData.length}\n`) |
| 182 | + |
| 183 | + if (!name.endsWith("/")) { |
| 184 | + let content: Buffer |
| 185 | + if (compression === 0) { |
| 186 | + content = Buffer.from(compData) // 显式拷贝 |
| 187 | + } else if (compression === 8) { |
| 188 | + content = inflateRawSync(compData) |
| 189 | + } else { |
| 190 | + throw new InvalidSkillPackageError(`Unsupported compression method ${compression} for: ${name}`) |
| 191 | + } |
| 192 | + const parts = name.split("/") |
| 193 | + const relPath = parts.length > 1 ? parts.slice(1).join("/") : parts[0]! |
| 194 | + if (relPath) files.set(relPath, content) |
| 195 | + process.stderr.write(`[zip] stored relPath=${relPath} contentLen=${content.length}\n`) |
| 196 | + } |
| 197 | + |
| 198 | + offset = dataOffset + compSize |
| 199 | + } |
| 200 | + |
| 201 | + process.stderr.write(`[zip] total files: ${files.size} [${[...files.keys()].join(', ')}]\n`) |
| 202 | + |
| 203 | + if (files.size === 0) { |
| 204 | + throw new InvalidSkillPackageError("Invalid skill package: zip appears empty or corrupt") |
175 | 205 | } |
176 | 206 |
|
177 | | - // 校验 SKILL.md 存在 |
178 | 207 | const hasSkillMd = [...files.keys()].some( |
179 | 208 | (p) => p === "SKILL.md" || p.toLowerCase() === "skill.md" |
180 | 209 | ) |
|
0 commit comments