/** * WebSkillLoader —— 上行适配器:按 `skillZipUrl` 协议加载技能正文。 * * 防腐层向上端口 {@link ISkillLoader} 的统一实现:对每个 {@link ISkillResource} 的 * `config.skillZipUrl` 按协议分发—— * - `http(s)://`:fetch 远端 zip 字节 → fflate `unzip` 解压 → 在解压文件树里识别技能正文 * (`SKILL.md`,任意层级;缺省回退首个顶层 `.md`)→ 返回正文 `string[]`。 * - `file://`:本地路径(目录或 zip 文件)。用 `node:url.fileURLToPath` 解析成绝对路径, * **校验必须落在构造时传入的 `root`(工作区 cwd)内**(防越界读敏感文件),再 `stat` 分发: * 目录 → 读其下 `SKILL.md`;zip 文件 → 同 http 分支解压取正文;其它文件 → 当裸 md 直接读。 * * 设计要点: * - **file:// 越界防卫**:`file://` 解析后的绝对路径必须 `=== root` 或以 `root + sep` 开头, * 否则抛「路径逃逸 root」错。语义对齐原 LocalFileSkillLoader,避免 `file:///etc/passwd` 之类。 * - **fetch 可注入**:经 `options.fetchImpl` 注入自定义 fetch(单测用假实现);缺省用全局 * `fetch`。沙箱运行时(Node 22+)自带全局 fetch。 * - **仅 http(s)/file**:协议白名单,拒绝 file/ftp 之外的 file 变体与其它协议。 * - **错误显式暴露**:fetch 失败 / 解压失败 / 无 md 正文 / 路径逃逸 → 抛 `Error`(带 resourceId * 诊断),不静默吞——skill 加载失败应让该轮显式报错,而非静默降级。 * - **零盘**:解压在内存(`Unzipped` 即 `Record`),不落临时目录, * 符合 linkquejs「不提供通用 shell / 不写业务库」的边界。 */ import type { ISkillResource } from '../vendor/linkque-agent-protocol/index.js'; import type { ISkillLoader } from '../core/ports.js'; /** WebSkillLoader 构造选项。 */ export interface WebSkillLoaderOptions { /** * 工作区根目录(必填)。`file://` 协议的 skillZipUrl 解析后的绝对路径必须落在该目录内, * 否则视为越界拒绝。通常是 agent 运行的工作区 cwd。 */ root: string; /** * 自定义 fetch 实现(可选,单测注入假 fetch)。缺省用全局 `fetch`。 * 入参为 zip URL,出参为 Response(需支持 `.arrayBuffer()`)。仅 http(s) 分支用到。 */ fetchImpl?: (url: string) => Promise; } /** * WebSkillLoader —— 按 `skillZipUrl` 协议加载技能正文的 ISkillLoader 实现。 * * `http(s)://` 走远端 fetch+解压;`file://` 走本地路径(受 `root` 越界校验)。 */ export declare class WebSkillLoader implements ISkillLoader { private readonly root; private readonly fetchImpl; constructor(options: WebSkillLoaderOptions); load(resources: ISkillResource[]): Promise; /** 加载单个 skill 资源:按 skillZipUrl 协议分发。 */ private loadOne; /** 加载 file:// 资源:解析绝对路径 → 越界校验 → 按 stat 类型分发。 */ private loadFromFile; /** fetch zip 字节;非 2xx 或网络失败抛错。 */ private fetchZip; }