Z ZPI 文档 v0.3.8
介绍页

源码

packages/desktop/src/main/custom-command-expansion.ts

typescript97 行查看原文
import type { CustomCommand } from "../shared/custom-commands.ts";

export function parseCustomCommandInvocation(text: string): { name: string; args: string } | undefined {
  const match = /^\/([a-z0-9_-][a-z0-9_:-]*(?:\/[a-z0-9_-][a-z0-9_:-]*)*)(?:\s+([\s\S]*))?$/i.exec(
    text.trim(),
  );
  if (!match || match[1].length > 1024) return;
  const name = match[1].toLowerCase().replaceAll(":", "/");
  if (["init", "compact", "side", "btw"].includes(name)) return;
  return { name, args: match[2]?.trim() ?? "" };
}

export function splitCustomCommandArguments(input: string): string[] {
  const args: string[] = [];
  let current = "";
  let quote: "'" | '"' | undefined;
  let escaping = false;
  let started = false;
  for (const char of input) {
    if (!quote && !escaping && /\s/.test(char)) {
      if (started) args.push(current);
      current = "";
      started = false;
      continue;
    }
    started = true;
    if (escaping) {
      current += char;
      escaping = false;
    } else if (char === "\\") escaping = true;
    else if (quote) {
      if (char === quote) quote = undefined;
      else current += char;
    } else if (char === "'" || char === '"') quote = char;
    else current += char;
  }
  if (escaping) current += "\\";
  if (started) args.push(current);
  return args;
}

export interface CustomCommandExpansionPlan {
  name: string;
  source: string;
  parts: ({ kind: "text"; text: string } | { kind: "shell"; command: string })[];
}

export function prepareCustomCommandPrompt(
  command: Pick<CustomCommand, "name" | "prompt">,
  rawArguments: string,
  source = "user/zpi",
): CustomCommandExpansionPlan {
  const name = command.name.replace(/^\/+/, "");
  const args = rawArguments.trim();
  const positional = splitCustomCommandArguments(args);
  let usedPlaceholder = false;
  // 只替换原模板,避免用户参数中的 $1 或 $ARGUMENTS 被再次解释而改写原文。
  const expand = (text: string) =>
    text.replace(/\$ARGUMENTS|\$(\d+)/g, (token, index: string | undefined) => {
      usedPlaceholder = true;
      return token === "$ARGUMENTS" ? args : (positional[Number(index) - 1] ?? "");
    });
  const parts: CustomCommandExpansionPlan["parts"] = [];
  // 先识别模板中的 Shell 段,再替换参数;用户参数中的 !`…` 不能新增执行动作。
  const pattern = /```!\s*\r?\n?([\s\S]*?)```|!`([^`]*)`/g;
  let cursor = 0;
  for (const match of command.prompt.matchAll(pattern)) {
    parts.push({ kind: "text", text: expand(command.prompt.slice(cursor, match.index)) });
    parts.push({ kind: "shell", command: expand((match[1] ?? match[2]).trim()) });
    cursor = match.index + match[0].length;
  }
  parts.push({ kind: "text", text: expand(command.prompt.slice(cursor)) });
  if (args && !usedPlaceholder) {
    const last = parts.at(-1);
    if (last?.kind === "text") last.text = last.text.trimEnd();
    parts.push({ kind: "text", text: `\n\nUser arguments:\n${args}` });
  }
  return { name, source, parts };
}

function formatPrompt(plan: CustomCommandExpansionPlan, body: string): string {
  return [`Run custom command /${plan.name}.`, `Command source: ${plan.source}.`, "", body.trim()].join("\n");
}

export async function expandCustomCommandPromptWithShell(
  command: Pick<CustomCommand, "name" | "prompt">,
  rawArguments: string,
  runShell: (command: string) => Promise<string>,
  source = "user/zpi",
): Promise<string> {
  const plan = prepareCustomCommandPrompt(command, rawArguments, source);
  let body = "";
  for (const part of plan.parts)
    body += part.kind === "text" ? part.text : part.command ? (await runShell(part.command)).trimEnd() : "";
  return formatPrompt(plan, body);
}