koishi插件市场镜像分享+自建教程

我的插件源:https://ksmirror.fjgjkg.xyz/market.json
注:更换完源后记得重启koishi,不然可能无法访问插件商店。
index.js的代码请用评论里的,这里的是老版本有bug

接下来是教程部分(该项目实际上是https://shangxueink.github.io/koishi-registry-aggregator/market.json 的镜像,因为github.io在国内不好访问,所以专门做个镜像)

本教程教你如何把 ksmirror 部署到 Cloudflare Workers,实现每 25 分钟自动拉取并缓存市场清单。

1. 准备工作

2. 创建项目目录

mkdir ksmirror && cd ksmirror

3. 复制文件

wrangler.toml

name = "ksmirror"
main = "index.js"
compatibility_date = "2024-05-01"
kv_namespaces = [
  { binding = "CACHE", id = "YOUR_KV_ID", preview_id = "YOUR_PREVIEW_KV_ID" }
]
[triggers]
crons = ["*/25 * * * *"]

替换 YOUR_KV_ID

index.js

export default {
  async fetch(req, env) {
    const url = new URL(req.url);
    if (url.pathname !== '/market.json') {
      return new Response('Not Found', { status: 404 });
    }
    try {
      // 1. 先看 KV 缓存
      const cached = await env.CACHE.get('market.json', 'text');
      if (cached) {
        return new Response(cached, {
          status: 200,
          headers: {
            'Content-Type': 'application/json',
            'Cache-Control': 'public, max-age=1500',
            'X-Cache': 'HIT'
          }
        });
      }
      // 2. 拉取上游
      const upstream = await fetch(
        'https://shangxueink.github.io/koishi-registry-aggregator/market.json',
        { cf: { cacheTtl: 0 } }
      );
      if (!upstream.ok) {
        return new Response(`Upstream ${upstream.status}`, { status: 502 });
      }
      // 3. 过滤非法 JSON 记录
      const raw = await upstream.text();
      const filtered = filterValidPlugins(raw);
      // 4. 写 KV 并返回
      const body = JSON.stringify(filtered, null, 2);
      await env.CACHE.put('market.json', body, { expirationTtl: 1500 });
      return new Response(body, {
        status: 200,
        headers: {
          'Content-Type': 'application/json',
          'Cache-Control': 'public, max-age=1500',
          'X-Cache': 'MISS'
        }
      });
    } catch (e) {
      return new Response('Internal Error: ' + e.message, { status: 500 });
    }
  },
  async scheduled(_, env) {
    try {
      const r = await fetch(
        'https://shangxueink.github.io/koishi-registry-aggregator/market.json',
        { cf: { cacheTtl: 0 } }
      );
      if (!r.ok) {
        console.error('Upstream', r.status);
        return;
      }
      const raw = await r.text();
      const filtered = filterValidPlugins(raw);
      await env.CACHE.put('market.json', JSON.stringify(filtered, null, 2), {
        expirationTtl: 1500
      });
      console.log('KV refreshed');
    } catch (e) {
      console.error('Scheduled error', e);
    }
  }
};
// ---------- 工具函数 ----------
function filterValidPlugins(rawText) {
  let arr;
  try {
    // 先整体 parse 一次
    arr = JSON.parse(rawText);
  } catch {
    // 整体都不是合法 JSON,直接返回空数组
    return [];
  }
  if (!Array.isArray(arr)) return [];
  // 逐条二次校验:能 stringify → parse 成功的才保留
  return arr.filter(item => {
    try {
      JSON.parse(JSON.stringify(item));
      return item && typeof item === 'object';
    } catch {
      return false;
    }
  });
}

4. 登录并创建 KV

wrangler login
wrangler kv namespace create CACHE

把返回的 id 填进 wrangler.toml。(preview_id 不用管

5. 部署

wrangler deploy

6. 绑定自定义域名(建议,防止被墙

  1. 在 Cloudflare Dashboard → Workers → ksmirror → Settings → Triggers → Custom Domains 添加 ksmirror.你的域名.顶级域名
  2. 或手动加一条 CNAME:
    ksmirror CNAME ksmirror.<your-account>.workers.dev

7. 验证(以我的源为例

curl https://ksmirror.fjgjkg.xyz/market.json

常见问题

  • 域名解析失败: 确保 DNS 记录已生效,等待 1-2 分钟。
  • Requests 超标: 提高浏览器缓存时间(如 max-age=7200)。
2 个赞

新版本的index.js
(感谢shangxueink大佬的项目
(感谢ai提供了大部分的代码编写,ai太好用辣! :grinning:

  • 由「拉取 shangxueink/market.json」改为 koishi-registry.yumetsuki.moe/index.json
  • 去重与隐藏过滤
    引入与 Node 脚本相同的 shouldHidePlugin 逻辑:
    ‑ 跳过 manifest.market.hidden / manifest.hidden / ignored === true 的插件
  • 增加即时刷新功能
    不加/market.json后缀时即时刷新插件列表,方便随时同步

index.js

// ========== 配置 ==========
const REGISTRY_URL = 'https://koishi-registry.yumetsuki.moe/index.json';

// ========== 工具函数 ==========
function shouldHidePlugin(p) {
  return (
    (p.manifest?.market?.hidden === true) ||
    (p.manifest?.hidden === true) ||
    (p.ignored === true)
  );
}

async function fetchRegistry() {
  const res = await fetch(REGISTRY_URL, { cf: { cacheTtl: 0 } });
  if (!res.ok) throw new Error(`Registry fetch failed: ${res.status}`);
  return res.json();
}

// 过滤并返回最终对象
async function buildMerged() {
  const data = await fetchRegistry();
  if (!Array.isArray(data.objects)) {
    throw new Error('Invalid registry format');
  }

  const list = data.objects.filter(p => !shouldHidePlugin(p));

  return {
    time: new Date().toUTCString(),
    total: list.length,
    version: 1,
    objects: list
  };
}

// ========== 路由 ==========
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(req) {
  const url = new URL(req.url);

  // 1. 根路径:强制刷新
  if (url.pathname === '/') {
    try {
      const merged = await buildMerged();
      const body   = JSON.stringify(merged, null, 2);
      await CACHE.put('market.json', body, { expirationTtl: 1500 });

      return new Response(
        `Registry refreshed at ${merged.time}\nTotal plugins: ${merged.total}`,
        { status: 200, headers: { 'Content-Type': 'text/plain; charset=utf-8' } }
      );
    } catch (e) {
      return new Response(`Refresh failed: ${e.message}`, { status: 500 });
    }
  }

  // 2. /market.json:缓存逻辑
  if (url.pathname === '/market.json') {
    try {
      const cached = await CACHE.get('market.json', 'text');
      if (cached) {
        return new Response(cached, {
          status: 200,
          headers: {
            'Content-Type': 'application/json',
            'Cache-Control': 'public, max-age=1500',
            'X-Cache': 'HIT'
          }
        });
      }

      const merged = await buildMerged();
      const body   = JSON.stringify(merged, null, 2);
      await CACHE.put('market.json', body, { expirationTtl: 1500 });

      return new Response(body, {
        status: 200,
        headers: {
          'Content-Type': 'application/json',
          'Cache-Control': 'public, max-age=1500',
          'X-Cache': 'MISS'
        }
      });
    } catch (e) {
      return new Response(`Internal Error: ${e.message}`, { status: 500 });
    }
  }

  // 3. 其他路径
  return new Response('Not Found', { status: 404 });
}

// ========== 定时任务 ==========
addEventListener('scheduled', event => {
  event.waitUntil(handleScheduled());
});

async function handleScheduled() {
  console.log('Scheduled sync started');
  try {
    const merged = await buildMerged();
    await CACHE.put('market.json', JSON.stringify(merged, null, 2), { expirationTtl: 1500 });
    console.log(`KV updated: ${merged.total} plugins`);
  } catch (e) {
    console.error('Scheduled sync failed', e);
  }
}

只用修改index.js的内容,其他都不用动

2 个赞

怎么给你找到了()

其实这个镜像源不怎么好,对已经在npm删库的插件没做验证。导致可能部分插件会装不了


推荐使用 → https://koishi-registry.yumetsuki.moe/index.json

2 个赞

感谢大佬指点,已更改同步源

2 个赞