标签 插件开发 下的文章

前几天看到: 让 GPT5.2 在 OpenCode 里说人话思路,我进行了尝试,他使用了 gemini-3-flash 对 GPT5.2 的思考块进行翻译,我用过之后,经常翻译超时,于是想到,能不能让 GPT5.2 自己翻译自己,基于这位佬友的代码进行了二开,效果嘎嘎好。效果如图:


安装:
1、复制本帖最下方代码,保存为 think-translator.ts(文件名随意)
2、打开 think-translator.ts
3、把 GPT5.2 的中转 API 信息写到 6~8 行(加了注释了,一眼就能看到)
4、把 think-translator.ts 放到 C:\Users\xxxx (你的名字).config\opencode\plugin 里,opencode 会自动加载
5、完事,开蹬

import type { Plugin } from "@opencode-ai/plugin";
import type { Message, Part } from "@opencode-ai/sdk";

declare function require(id: string): unknown;

// OpenAI 兼容接口(可用 /v1、/v1/chat/completions 或 /v1/responses 作为 baseURL)
const TRANSLATION_BASE_URL = "https://www.right.codes/codex/v1";// 填你的订阅 地址(示例是right code)
const TRANSLATION_API_KEY = ""; // 填你的订阅 Key
const TRANSLATION_MODEL_ID = "gpt-5.2"; // 可选:gpt-5.2 | gpt-5.2-low | gpt-5.2-medium

type PartEvent = Part & {
  time?: {
    end?: unknown;
  };
  text?: string;
};

type PatchResult = {
  response?: {
    status?: number;
  };
};

type ProviderConfig = {
  baseURL: string;
  apiKey: string;
  modelID: string;
};


const START_TAG = "[〔翻译开始〕]";
const END_TAG = "[〔翻译结束〕]";

const TITLE_TRANSLATION_PREFIX = "〔译: ";
const TITLE_TRANSLATION_SUFFIX = "〕";

function stripTranslationBlocks(text: string): string {
  while (true) {
    const start = text.indexOf(START_TAG);
    if (start === -1) return text;

    const end = text.indexOf(END_TAG, start + START_TAG.length);
    if (end === -1) return text.slice(0, start).trimEnd();

    const after = end + END_TAG.length;
    let restStart = after;
    while (restStart < text.length && text[restStart] === "\n") restStart += 1;

    const left = text.slice(0, start).trimEnd();
    const right = text.slice(restStart);
    text = left ? left + "\n\n" + right : right;
  }
}

function stripTitleTranslation(title: string): string {
  const start = title.indexOf(TITLE_TRANSLATION_PREFIX);
  if (start === -1) return title.trim();

  const end = title.indexOf(TITLE_TRANSLATION_SUFFIX, start + TITLE_TRANSLATION_PREFIX.length);
  if (end === -1) return title.slice(0, start).trim();

  return (title.slice(0, start) + title.slice(end + TITLE_TRANSLATION_SUFFIX.length)).trim();
}

function applyTitleTranslation(base: string, translation: string): string {
  if (!base.startsWith("**")) return base;
  const end = base.indexOf("**", 2);
  if (end === -1) return base;

  const rawTitle = base.slice(2, end);
  const title = stripTitleTranslation(rawTitle);
  if (!title) return base;

  const suffix = translation ? ` ${TITLE_TRANSLATION_PREFIX}${translation}${TITLE_TRANSLATION_SUFFIX}` : "";
  const combined = `**${title}${suffix}**`;
  return combined + base.slice(end + 2);
}

function extractTitleAndBody(base: string): { title: string; body: string } {
  if (!base.startsWith("**")) return { title: "", body: base };

  const end = base.indexOf("**", 2);
  if (end === -1) return { title: "", body: base };

  const rawTitle = base.slice(2, end);
  const title = stripTitleTranslation(rawTitle);
  const body = base.slice(end + 2).replace(/^\s+/, "");
  return { title, body };
}

function buildTranslationBlockPending(): string {
  return `${START_TAG}\n译文生成中…\n${END_TAG}`;
}

function buildTranslationBlockDone(bodyCn: string): string {
  const text = bodyCn.trim();
  return `${START_TAG}\n${text}\n${END_TAG}`;
}


function loadProviderConfigFromOpencodeConfig(_directory: string): ProviderConfig {
  return {
    baseURL: TRANSLATION_BASE_URL,
    apiKey: TRANSLATION_API_KEY,
    modelID: TRANSLATION_MODEL_ID,
  };
}

function extractSseTextParts(data: string): string {
  let parsed: unknown;
  try {
    parsed = JSON.parse(data);
  } catch {
    return "";
  }

  // OpenAI Chat Completions streaming: { choices: [{ delta: { content: "..." } }] }
  const chat = parsed as {
    choices?: Array<{
      delta?: { content?: string; text?: string };
      message?: { content?: string };
      text?: string;
    }>;
  };

  let out = "";
  for (const choice of chat.choices ?? []) {
    const d = choice.delta;
    if (d && typeof d.content === "string") out += d.content;
    else if (d && typeof d.text === "string") out += d.text;
    else if (choice.message && typeof choice.message.content === "string") out += choice.message.content;
    else if (typeof choice.text === "string") out += choice.text;
  }
  if (out) return out;

  // OpenAI Responses streaming (common proxy format): { type: "response.output_text.delta", delta: "..." }
  const resp = parsed as {
    type?: string;
    delta?: string;
    output_text?: string;
    text?: string;
    output?: Array<{
      content?: Array<{ type?: string; text?: string }>;
    }>;
    response?: {
      output?: Array<{
        content?: Array<{ type?: string; text?: string }>; // input_text / output_text
      }>;
    };
  };

  if (resp.type && typeof resp.delta === "string") return resp.delta;
  if (typeof resp.output_text === "string") return resp.output_text;
  if (resp.type && typeof resp.text === "string" && resp.type.endsWith(".delta")) return resp.text;

  // Non-stream JSON fallbacks (responses/chat) can also land here in some proxies.
  if (resp.response?.output) {
    let joined = "";
    for (const item of resp.response.output) {
      for (const c of item.content ?? []) {
        if (typeof c.text === "string") joined += c.text;
      }
    }
    if (joined) return joined;
  }

  if (resp.output) {
    let joined = "";
    for (const item of resp.output) {
      for (const c of item.content ?? []) {
        if (typeof c.text === "string") joined += c.text;
      }
    }
    if (joined) return joined;
  }

  return "";
}

type OpenAIEndpointKind = "chat.completions" | "responses";

function resolveOpenAIEndpoint(baseURL: string): { url: string; kind: OpenAIEndpointKind } {
  const base = baseURL.replace(/\/+$/, "");
  if (base.endsWith("/chat/completions")) return { url: base, kind: "chat.completions" };
  if (base.endsWith("/responses")) return { url: base, kind: "responses" };
  return { url: `${base}/chat/completions`, kind: "chat.completions" };
}

function buildTranslationPrompt(english: string): string {
  return (
    "You are a translation engine. Translate the English content to Simplified Chinese.\n" +
    "Rules (STRICT):\n" +
    "- Output ONLY the Chinese translation.\n" +
    "- No labels, no commentary.\n" +
    "- Preserve line breaks.\n" +
    "- Keep bullets as bullets.\n" +
    "- Do NOT omit or summarize any content.\n" +
    "\n" +
    "English:\n" +
    english
  );
}

async function* streamTextFromResponse(res: Response, signal?: AbortSignal): AsyncGenerator<string> {
  const contentType = res.headers.get("content-type") ?? "";
  if (contentType.includes("text/event-stream")) {
    for await (const chunk of streamSseTextParts(res, signal)) yield chunk;
    return;
  }

  if (!res.ok) {
    const msg = await res.text().catch(() => "");
    throw new Error(`http_${res.status}:${msg.slice(0, 160)}`);
  }

  const json = await res.json().catch(() => null);
  if (!json) return;

  // Reuse the same extractor for non-stream JSON.
  const text = extractSseTextParts(JSON.stringify(json));
  if (text) yield text;
}

async function* streamSseTextParts(response: Response, signal?: AbortSignal): AsyncGenerator<string> {
  if (!response.ok) {
    const msg = await response.text().catch(() => "");
    throw new Error(`http_${response.status}:${msg.slice(0, 160)}`);
  }

  const stream = response.body;
  if (!stream) return;

  const reader = stream.getReader();
  const decoder = new TextDecoder();

  let aborted = false;
  const abortError = () => new Error(String(signal?.reason ?? "aborted"));
  const onAbort = () => {
    aborted = true;
    try {
      reader.cancel();
    } catch {
      return;
    }
  };

  if (signal) {
    if (signal.aborted) onAbort();
    else signal.addEventListener("abort", onAbort, { once: true });
  }

  let buffer = "";

  const consume = function* () {
    while (true) {
      let sep = buffer.indexOf("\n\n");
      let advance = 2;
      if (sep === -1) {
        sep = buffer.indexOf("\r\n\r\n");
        advance = 4;
      }
      if (sep === -1) break;

      const chunk = buffer.slice(0, sep);
      buffer = buffer.slice(sep + advance);

      const lines = chunk.split(/\r?\n/);
      for (const line of lines) {
        const prefix = "data:";
        if (!line.startsWith(prefix)) continue;

        const data = line.slice(prefix.length).trim();
        if (!data) continue;
        if (data === "[DONE]") continue;

        const text = extractSseTextParts(data);
        if (text) yield text;
      }
    }
  };

  while (true) {
    if (aborted) throw abortError();
    const { done, value } = await reader.read();
    if (done) break;
    if (aborted) throw abortError();

    buffer += decoder.decode(value, { stream: true });
    for (const piece of consume()) yield piece;
  }

  if (aborted) throw abortError();
  buffer += "\n\n";
  for (const piece of consume()) yield piece;
}

function splitForTranslation(input: string): string[] {
  const text = input.replace(/\r\n/g, "\n").trim();
  if (!text) return [];

  const out: string[] = [];
  for (const para of text.split(/\n{2,}/g)) {
    const p = para.trim();
    if (!p) continue;

    const lines = p.split("\n");
    let buf: string[] = [];

    const flush = () => {
      const s = buf.join("\n").trim();
      if (s) out.push(s);
      buf = [];
    };

    for (const line of lines) {
      const l = line.trimEnd();
      if (/^\s*[-*•]\s+/.test(l) || /^\s*\d+\./.test(l)) {
        flush();
        out.push(l.trim());
      } else {
        buf.push(l);
      }
    }

    flush();
  }

  return out;
}

type StreamUpdate = {
  titleChunk?: string;
  bodyChunk?: string;
  done?: boolean;
};

async function translateViaOpenAIStream(
  cfg: ProviderConfig,
  input: string,
  signal?: AbortSignal
): Promise<AsyncGenerator<string>> {
  const { url, kind } = resolveOpenAIEndpoint(cfg.baseURL);
  const prompt = buildTranslationPrompt(input);

  const body =
    kind === "responses"
      ? {
          model: cfg.modelID,
          input: prompt,
          temperature: 0,
          max_output_tokens: 1024,
          stream: true,
        }
      : {
          model: cfg.modelID,
          messages: [
            { role: "system", content: "You are a translation engine." },
            { role: "user", content: prompt },
          ],
          temperature: 0,
          max_tokens: 1024,
          stream: true,
        };

  let res: Response;
  try {
    res = await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Accept: "text/event-stream",
        Authorization: `Bearer ${cfg.apiKey}`,
      },
      body: JSON.stringify(body),
      signal,
    });
  } catch (e) {
    if (signal?.aborted) throw new Error(String(signal.reason ?? "aborted"));
    throw e;
  }

  async function* gen(): AsyncGenerator<string> {
    for await (const chunk of streamTextFromResponse(res, signal)) yield chunk;
  }

  return gen();
}

function renderTranslationBlock(body: string): string {
  const content = body.trim() ? body : "译文生成中…";
  return `${START_TAG}\n${content}\n${END_TAG}`;
}

function shouldFlush(lastFlush: number): boolean {
  return Date.now() - lastFlush >= 300;
}

async function translateReasoningStreamed(
  cfg: ProviderConfig,
  titleEn: string,
  bodyEn: string,
  onUpdate: (u: { titleCn?: string; bodyCn?: string; done?: boolean }) => void,
  signal?: AbortSignal
): Promise<void> {
  let titleCn = "";
  let bodyCn = "";

  let lastFlush = 0;

  const flush = (done?: boolean) => {
    onUpdate({
      titleCn: titleCn ? titleCn : "译文生成中…",
      bodyCn: bodyCn ? bodyCn : "译文生成中…",
      done,
    });
    lastFlush = Date.now();
  };

  flush(false);

  const titleTask = (async () => {
    if (!titleEn.trim()) return;

    const titleStream = await translateViaOpenAIStream(cfg, titleEn, signal);
    for await (const chunk of titleStream) {
      titleCn += chunk;
      if (shouldFlush(lastFlush)) flush(false);
    }
  })();

  const bodyTask = (async () => {
    if (!bodyEn.trim()) return;

    const segments = splitForTranslation(bodyEn);
    for (let i = 0; i < segments.length; i += 1) {
      const seg = segments[i];
      const segStream = await translateViaOpenAIStream(cfg, seg, signal);
      let segOut = "";
      for await (const chunk of segStream) {
        segOut += chunk;
        if (shouldFlush(lastFlush)) {
          const combined = (bodyCn + (bodyCn ? "\n\n" : "") + segOut).trim();
          onUpdate({ titleCn: titleCn ? titleCn : "译文生成中…", bodyCn: combined, done: false });
          lastFlush = Date.now();
        }
      }
      if (segOut.trim()) {
        bodyCn = (bodyCn + (bodyCn ? "\n\n" : "") + segOut.trim()).trim();
      }
      flush(false);
    }
  })();

  await Promise.all([titleTask, bodyTask]);
  flush(true);
}

export const ThinkTranslator: Plugin = async ({ directory, client }) => {
  const providerCfg = loadProviderConfigFromOpencodeConfig(directory);

  const patch = (client as unknown as {
    _client?: {
      patch?: (opts: { url: string; body?: unknown }) => Promise<PatchResult>;
    };
  })._client?.patch;

  const lastAssistantMessageBySession = new Map<string, string>();
  const reasoningByPartID = new Map<string, string>();
  const patchedPartIDs = new Set<string>();

  const pendingPatchByPartID = new Map<string, { part: PartEvent; text: string }>();
  const patchInFlightByPartID = new Set<string>();

  const MAX_CONCURRENT_TRANSLATIONS = 3;
  const MAX_RETRIES = 5;
  const FIRST_TOKEN_TIMEOUT_MS = 10_000;
  const TOTAL_TIMEOUT_MS = 120_000;

  type Task = {
    part: PartEvent;
    base: string;
    title: string;
    body: string;
    createdAt: number;
    attempt: number;
    generation: number;
  };

  const queue: Task[] = [];
  let running = 0;

  const flushPatch = (partID: string) => {
    if (!patch) return;
    if (patchInFlightByPartID.has(partID)) return;

    const pending = pendingPatchByPartID.get(partID);
    if (!pending) return;

    pendingPatchByPartID.delete(partID);
    patchInFlightByPartID.add(partID);

    const url = `/session/${pending.part.sessionID}/message/${pending.part.messageID}/part/${partID}`;
    const body = { ...pending.part, text: pending.text };

    let timer: ReturnType<typeof setTimeout> | undefined;
    const timeout = new Promise<never>((_r, reject) => {
      timer = setTimeout(() => reject(new Error("patch.timeout")), 2000);
    });

    Promise.race([patch({ url, body }), timeout])
      .catch(() => {
        return;
      })
      .finally(() => {
        if (timer) clearTimeout(timer);
        patchInFlightByPartID.delete(partID);
        if (pendingPatchByPartID.has(partID)) flushPatch(partID);
      });
  };

  const enqueuePatch = (p: PartEvent, updatedText: string) => {
    if (!patch) return;

    const prev = pendingPatchByPartID.get(p.id);
    if (prev && prev.text === updatedText) return;

    pendingPatchByPartID.set(p.id, { part: p, text: updatedText });
    flushPatch(p.id);
  };

  const runQueue = () => {
    while (running < MAX_CONCURRENT_TRANSLATIONS && queue.length) {
      const task = queue.shift()!;
      running += 1;

      const { part, base, title, body, createdAt } = task;

      const startAttempt = (attempt: number, generation: number) => {
        let sawFirstToken = false;
        const attemptStartedAt = Date.now();

        let titleCn = "";
        let bodyCn = "";
        let lastFlush = 0;

        const update = (u: { titleCn?: string; bodyCn?: string; done?: boolean }) => {
          const nextTitleCn = u.titleCn ?? (sawFirstToken ? titleCn : "译文生成中…");
          const nextBodyCn = u.bodyCn ?? (sawFirstToken ? bodyCn : "译文生成中…");

          const nextBase = applyTitleTranslation(base, nextTitleCn);
          const nextBlock = renderTranslationBlock(nextBodyCn);
          const nextText = nextBase + "\n\n" + nextBlock + "\n";
          enqueuePatch(part, nextText);

          if (typeof u.titleCn === "string") titleCn = u.titleCn;
          if (typeof u.bodyCn === "string") bodyCn = u.bodyCn;
        };

        const controller = new AbortController();
        const attemptSignal = controller.signal;

        let firstTokenTimer: ReturnType<typeof setTimeout> | undefined;
        firstTokenTimer = setTimeout(() => {
          if (sawFirstToken) return;
          controller.abort("first_token_timeout");
        }, FIRST_TOKEN_TIMEOUT_MS);

        const totalTimer = setTimeout(() => {
          controller.abort("total_timeout");
        }, TOTAL_TIMEOUT_MS);

        const onToken = () => {
          if (sawFirstToken) return;
          sawFirstToken = true;
          if (firstTokenTimer) {
            clearTimeout(firstTokenTimer);
            firstTokenTimer = undefined;
          }
        };

        const cleanupTimers = () => {
          if (firstTokenTimer) {
            clearTimeout(firstTokenTimer);
            firstTokenTimer = undefined;
          }
          clearTimeout(totalTimer);
        };

        const done = (finalTitle: string, finalBody: string) => {
          cleanupTimers();
          update({ titleCn: finalTitle, bodyCn: finalBody, done: true });
        };

        const fail = (msg: string) => {
          cleanupTimers();
          update({ titleCn: msg, bodyCn: msg, done: true });
        };

        (async () => {
          try {
            if (!providerCfg.baseURL.trim() || !providerCfg.apiKey.trim() || !providerCfg.modelID.trim()) {
              fail("未配置翻译接口(baseURL/apiKey/modelID)");
              return;
            }

            update({ titleCn: "译文生成中…", bodyCn: "译文生成中…", done: false });

            await translateReasoningStreamed(providerCfg, title, body, (u) => {
              if ((u.titleCn && u.titleCn.trim()) || (u.bodyCn && u.bodyCn.trim())) onToken();
              const now = Date.now();
              if (now - lastFlush < 300 && !u.done) return;
              lastFlush = now;
              update(u);
            }, attemptSignal);

            done(titleCn || "", bodyCn || "");
          } catch (e) {
            const elapsed = Date.now() - createdAt;
            const nextAttempt = attempt + 1;
            const err = String(e);

            if (elapsed >= TOTAL_TIMEOUT_MS || err.includes("total_timeout")) {
              fail("翻译超时");
              return;
            }

            if (nextAttempt <= MAX_RETRIES) {
              setTimeout(() => startAttempt(nextAttempt, generation + 1), 300 * nextAttempt);
              return;
            }

            if (err.includes("http_")) {
              const code = err.match(/http_(\d{3})/)?.[1] ?? "";
              fail(code ? `翻译失败(HTTP ${code})` : "翻译失败");
              return;
            }

            if (err.includes("first_token_timeout")) {
              fail("翻译无响应");
              return;
            }

            fail("翻译失败");
          } finally {
            cleanupTimers();
            running -= 1;
            runQueue();
          }
        })();
      };

      startAttempt(task.attempt, task.generation);
    }
  };

  const enqueueTranslateAndPatch = (p: PartEvent, base: string) => {
    const { title, body } = extractTitleAndBody(base);

    queue.push({
      part: p,
      base,
      title,
      body,
      createdAt: Date.now(),
      attempt: 1,
      generation: 1,
    });

    runQueue();
  };

  return {
    "experimental.chat.messages.transform": async (_input, output) => {
      for (const m of output.messages ?? []) {
        if (m.info.role !== "assistant") continue;

        for (const p of m.parts ?? []) {
          if (p.type !== "text" && p.type !== "reasoning") continue;
          if (typeof p.text !== "string") continue;

          p.text = stripTranslationBlocks(p.text);
          if (p.type === "reasoning") p.text = stripTitleTranslation(p.text);
        }
      }
    },

    event: async ({ event }) => {
      if (event.type === "message.updated") {
        const info = (event.properties as { info: Message }).info;
        if (info.role === "assistant") lastAssistantMessageBySession.set(info.sessionID, info.id);
        return;
      }

      if (event.type !== "message.part.updated") return;

      const { part, delta } = event.properties as { part: Part; delta?: string };
      const p = part as unknown as PartEvent;

      const expectedMessageID = lastAssistantMessageBySession.get(p.sessionID);
      if (!expectedMessageID || p.messageID !== expectedMessageID) return;

      if (patchedPartIDs.has(p.id)) return;

      if (p.type === "reasoning" && typeof delta === "string" && delta.length) {
        const prev = reasoningByPartID.get(p.id) ?? "";
        reasoningByPartID.set(p.id, prev + delta);
      }

      if (p.type !== "reasoning" || !p.time?.end) return;

      const full = reasoningByPartID.get(p.id) ?? p.text ?? "";
      reasoningByPartID.delete(p.id);

      const base = stripTranslationBlocks(full).trimEnd();
      if (!base) return;

      const pendingBase = applyTitleTranslation(base, "标题译文生成中…");
      const pendingBlock = buildTranslationBlockPending();
      const pendingText = pendingBase + "\n\n" + pendingBlock + "\n";

      patchedPartIDs.add(p.id);
      enqueuePatch(p, pendingText);

      enqueueTranslateAndPatch(p, base);
    },
  };
};


📌 转载信息
原作者:
Tongz
转载时间:
2026/1/23 15:34:53

摘要

插件作为大模型与智能体突破原生能力边界、实现场景化功能落地的核心载体,是从 “通用 AI 能力” 到 “行业专属解决方案” 的关键桥梁。本文从插件的核心定义与价值出发,系统拆解大模型 / 智能体插件的底层工作逻辑,梳理从 0 到 1 的插件认知、选型、使用、定制全流程,详解不同场景下的插件搭配技巧与避坑指南,同时结合行业实操案例给出落地建议,并补充高频 QA 问答解决入门核心痛点,帮助零基础从业者快速掌握插件使用逻辑,实现大模型与智能体的能力最大化延伸,玩转插件生态的核心玩法。​关键词​:插件;大模型插件;智能体插件;AI 工具使用;从 0 到 1 学插件;插件定制;AI 能力延伸;智能体生态

一、插件的核心认知:大模型与智能体的 “能力扩展卡”

1.1 插件的定义与核心价值

插件是为大模型、智能体量身打造的​模块化功能扩展组件​,通过标准化接口与大模型 / 智能体核心系统对接,无需改变底层模型架构,即可快速为其新增专属功能、接入外部数据、实现跨平台联动。简单来说,大模型 / 智能体的原生能力是 “通用基础款”,而插件就是 “个性化拓展包”,让 AI 从 “能说会想” 升级为 “能做会干”。

其核心价值体现在三大维度:

  • 突破能力边界​:弥补大模型 “知识滞后、计算薄弱、无实操能力” 的短板,比如通过计算器插件解决数学运算、通过翻译插件实现多语种精准转换、通过数据分析插件完成数据可视化;
  • 适配场景落地​:针对办公、学习、研发、电商等不同场景,提供定制化功能,让通用 AI 适配专属需求,比如自媒体从业者用排版插件、程序员用代码调试插件、运营者用数据统计插件;
  • 降低使用门槛​:无需掌握 AI 开发技术,普通用户通过一键安装插件,即可让大模型 / 智能体具备专业能力,实现 “零代码玩转高阶 AI”。

1.2 插件与大模型、智能体的底层工作逻辑

插件与大模型 / 智能体的协作遵循 **“调用 - 执行 - 反馈”** 的闭环逻辑,核心分为三步,零基础也能轻松理解:

  1. 需求识别​:用户向大模型 / 智能体发出指令后,其核心系统先判断原生能力是否能满足,若无法满足则自动匹配已安装的对应插件;
  2. 插件调用​:核心系统通过标准化接口向插件发送执行指令,插件承接需求后完成专属处理(如数据计算、外部查询、功能执行);
  3. 结果反馈​:插件将处理结果回传给核心系统,由大模型 / 智能体整理成自然语言或可视化结果,反馈给用户。

整个过程毫秒级完成,用户感知不到底层调用逻辑,仅需发出自然语言指令,即可实现插件功能的无缝使用,这也是插件能快速普及的核心原因。

1.3 插件的核心分类:按功能与使用场景划分

目前主流的大模型 / 智能体插件生态,按功能属性可分为 6 大类,覆盖绝大多数日常与工作场景,零基础入门可先从高频通用类开始掌握:

插件分类核心功能典型代表适用人群 / 场景
通用工具类解决基础办公 / 学习需求计算器、翻译、思维导图、OCR全体用户,日常办公 / 学习
数据处理类数据统计、分析、可视化表格分析、数据可视化、SQL 查询运营、分析师、财务人员
内容创作类辅助内容生产、优化、排版文案润色、图文排版、字幕生成自媒体、文案、教师
研发开发类代码编写、调试、漏洞检测代码解释、Bug 修复、接口调试程序员、开发工程师
跨平台联动类实现 AI 与其他工具的无缝对接办公软件、云盘、思维导图工具全体用户,多工具协同办公
行业专属类适配特定行业的专业需求电商选品、医疗咨询、法律检索电商运营、医护、法律从业者

二、从 0 到 1:插件使用全流程,新手也能一步到位

2.1 第一步:选对平台 —— 插件生态的核心载体

插件的使用依赖于支持插件功能的大模型 / 智能体平台,零基础入门优先选择插件生态完善、操作门槛低、免费插件多的主流平台,避免因平台小众导致插件资源少、使用难度高,以下是目前最适合新手的三大主流平台,各有优势:

  1. 通用大模型平台​:ChatGPT(4o 及以上版本)、文心一言 4.0、讯飞星火 V4.0,插件生态完善,覆盖全品类插件,操作界面简洁,一键安装即可使用,适合全场景需求;
  2. 智能体专属平台​:Coze、LangChain Bot,主打智能体插件联动,支持插件自定义编排,适合需要多插件协同完成复杂任务的场景;
  3. 办公类 AI 平台​:WPS AI、飞书智谱,插件与办公软件深度融合,主打办公场景专属插件,适合职场办公人群。

新手建议​:优先从 ChatGPT 或文心一言入手,插件资源最丰富,操作最简洁,能快速完成从 0 到 1 的插件使用入门。

2.2 第二步:插件选型 —— 按需选择,拒绝盲目安装

插件并非越多越好,盲目安装大量插件会导致大模型 / 智能体响应变慢、匹配插件出错,零基础入门的核心原则是 **“刚需优先、少而精”**,按 “场景 - 需求 - 插件” 的逻辑选型,具体步骤:

  1. 明确使用场景​:确定自己使用大模型 / 智能体的核心场景,比如是日常办公、自媒体创作,还是编程开发;
  2. 梳理核心需求​:从场景中提炼需要解决的具体问题,比如办公场景需要 “PDF 解析、表格制作”,创作场景需要 “文案润色、图文排版”;
  3. 匹配对应插件​:根据需求选择功能精准的插件,比如 PDF 解析选专属 OCR 插件,文案润色选内容创作类插件,避免一个需求安装多个同类插件。

新手避坑​:同一功能的插件只需安装 1-2 个即可,比如翻译插件无需同时安装百度翻译、谷歌翻译、DeepL 翻译,选择适配自己使用习惯的一款即可。

2.3 第三步:基础使用 —— 一键安装,三步玩转核心功能

主流平台的插件操作均实现​可视化、零代码​,零基础用户无需掌握任何技术,只需三步即可完成插件的安装与使用,以通用大模型平台为例,操作流程高度统一:

  1. 插件市场入口​:登录平台后,在侧边栏或设置中找到「插件市场 / 应用中心」,这是所有插件的集中入口;
  2. 一键安装插件​:在插件市场中搜索需要的插件,点击「安装 / 启用」,平台会自动完成接口对接,安装完成后插件会出现在「已安装插件」列表中;
  3. 自然语言调用​:无需额外操作,直接向大模型 / 智能体发出自然语言指令,系统会自动匹配插件执行,比如安装了计算器插件后,直接说 “计算 10000 元按年化 3.5% 计息,存 5 年的复利是多少”,系统会自动调用插件计算并给出结果。

小技巧​:若系统未自动匹配插件,可在指令中明确提及插件名称,比如 “用思维导图插件把《从 0 到 1 玩转插件》的核心框架做成思维导图”,提升插件调用精准度。

2.4 第四步:进阶搭配 —— 多插件协同,实现复杂任务落地

当掌握单一插件的使用后,可通过​多插件协同搭配​,让大模型 / 智能体完成更复杂的场景化任务,这是 “玩转插件” 的核心进阶技巧。多插件搭配的核心逻辑是 **“按任务流程拆解,依次匹配插件”**,举 3 个高频场景的经典搭配案例,新手可直接照搬:

案例 1:办公场景 —— 快速完成一份市场分析报告

任务流程​:解析市场调研 PDF 数据 → 整理成表格 → 进行数据可视化 → 生成分析报告​插件搭配​:PDF 解析插件(OCR)+ 表格分析插件 + 数据可视化插件 + 文案创作插件​使用指令​:“用 PDF 解析插件提取这份市场调研文件的核心数据,用表格分析插件整理成销售数据表格,再用数据可视化插件生成柱状图,最后用文案创作插件基于数据生成一份 500 字的市场分析报告”

案例 2:创作场景 —— 打造一篇自媒体爆款推文

任务流程​:生成推文选题 → 撰写推文文案 → 优化排版 → 生成配图思路​插件搭配​:选题生成插件 + 文案润色插件 + 图文排版插件 + 创意设计插件​使用指令​:“用选题生成插件给美妆品类生成 3 个小红书爆款选题,选其中一个用文案润色插件撰写 800 字推文,用图文排版插件优化排版格式,最后用创意设计插件给出推文配图思路”

案例 3:研发场景 —— 快速调试一段 Python 代码

任务流程​:检查代码漏洞 → 修复 Bug→ 解释代码逻辑 → 生成注释​插件搭配​:代码检测插件 + Bug 修复插件 + 代码解释插件 + 注释生成插件​使用指令​:“用代码检测插件检查这段 Python 代码的漏洞,用 Bug 修复插件修正错误,用代码解释插件逐行说明逻辑,最后用注释生成插件为代码添加标准注释”

2.5 第五步:高阶定制 —— 打造专属插件,适配个性化需求

当现有插件无法满足专属需求时,可尝试​插件定制​,目前主流平台均提供​低代码 / 零代码插件定制工具​,零基础用户也能从 0 到 1 打造自己的专属插件,核心流程分为 4 步,无需掌握复杂开发技术:

  1. 明确定制需求​:确定专属插件的核心功能、使用场景、输入输出要求,比如定制一款 “电商商品标题优化插件”,核心功能是根据商品属性生成高点击率标题;
  2. 选择定制平台​:优先选择平台自带的插件定制工具,如 ChatGPT 的 Plugin Builder、文心一言的插件开发平台,无需对接复杂接口,可视化操作;
  3. 配置插件功能​:在定制工具中,通过拖拽、选择、填写参数的方式,配置插件的核心功能,比如为电商标题插件设置 “商品属性输入框、标题风格选择(简约 / 爆款 / 专业)、标题字数限制” 等;
  4. 测试与发布​:完成配置后,进行多次测试,验证插件功能是否符合预期,测试通过后即可发布到自己的插件列表,实现专属使用,部分平台还支持将定制插件分享到插件市场。

新手建议​:入门阶段先从简单功能插件开始定制,比如 “专属话术生成插件”“日常打卡插件”,熟悉定制逻辑后,再尝试复杂功能插件。

三、插件使用的核心避坑指南:新手少走 90% 的弯路

从 0 到 1 玩转插件,不仅要会用,更要会​避坑​,结合大量新手实操案例,梳理出 6 个最易踩的坑,以及对应的解决方案,帮新手快速避开误区:

3.1 坑 1:盲目安装大量插件,导致系统响应变慢

问题​:认为插件越多功能越全,安装数十个同类插件,导致大模型 / 智能体匹配插件时耗时增加,响应变慢,甚至出现插件冲突。​解决方案​:遵循 “​刚需安装、定期清理​” 原则,仅安装当前场景需要的插件,每 1-2 周清理一次未使用的插件,保持已安装插件列表简洁。

3.2 坑 2:忽略插件权限,导致数据安全风险

问题​:安装插件时随意授权,部分插件会请求访问用户的聊天记录、上传文件、个人数据等权限,导致数据泄露风险。​解决方案​:安装插件前​必看权限说明​,拒绝授权与插件功能无关的权限,比如一款计算器插件若请求访问聊天记录,直接拒绝安装;优先选择平台官方开发的插件,第三方插件需确认资质后再安装。

3.3 坑 3:指令描述模糊,导致插件调用失败

问题​:向大模型 / 智能体发出的指令过于模糊,系统无法准确匹配插件,比如只说 “帮我处理这份数据”,未说明具体处理需求。​解决方案​:指令描述遵循 **“场景 + 需求 + 插件”** 的三要素原则,明确告知系统要做什么、用什么插件做,比如 “在电商场景下,帮我用选品插件分析抖音美妆类目的爆款商品数据”。

3.4 坑 4:过度依赖插件,忽视大模型原生能力

问题​:任何需求都想通过插件解决,即使大模型原生能力能轻松完成,比如用翻译插件翻译简单的日常语句,反而增加操作成本。​解决方案​:先判断​需求是否需要插件​,大模型原生的自然语言理解、文案创作、逻辑分析等能力能解决的问题,无需调用插件,让插件成为 “补充能力” 而非 “唯一能力”。

3.5 坑 5:未及时更新插件,导致功能失效

问题​:插件安装后长期不更新,当大模型 / 智能体平台升级或插件底层功能调整时,出现插件调用失败、功能失效的问题。​解决方案​:开启插件的​自动更新功能​,或定期在插件市场检查已安装插件的更新状态,及时更新至最新版本,保证插件功能正常使用。

3.6 坑 6:多插件搭配逻辑混乱,导致任务执行出错

问题​:多插件协同时,未按任务流程合理搭配,插件顺序混乱,导致系统无法按预期执行,比如先让数据可视化插件生成图表,再让 PDF 解析插件提取数据。​解决方案​:多插件搭配前,先​梳理任务的先后流程​,按 “先输入、再处理、最后输出” 的逻辑匹配插件,确保插件调用顺序与任务流程一致,避免逻辑混乱。

四、插件生态的未来发展趋势:大模型与智能体的核心竞争力

插件作为大模型与智能体实现 **“能力落地、生态繁荣”** 的核心载体,其发展趋势与大模型、智能体的技术迭代深度绑定,未来三大发展方向值得关注,也是新手玩转插件需要提前布局的重点:

4.1 插件轻量化:零代码定制成为主流,全民可造插件

未来插件的开发门槛将持续降低,低代码 / 零代码定制工具将成为行业标配,不仅专业开发者能打造插件,普通用户也能通过简单的参数配置、功能拖拽,打造自己的专属插件,实现 “全民造插件” 的生态格局,插件将从 “专业产品” 变为 “个人化工具”。

4.2 插件智能化:智能体自主完成插件编排与适配

大模型与智能体的能力持续升级,未来将具备​自主插件编排能力​—— 用户只需发出核心需求,无需指定插件,智能体就能根据任务逻辑,自主选择、搭配、调用插件,完成复杂任务。比如用户说 “帮我完成一份年度销售总结”,智能体将自主匹配数据提取、表格分析、可视化、文案创作等插件,全程无需人工干预。

4.3 插件生态化:跨平台插件互联互通,形成全域能力网络

不同大模型、智能体平台的插件生态将从 “孤立发展” 走向 “互联互通”,通过标准化的插件接口,实现跨平台插件的无缝调用,比如在智能体平台可直接调用大模型的办公插件,在办公软件中可直接调用智能体的行业插件,形成覆盖全场景、跨平台的​插件能力网络​,让 AI 的能力延伸到每一个工作与生活场景。

五、行业高频 QA 问答

5.1 零基础新手,先从哪类插件开始学习使用最合适?

优先从通用工具类插件入手,比如计算器、翻译、PDF 解析、思维导图插件。这类插件功能简单、使用频率高、适配全场景,无需专业知识就能快速上手,能帮助新手快速熟悉插件的安装、调用逻辑,建立使用信心,掌握后再逐步拓展到内容创作、数据处理等专项插件。

5.2 免费插件和付费插件的区别是什么,新手需要付费购买插件吗?

核心区别在于​功能精准度、使用限制、专属服务​:免费插件能满足基础需求,部分存在使用次数、功能简化的限制;付费插件功能更精准、无使用限制,部分还提供专属售后与定制化优化。新手​无需过早付费​,目前主流平台的免费插件已能覆盖 90% 的日常与工作需求,建议先通过免费插件掌握使用逻辑,当现有免费插件无法满足核心工作需求时,再针对性购买付费插件。

5.3 不同大模型 / 智能体平台的插件可以互通使用吗?

目前多数平台的插件​暂不支持直接互通​,因各平台的插件接口标准、底层逻辑存在差异,比如 ChatGPT 的插件无法直接在文心一言中使用。但未来随着行业标准化推进,跨平台插件互联互通将成为趋势,现阶段若需要在多个平台使用同类功能,可在各平台分别安装对应的同款或同类插件。

5.4 安装插件后,大模型 / 智能体的响应速度变慢,该怎么解决?

可按以下步骤逐一排查解决:1. 清理未使用的插件,卸载同类冗余插件,减少插件匹配压力;2. 检查插件是否为最新版本,及时更新失效插件;3. 若使用多插件协同,尝试拆分任务,单次仅调用 1-2 个插件,避免同时调用大量插件;4. 关闭平台后台无关程序,保证网络通畅,提升接口传输速度。

5.5 如何判断一款插件是否适合自己,有没有核心筛选标准?

核心筛选标准有 4 点:1. ​功能匹配​:插件核心功能与自己的核心需求高度契合,无多余无效功能;2. ​操作简单​:零基础能快速上手,无需复杂的参数配置;3. ​权限安全​:插件请求的权限与功能匹配,无过度授权;4. ​口碑良好​:在插件市场中评分高、评论正面,官方开发或第三方资质可靠,避免使用小众无资质插件。

5.6 新手可以尝试开发插件吗,需要掌握哪些基础技能?

新手可以尝试,目前主流平台的​低代码 / 零代码定制工具​,让零基础用户也能开发简单插件,无需掌握复杂的编程技术。若仅做基础定制,只需掌握​需求梳理能力​,能明确插件的功能、使用场景即可;若想开发更复杂的插件,可逐步学习简单的编程基础(如 Python)、API 接口知识,提升定制能力。

六、结论

从 0 到 1 玩转插件,本质是掌握​大模型与智能体的能力延伸逻辑​—— 插件并非简单的 “功能工具”,而是让通用 AI 技术落地到具体场景的核心桥梁。对于零基础从业者而言,无需畏惧技术门槛,从核心认知入手,按 “选型 - 安装 - 使用 - 搭配 - 定制” 的全流程逐步推进,避开盲目安装、权限泄露、指令模糊等核心误区,就能快速掌握插件的核心玩法。

插件生态的发展,正推动大模型与智能体从 “通用能力” 向 “个性化、场景化能力” 升级,未来随着插件的轻量化、智能化、生态化发展,插件将成为每一个 AI 使用者的必备工具。新手只需从当下开始,从一款插件、一个场景入手,边用边学、边练边进阶,就能在插件生态中找到适合自己的使用方法,真正实现 “玩转插件”,让大模型与智能体成为工作与生活的高效助手,释放 AI 的最大价值。

参考文献

[1] 斯坦福大学. AI 指数报告 2026 [R]. 斯坦福大学人类与人工智能研究院,2026.
[2] 中国人工智能产业发展联盟。大模型插件生态建设与应用指南 2026 [R]. 2026.
[3] 字节跳动 AI 实验室. Coze 智能体插件平台开发与使用手册 2026 [R]. 2026.
[4] OpenAI 官方文档. ChatGPT Plugin 开发与使用指南 [Z]. 2026.
[5] 百度 AI 研究院。文心一言插件生态与场景落地实践 2026 [R]. 2026.
[6] 知乎科技研究院. 2026 年 AI 插件使用行为分析报告 [R]. 2026.

Finger - Burp Suite 自动化指纹识别与主动探测插件
Finger 是一款专为 Burp Suite 打造的模块化指纹识别插件,旨在通过被动流量监控与智能主动探测,快速识别目标站点的技术栈(CMS、框架、中间件、API 接口及敏感路径泄露)。

指纹识别.png


https://github.com/238469/burp-finger
🚀 核心特性

中英文双语支持:内置完善的国际化(I18n)机制,支持中英文界面动态切换,满足不同用户需求。
双模识别:支持被动流量匹配与主动路径探测。
在线规则更新:支持从 GitHub/Gitee 或自定义 URL 实时更新指纹库,并具备“智能合并”功能,在更新官方库的同时保留用户自定义规则。
正则匹配引擎:在传统的字符串匹配基础上,新增正则表达式支持,可对 Header 和 Body 进行更精细的特征提取。
风险提示功能:指纹规则支持 description 描述字段,在识别出指纹的同时,可直接提示关联的敏感路径或潜在漏洞风险。
智能去重机制:
请求级去重:同一 URL 在单次会话中仅进行一次匹配,避免重复扫描。
展示级去重:同一 URL 下的重复指纹会自动压缩显示,保持界面整洁。
智能递归探测:支持多级目录递归扫描,自动向上追溯父级目录,深度可调(0-N)。
精准规则引擎:

规则配置.png


支持 header、body、status、hash (Favicon MurmurHash3/MD5) 多维度匹配。
支持多关键字 AND 逻辑。
支持状态码强校验。
Nuclei 集成:识别指纹后自动生成 Nuclei 扫描标签(Tags),支持右键一键生成 Nuclei 扫描命令。
规则管理:内置 UI 管理界面,支持主动/被动规则过滤、指纹搜索、在线更新、规则导出/导入。
高性能与可配置性:
采用多线程执行、限流保护(Rate Limiting)。
系统配置面板:支持动态调整线程数、每秒发包数(RPS)。
智能过滤:支持全局配置状态码黑名单(如 404, 403)及响应体关键字黑名单,减少无效匹配。
丰富的指纹库:内置 2000+ 指纹规则,覆盖 Spring Boot Actuator、Swagger、GraphQL、常见备份文件、.git/.svn 泄露、主流 Web 编辑器、编程语言、前端框架等。
🛠️ 安装方法

编译项目:
mvn clean package -DskipTests
加载插件:
打开 Burp Suite -> Extensions -> Installed -> Add。
选择 target/finger-1.0-SNAPSHOT-jar-with-dependencies.jar。
配置:
插件加载后会自动在 rules/ 目录下搜索 fingerprints.json。
首次使用建议进入 System Config 标签页配置合适的线程数和过滤规则。

📖 使用指南
1 被动识别
插件会自动监听 Proxy 流量,实时识别经过的所有请求和响应中的技术指纹。识别结果展示在 Finger 标签页的表格中。
2 主动探测
开启/关闭:在 Finger 标签页勾选 Enable Active Scan。
探测深度:通过 Scan Depth 调整递归深度。
0: 仅探测根目录。
1: 探测根目录及当前请求的一级父目录。
触发机制:当正常浏览网页时,插件会根据当前 URL 自动触发对相关敏感路径(如 /actuator/env, .git/HEAD 等)的探测。
3 系统配置 (System Config)

系统配置.png


并发控制:动态设置 Thread Count(建议 10-50)和 RPS(建议 10-100)。
全局过滤:
Exclude Status Codes: 设置不参与匹配的状态码列表(逗号或换行分隔)。
Exclude Body Keywords: 设置包含特定内容时跳过匹配的关键字列表(换行分隔)。 4 Nuclei 集成

nuclei联动.png


在识别结果表格中点击右键,选择 Copy Nuclei Scan Command。
插件会自动根据当前识别到的所有指纹名称,格式化为 Nuclei 的 tags(如 spring-boot,nginx),生成完整的命令行。 5规则管理
内置规则管理器,支持指纹搜索、在线更新、导出自定义规则。

OpenCode 竟然没有 “任务完成 / 请求权限 / 运行失败时发出通知” 这个功能。mohak34/opencode-notifier 这个插件倒是可以实现,但是最近的版本这个插件会导致 OpenCode 自带的 bun 发生 Segmentation fault,运行两步就崩溃。

于是我原汤化原食自己 vibe 了一个,把压缩包里的文件放在 ~/.config/opencode/plugins 里即可。

session-notify.zip

可以在 47 行配置是否弹出通知以及是否发出声音,一般只推荐开其中一个。Windows 可用,Linux 和 MacOS 我没测试,大概也可用,Maybe。


📌 转载信息
原作者:
MUTED64
转载时间:
2026/1/19 18:30:21

简介:CCometixLine (ccline) 是对于 claude code 显示一个状态栏信息的工具,由于原作者不考虑 cliproxyapi 相关的 PR, 所以自行 fork 并添加,后续如原仓库有重要 / 安全更新本 fork 会尽快合并更新.


较原仓库添加功能:

  1. 模型别名 (通用)
  2. cliproxyapi 额度显示 (当前只做了 opus,3pro,3flash), 每个模型都能设置别名 / 颜色


效果图:


fork 仓库:


使用方法:由于还没有配置 NPM, 只能手动安装,

  1. 发现严重问题,没有配置 cliproxykey 的管理 key 的地方,请先不使用等更新,或暂时修改cliproxyapi管理的密钥为nbkey测试 (不建议)

  2. 安装官方版 (安装完要配置) GitHub - Haleclipse/CCometixLine: Claude Code statusline tool written in Rust

  3. 打开 fork 仓库

  4. release

  5. 选择对应你的系统版本下载

  6. exe 覆盖官方的安装位置 C:\Users\Administrator\.claude\ccline\ccline.exe

  7. 配置默认关闭需手动开启,

  8. 终端命令 ccline

  9. Configuration Mode

  10. segment 选择 CLI Proxy API Quota , 然后 enter 开启,s 保存 (必须)

  11. 可选,tab 选到 options 设置别名 / 颜色,s 保存


ps: 360 可能误报,我自己本地构建无误报,原仓库 action 自动构建有误报,如有担忧可自行 AI 审核构建

体检报告 (win64) :

https://www.virscan.org/report/e12d7b36366e6a47b9cd6c7c27295eff659659114676d513818d9238b5913eed


📌 转载信息
原作者:
kei233
转载时间:
2026/1/12 15:39:48

UniHub 是一个 插件化的本地桌面工具箱,通过安装插件来扩展功能,主打离线可用、数据不出本机、可折腾。

20260106154230_rec_-min

v1.1.0 更新要点:

  • 新增官方插件:
    • Excalidraw(本地绘图 / 白板)
    • Ctool(40+ 开发工具:加解密 / Hash / Base64 / 时间转换等)
    • TodoList(本地待办清单)
  • 新增系统应用扫描 + 全局搜索

插件开发很轻量:package.json + index.html → zip 即可安装。

项目还在早期阶段,欢迎 Star​ / Issue / PR / 插件共建 以及建议和使用反馈


📌 转载信息
原作者:
skylertong
转载时间:
2026/1/6 19:08:29



链接:
jarrodwatts/claude-hud: A Claude Code plugin that shows what’s happening - context usage, active tools, running agents, and todo progress

推特看到的,plugin 安装挺简单的,可以用来了解 cc 执行的情况,然后原项目中 win11 不支持可以按下面自己修改一下:

  • 找到配置文件 打开你的用户目录下的 .claude 文件夹,找到 settings.json 文件。 通常路径为:C:\Users\< 你的用户名 >\.claude\settings.json
  • 修改配置 在 settings.json 文件中添加或修改 statusLine 字段。请使用以下 JSON 配置,注意 将 <username> 替换为你实际的 Windows 用户名:
{
  "statusLine": {
    "type": "command",
    "command": "node C:\\Users\\<username>\\.claude\\plugins\\cache\\claude-hud\\claude-hud\\0.0.2\\dist\\index.js"
  }
}


一些详细功能说明:

Claude HUD 主要在终端底部显示以下四类核心信息,旨在让你实时掌握 Claude Code 的运行状态:

1. 会话基础信息 (Session Info)

这是第一行内容,展示当前环境和资源消耗情况。

  • 当前模型 :显示正在使用的模型名称(如 [Opus 4.5])。
  • 上下文健康度 (Context Health):通过可视化进度条(████░░)和百分比显示上下文窗口的使用量。颜色会随着使用量增加从绿色变为黄色,最后变红。
  • 细节 :当上下文使用率超过 85% 时,会额外显示具体的 token 使用量(输入 token 和缓存 token);超过 95% 时会显示 COMPACT 警告。
  • 配置统计 :显示已加载的 CLAUDE.md 文件数、规则 (rules)、MCP 工具和钩子 (hooks) 的数量。
  • 会话时长 :显示当前会话已运行的时间()。

2. 工具活动 (Tool Activity)

显示 Claude 正在使用或已经使用的工具,让你知道它是否 “卡住” 了或正在忙碌。

  • 正在运行的工具 :实时显示正在执行的工具名称及目标文件(例如 ◐ Glob: src/index.ts),让你看到它正在读取或编辑哪些文件。
  • 已完成工具统计 :聚合显示已执行完成的工具及其调用次数(例如 ✓ TaskOutput ×2 | ✓ Skill ×1)。

3. Agent 状态 (Agent Status)

追踪 Claude 内部派生的子智能体 (Sub-agents) 的行为。

  • 活动状态 :显示正在运行的 Agent 类型及其当前任务描述(例如 Explore: Explore home directory structure)。
  • 耗时监控 :显示每个 Agent 已经运行了多久,或者已完成 Agent 的总耗时。

4. 待办进度 (Todo Progress)

展示任务完成情况,直接读取 transcript 中的 todo 列表。

  • 当前任务 :显示正在进行中的具体待办事项(例如 ▸ Fix bugs in login flow)。
  • 总体进度 :以分数形式显示完成度(例如 (5/5)),全部完成后会显示绿色对勾。

通过这四行信息,将原本不可见的后台处理过程(如 Token 消耗、后台工具调用、子任务拆解)可视化,直接显示在你的输入框下方。


📌 转载信息
原作者:
lilililiwq
转载时间:
2026/1/5 13:02:30

之前用过 @shekohex 的 opencode-google-antigravity-auth@NoeFabris 的 opencode-antigravity-auth
两边的功能都想要,所以把它们合了。

主要功能:

  • google_search 工具集成(来自 @shekohex
  • CLI / Anti quota 支持,通过 opencode auth login 使用(来自 @NoeFabris
  • Google Antigravity OAuth 认证,支持自动刷新 token
  • 支持 gemini-3-pro-high、claude-opus-4-5-thinking 等模型
  • 以及两个插件原有的其他优秀功能

安装方式:

{ "plugin": ["opencode-antigravity-auth-remix@1.0.7"] } 

仓库地址:GitHub - Darkstarrd-dev/opencode-antigravity-auth

现在可以在 opencode 里爽用双重额度,真的是踩不完,完全踩不完

antigravity 反人类,回到 opencode 舒服太多


📌 转载信息
转载时间:
2026/1/5 12:14:55

各位佬友好,最近在折腾 NoneBot2 机器人的时候,发现有时候 LLM 或者其他插件可能会输出一些不该说的 “违禁词”,导致账号风控或者刷屏炸群。 为了解决这个问题,我先找了插件商店但是没有类似的词汇黑名单插件,我弄了一个主动审查插件 nonebot-plugin-word-censor,主要用于拦截机器人发出的消息。

项目地址 Github: https://github.com/ChlorophyTeio/nonebot-plugin-word-censor

目前插件已经上传并申请发布,商店发布检查结果已经通过。

安装插件后,机器人的处理将变为:收到 QQ 消息 → Nonebot 其他插件 → nonebot-plugin-word-censor → 发送 QQ 消息。

测试效果如图

原理主要是利用了 NoneBot 的 Bot.call_api 钩子机制。在 API 调用前,检查 data['message'] 字段。如果命中黑名单,则直接 Raise 一个 Mock 异常,欺骗上层调用者 “API 调用已完成” 或者直接中断,从而阻止请求发送到 OneBot 端。

这个审查插件比较简陋,后续会不断优化它,欢迎佬友们提 Issue 并给予意见。


📌 转载信息
原作者:
chlorophyimo
转载时间:
2026/1/4 10:08:43

Jetbrains家的KeyGen网上有很多,直接百度一下就能搜出来。
主要是ReSharper有联网验证,而且是.NET平台的软件,不能像Java一样用--javaagent直接进行注入。
这里给一个用Visual Studio拓展来解决的思路。

using HarmonyLib;
using System;

namespace ReSharperCrack
{
    [HarmonyPatch]
    internal static class Patches
    {
        static Patches()
        {
            try
            {
                var harmonyVersion = typeof(Harmony).Assembly.GetName().Version?.ToString() ?? "unknown";
                Logger.Log($"Patch loaded. Start. Harmony={harmonyVersion}, AppDomain={AppDomain.CurrentDomain.FriendlyName}");
            }
            catch (Exception ex)
            {
                Logger.Log($"Patch static ctor exception: {ex}");
            }
        }

        [HarmonyPatch("JetBrains.Application.License2.UserLicenses.UserLicenseViewSubmodel", "AddLicense")]
        [HarmonyPrefix]
        internal static bool AddLicense_Prefix(ref bool validateLicenseKey)
        {
            try
            {
                Logger.Log($"AddLicense_Prefix called. validateLicenseKey(before)={validateLicenseKey}");
                validateLicenseKey = false;
                Logger.Log($"AddLicense_Prefix finished. validateLicenseKey(after)={validateLicenseKey}");
                return true;
            }
            catch (Exception ex)
            {
                Logger.Log($"AddLicense_Prefix exception: {ex}");
                // be conservative: let original run if our prefix fails
                return true;
            }
        }

        [HarmonyPatch("JetBrains.Application.License2.NewLicenses.UserLicenseService", "VerifyCertificate")]
        [HarmonyPostfix]
        internal static void VerifyCertificate_Postfix(ref object __result)
        {
            try
            {
                Logger.Log($"VerifyCertificate_Postfix called. __result(before)={(__result == null ? "<null>" : __result + " (" + __result.GetType().FullName + ")")}");

                if ((int)__result != 0)
                {
                    __result = 0;
                }

                Logger.Log($"VerifyCertificate_Postfix finished. __result(after)={(__result == null ? "<null>" : __result + " (" + __result.GetType().FullName + ")")}");
            }
            catch (Exception ex)
            {
                Logger.Log($"VerifyCertificate_Postfix exception: {ex}");
            }
        }
    }
}

以及入口类:

/*...*/
[PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
[Guid(ReSharperCrackPackage.PackageGuidString)]
[ProvideAutoLoad(UIContextGuids80.NoSolution, PackageAutoLoadFlags.BackgroundLoad)]
public sealed class ReSharperCrackPackage : AsyncPackage
{
    protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
    {
        // 在这里加载LibHarmony的Hook
    }
}
/*...*/

编译成vsix拓展,装载在Visual Studio上。

接着,使用License Key激活就可以了。

ReSharper稳定破解

项目简介

开发了一个 Apache Answer 的微信 OAuth 登录插件,让你的 Answer 社区可以支持微信扫码登录。

GitHub: https://github.com/starvpn/answer-user-wxcom

✨ 主要特性

  • ? 标准 OAuth 2.0 协议
  • ? 中英文双语支持
  • ? 微信官方 Logo 样式
  • ⚙️ 配置简单(只需 AppID + AppSecret )
  • ? 内置 CSRF 防护和安全验证
  • ? 支持扫码登录和调用 PC 微信能力

? 一条命令安装

answer build \
--with github.com/starvpn/answer-user-wxcom \
--output ./new_answer

也支持 Docker 部署,可以和其他插件一起安装。

为什么做这个?

最近在用 Answer 搭建社区,发现国内用户更习惯微信登录。虽然 Answer 有 GitHub/Google 等登录方式,但对国内用户不太友好。于是按照 Answer 的插件机制开发了这个微信登录插件。

技术细节

  • 基于 Answer 1.3.0+ 的 Connector 插件机制
  • 遵循微信开放平台的网站应用接入规范
  • 实现了完整的 OAuth 2.0 授权码流程
  • 使用 State 参数防止 CSRF 攻击
  • 支持 i18n 国际化

适用场景

  • 国内的 Answer 问答社区
  • 需要微信登录的技术论坛
  • 企业内部知识库(配合企业微信)

大模型选择:从通用到专业的进阶之路

1. 通用全能型:ChatGPT系列

ChatGPT-4o和GPT-4在代码理解和生成方面表现卓越,特别适合:

  • 插件架构设计:能够理解Typecho的插件机制,提供合理的代码结构
  • PHP代码生成:准确生成符合Typecho规范的Hook挂载代码
  • 问题调试:快速分析代码错误,提供修复方案

提示词示例:

请基于Typecho 1.2版本,设计一个文章浏览量统计插件。需要实现:
1. 使用Widget_Archive_Render钩子
2. 创建独立的数据表存储浏览记录
3. 防止同一用户重复刷新计数
请给出完整的插件代码结构。

2. 代码专项型:GitHub Copilot & Claude

GitHub Copilot 在具体编码环节表现出色:

  • 实时代码补全,减少重复性编码工作
  • 智能生成Typecho特有的Hook调用方式
  • 提供多种实现方案的代码片段

Claude-3.5 Sonnet 在代码逻辑分析方面优势明显:

  • 深度理解复杂业务需求
  • 提供清晰的代码注释和文档
  • 擅长重构和优化现有代码

3. 国产精品:DeepSeek与通义千问

对于中文开发者,这些模型有独特优势:

  • 对中文注释和文档理解更精准
  • 符合国内开发习惯的代码风格
  • 免费或低成本使用,性价比高

实战案例:用AI开发Typecho插件

第一步:需求分析与架构设计

向AI提供清晰的Typecho版本信息和功能需求:

我需要为Typecho 1.2.1开发一个简单的SEO插件,功能包括:
- 自动生成文章meta描述
- 自定义文章关键词
- 生成XML网站地图
请设计插件目录结构和核心文件。

第二步:核心代码生成

针对具体功能模块,分段向AI请求代码:

请编写Plugin.php文件,实现以下Hook:
1. 使用Widget_Archive_Render在文章页输出meta标签
2. 使用Widget_Contents_Post_Edit在编辑页面添加SEO字段
3. 使用Action_Controller在后台添加管理菜单

第三步:调试与优化

遇到问题时,向AI提供错误信息和相关代码:

我的Typecho插件在挂载Widget_Archive_Render时出现500错误,相关代码是:
[你的代码]
错误信息是:Call to undefined method Typecho_Widget::getArchiveType()
请帮我分析和修复这个问题。

提升AI编码效果的关键技巧

1. 提供充分的上下文

  • Typecho具体版本号
  • PHP版本信息
  • 已安装的其他插件情况
  • 特殊需求或限制条件

2. 分步骤迭代开发

不要一次性要求完整插件,而是:

  • 先设计插件框架
  • 再实现核心功能
  • 逐步添加辅助功能
  • 最后进行测试优化

3. 善用代码审查

生成代码后,让AI自我审查:

请检查上面生成的代码:
1. 是否存在安全漏洞(如SQL注入、XSS攻击)
2. 是否符合Typecho编码规范
3. 是否有性能优化空间

注意事项与最佳实践

数据安全第一:AI生成的数据库操作代码需要严格验证,防止SQL注入

兼容性测试:务必在测试环境中验证插件兼容性,特别是Hook的触发时机

代码理解:不要直接复制粘贴,确保理解每一行代码的作用

版本控制:使用Git管理开发过程,便于回滚和追踪变更

结语

选择合适的AI助手,结合Typecho官方文档,能够显著提升插件开发效率。建议从简单插件开始,逐步积累经验。记住,AI是强大的辅助工具,但开发者的思考和验证同样重要。

希望这份指南能帮助您在Typecho插件开发道路上走得更远。如果您在具体实践中遇到问题,欢迎继续交流讨论!