87 lines
3.0 KiB
JavaScript
87 lines
3.0 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Skill 加载器 - 从远程 Worker 拉取 prompt (含本地 LRU 缓存 + 离线降级)
|
|
* 用法: node load-skill.js <skillName>
|
|
* 由 stub SKILL.md 通过 Bash 调用, stdout 输出 prompt 给模型消费
|
|
*/
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const os = require("os");
|
|
const https = require("https");
|
|
const crypto = require("crypto");
|
|
|
|
const API_BASE = process.env.BW_API_BASE || "https://bookworm-router.bookworm-api.workers.dev";
|
|
const CLAUDE_DIR = path.join(os.homedir(), ".claude");
|
|
const TOKEN_FILE = path.join(CLAUDE_DIR, ".bw-token");
|
|
const CACHE_DIR = path.join(CLAUDE_DIR, ".skill-cache");
|
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 小时
|
|
|
|
function die(msg, code = 1) { process.stderr.write(`[FAIL] ${msg}\n`); process.exit(code); }
|
|
|
|
const skillName = process.argv[2];
|
|
if (!skillName || !/^[a-z0-9-]{1,64}$/.test(skillName)) die("usage: load-skill <skill-name>");
|
|
|
|
function readToken() {
|
|
if (!fs.existsSync(TOKEN_FILE)) die("未激活, 请先运行: node activate.js");
|
|
const d = JSON.parse(fs.readFileSync(TOKEN_FILE, "utf8"));
|
|
if (Date.now() > d.expires_at - 24 * 60 * 60 * 1000) {
|
|
process.stderr.write(`[WARN] Token 即将过期, 请重新激活\n`);
|
|
}
|
|
if (Date.now() > d.expires_at) die("Token 已过期, 请重新激活");
|
|
return d.token;
|
|
}
|
|
|
|
try { fs.mkdirSync(CACHE_DIR, { recursive: true, mode: 0o700 }); } catch {}
|
|
const cacheFile = path.join(CACHE_DIR, crypto.createHash("sha256").update(skillName).digest("hex").slice(0, 16));
|
|
|
|
// 先查缓存
|
|
function tryCache(fresh) {
|
|
if (!fs.existsSync(cacheFile)) return null;
|
|
const st = fs.statSync(cacheFile);
|
|
if (fresh && Date.now() - st.mtimeMs > CACHE_TTL_MS) return null;
|
|
return fs.readFileSync(cacheFile, "utf8");
|
|
}
|
|
|
|
const fresh = tryCache(true);
|
|
if (fresh) { process.stdout.write(fresh); process.exit(0); }
|
|
|
|
// 远程拉取
|
|
function fetch() {
|
|
return new Promise((resolve, reject) => {
|
|
const token = readToken();
|
|
const req = https.request({
|
|
hostname: new URL(API_BASE).hostname,
|
|
port: 443,
|
|
path: `/skill/${skillName}`,
|
|
method: "POST",
|
|
timeout: 15000,
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", "Content-Length": 2 }
|
|
}, (res) => {
|
|
let buf = "";
|
|
res.on("data", c => buf += c);
|
|
res.on("end", () => {
|
|
if (res.statusCode === 200) resolve(buf);
|
|
else reject(new Error(`HTTP ${res.statusCode}: ${buf.slice(0, 150)}`));
|
|
});
|
|
});
|
|
req.on("error", reject);
|
|
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
|
|
req.write("{}");
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
fetch().then(prompt => {
|
|
fs.writeFileSync(cacheFile, prompt, { mode: 0o600 });
|
|
process.stdout.write(prompt);
|
|
}).catch(e => {
|
|
// 离线降级: 用过期缓存
|
|
const stale = tryCache(false);
|
|
if (stale) {
|
|
process.stderr.write(`[WARN] 离线模式, 使用 stale cache: ${e.message}\n`);
|
|
process.stdout.write(stale);
|
|
} else {
|
|
die(`远程获取失败 + 无缓存: ${e.message}`);
|
|
}
|
|
});
|