ساخت افزونه VS Code با هوش مصنوعی؛ آموزش کامل Extension با TypeScript، API درواره و فایل VSIX

در این آموزش یک افزونه هوش مصنوعی برای VS Code می‌سازیم که کد انتخاب‌شده را توضیح، بازنویسی و مستندسازی می‌کند، پاسخ Streaming می‌دهد و به فایل VSIX تبدیل می‌شود.

Share
ساخت افزونه VS Code با هوش مصنوعی؛ آموزش کامل Extension با TypeScript، API درواره و فایل VSIX

افزونه‌های Visual Studio Code به توسعه‌دهندگان اجازه می‌دهند ابزارهای اختصاصی خود را مستقیماً وارد محیط برنامه‌نویسی کنند. با ترکیب VS Code Extension API و مدل‌های هوش مصنوعی می‌توانید دستیار کدنویسی اختصاصی، ابزار توضیح کد، مولد مستندات، بازنویسی‌کننده کد یا تحلیل‌گر فایل بسازید.

در این آموزش، یک افزونه واقعی با TypeScript می‌سازیم که می‌تواند:

  • کد انتخاب‌شده را توضیح دهد.
  • برای کد پیشنهاد بازنویسی ارائه کند.
  • برای تابع یا کلاس مستندات تولید کند.
  • درباره فایل فعال سؤال بپرسد.
  • پاسخ مدل را به‌صورت Streaming نمایش دهد.
  • نتیجه را در یک سند Markdown جدید باز کند.
  • API Key کاربر را در Secret Storage نگه دارد.
  • از مدل انتخابی درواره استفاده کند.
  • به فایل قابل‌نصب VSIX تبدیل شود.

هدف این مقاله ساخت یک ابزار عملی است که بتوانید آن را توسعه دهید، روی VS Code خود نصب کنید یا به اعضای تیم بدهید.

افزونه هوش مصنوعی VS Code چگونه کار می‌کند؟

جریان اجرای افزونه ما به این صورت است:

  1. برنامه‌نویس بخشی از کد را انتخاب می‌کند.
  2. یکی از فرمان‌های افزونه را اجرا می‌کند.
  3. افزونه زبان فایل و کد انتخاب‌شده را می‌خواند.
  4. یک پرامپت ساختاریافته تولید می‌شود.
  5. درخواست به API درواره ارسال می‌شود.
  6. مدل پاسخ را به‌صورت Streaming برمی‌گرداند.
  7. Tokenهای پاسخ در Output Channel نمایش داده می‌شوند.
  8. نتیجه کامل در یک سند Markdown جدید باز می‌شود.

این معماری به افزونه اجازه می‌دهد بدون اجرای محلی مدل، از مدل‌های مختلف هوش مصنوعی استفاده کند.

با VS Code Extension API چه ابزارهایی می‌توان ساخت؟

VS Code Extension API مجموعه‌ای از رابط‌های JavaScript و TypeScript برای توسعه قابلیت‌های جدید در ویرایشگر است.

با استفاده از این API می‌توانید:

  • Command جدید بسازید.
  • به منوی کلیک راست گزینه اضافه کنید.
  • متن انتخاب‌شده را بخوانید.
  • فایل جدید ایجاد کنید.
  • محتوای Editor را ویرایش کنید.
  • Status Bar بسازید.
  • Sidebar یا Panel اضافه کنید.
  • Webview ایجاد کنید.
  • Diagnostic و Code Action نمایش دهید.
  • Completion Provider پیاده‌سازی کنید.
  • با Terminal و Workspace ارتباط داشته باشید.
  • تنظیمات افزونه را در اختیار کاربر قرار دهید.

مرجع کامل رابط‌ها در مستندات رسمی VS Code Extension API موجود است.

قابلیت‌های پروژه نهایی

افزونه‌ای که می‌سازیم چهار Command اصلی دارد:

Commandکاربرد
Explain Selected Codeتوضیح کد انتخاب‌شده
Refactor Selected Codeپیشنهاد بازنویسی و بهبود کد
Generate Documentationتولید مستندات برای کد
Ask About Current Fileپرسیدن سؤال درباره فایل فعال

دو Command مدیریتی نیز اضافه می‌کنیم:

Commandکاربرد
Set Darvareh API Keyذخیره API Key در Secret Storage
Clear Darvareh API Keyحذف API Key ذخیره‌شده

چرا API Key را در تنظیمات عادی ذخیره نمی‌کنیم؟

این روش مناسب نیست:

{
  "darvarehAI.apiKey": "YOUR_DARVAREH_API_KEY"
}

فایل settings.json برای تنظیمات معمولی مناسب است، نه اطلاعات محرمانه.

VS Code در ExtensionContext یک Secret Storage ارائه می‌کند که برای ذخیره داده‌های حساس طراحی شده است. طبق مرجع رسمی VS Code API، SecretStorage اطلاعات را مستقل از Workspace و به‌شکل محافظت‌شده روی سیستم کاربر نگه می‌دارد.

در پروژه ما کاربر API Key شخصی خود را وارد می‌کند و افزونه آن را با این API ذخیره می‌کند:

await context.secrets.store(
  "darvareh.apiKey",
  apiKey
);

پیش‌نیازهای آموزش

برای انجام پروژه به موارد زیر نیاز دارید:

  • Visual Studio Code
  • Node.js نسخه LTS
  • npm
  • Git
  • آشنایی مقدماتی با TypeScript
  • حساب کاربری در درواره
  • API Key درواره
  • شناسه یک مدل مناسب برنامه‌نویسی

بررسی نسخه Node.js:

node --version

بررسی npm:

npm --version

اطلاعات API مورد استفاده:

Base URL:
https://api.darvareh.ir/v1

API Key:
YOUR_DARVAREH_API_KEY

Model ID:
YOUR_MODEL_ID

برای مشاهده مدل‌ها و قیمت‌های به‌روز به صفحه مدل‌های درواره مراجعه کنید.

ساخت پروژه Extension با Yeoman

VS Code ابزار رسمی ساخت قالب اولیه افزونه را ارائه می‌کند.

دستور زیر را اجرا کنید:

npx --package yo \
  --package generator-code \
  -- yo code

در Wizard گزینه‌های زیر را انتخاب کنید:

What type of extension do you want to create?
New Extension (TypeScript)

What's the name of your extension?
Darvareh AI Assistant

What's the identifier of your extension?
darvareh-ai-assistant

What's the description?
AI coding assistant powered by Darvareh API

Initialize a git repository?
Yes

Which bundler to use?
esbuild

Which package manager to use?
npm

استفاده از Yeoman و generator-code روش پیشنهادی در راهنمای رسمی ساخت اولین افزونه VS Code است.

وارد پوشه پروژه شوید:

cd darvareh-ai-assistant

وابستگی‌ها را نصب کنید:

npm install

پروژه را در VS Code باز کنید:

code .

ساختار پروژه

ساختار اولیه پروژه تقریباً به این شکل است:

darvareh-ai-assistant/
├── .vscode/
│   ├── extensions.json
│   ├── launch.json
│   ├── settings.json
│   └── tasks.json
├── src/
│   ├── extension.ts
│   ├── darvareh-client.ts
│   └── prompts.ts
├── .gitignore
├── .vscodeignore
├── CHANGELOG.md
├── package.json
├── README.md
├── tsconfig.json
└── esbuild.js

تنظیم Manifest افزونه در package.json

فایل package.json علاوه بر وابستگی‌ها، Manifest افزونه VS Code نیز محسوب می‌شود.

بخش‌های مهم آن را به شکل زیر تنظیم کنید:

{
  "name": "darvareh-ai-assistant",
  "displayName": "Darvareh AI Assistant",
  "description": "Explain, refactor and document code with AI models through Darvareh API",
  "version": "0.1.0",
  "publisher": "your-publisher-id",
  "engines": {
    "vscode": "^1.100.0"
  },
  "categories": [
    "Programming Languages",
    "Machine Learning",
    "Other"
  ],
  "keywords": [
    "ai",
    "artificial intelligence",
    "coding assistant",
    "code explanation",
    "refactoring",
    "darvareh"
  ],
  "activationEvents": [],
  "main": "./dist/extension.js",
  "contributes": {
    "commands": [
      {
        "command": "darvarehAI.setApiKey",
        "title": "Darvareh AI: Set API Key",
        "category": "Darvareh AI"
      },
      {
        "command": "darvarehAI.clearApiKey",
        "title": "Darvareh AI: Clear API Key",
        "category": "Darvareh AI"
      },
      {
        "command": "darvarehAI.explainSelection",
        "title": "Darvareh AI: Explain Selected Code",
        "category": "Darvareh AI"
      },
      {
        "command": "darvarehAI.refactorSelection",
        "title": "Darvareh AI: Refactor Selected Code",
        "category": "Darvareh AI"
      },
      {
        "command": "darvarehAI.generateDocs",
        "title": "Darvareh AI: Generate Documentation",
        "category": "Darvareh AI"
      },
      {
        "command": "darvarehAI.askCurrentFile",
        "title": "Darvareh AI: Ask About Current File",
        "category": "Darvareh AI"
      }
    ],
    "menus": {
      "editor/context": [
        {
          "command": "darvarehAI.explainSelection",
          "when": "editorHasSelection",
          "group": "navigation@10"
        },
        {
          "command": "darvarehAI.refactorSelection",
          "when": "editorHasSelection",
          "group": "navigation@11"
        },
        {
          "command": "darvarehAI.generateDocs",
          "when": "editorHasSelection",
          "group": "navigation@12"
        }
      ]
    },
    "configuration": {
      "title": "Darvareh AI",
      "properties": {
        "darvarehAI.model": {
          "type": "string",
          "default": "YOUR_MODEL_ID",
          "description": "Model ID available in Darvareh"
        },
        "darvarehAI.temperature": {
          "type": "number",
          "default": 0.2,
          "minimum": 0,
          "maximum": 2,
          "description": "Controls response randomness"
        },
        "darvarehAI.maxTokens": {
          "type": "number",
          "default": 2000,
          "minimum": 100,
          "maximum": 16000,
          "description": "Maximum number of output tokens"
        },
        "darvarehAI.maxInputCharacters": {
          "type": "number",
          "default": 30000,
          "minimum": 1000,
          "maximum": 100000,
          "description": "Maximum number of input characters"
        }
      }
    }
  },
  "scripts": {
    "vscode:prepublish": "npm run package",
    "compile": "npm run check-types && npm run lint && node esbuild.js",
    "watch": "npm-run-all -p watch:*",
    "watch:esbuild": "node esbuild.js --watch",
    "watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
    "package": "npm run check-types && npm run lint && node esbuild.js --production",
    "check-types": "tsc --noEmit",
    "lint": "eslint src",
    "test": "vscode-test"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "@types/vscode": "^1.100.0",
    "@vscode/test-cli": "^0.0.10",
    "@vscode/test-electron": "^2.4.0",
    "esbuild": "^0.25.0",
    "eslint": "^9.0.0",
    "npm-run-all": "^4.1.5",
    "typescript": "^5.8.0"
  }
}

مقادیر نسخه در پروژه واقعی ممکن است با زمان نصب متفاوت باشند. فایل تولیدشده توسط Generator را مبنا قرار دهید و فقط بخش‌های contributes و تنظیمات موردنیاز را اضافه کنید.

مقدار زیر را با شناسه Publisher خود جایگزین کنید:

your-publisher-id

چرا Commandها را در package.json تعریف می‌کنیم؟

بخش contributes.commands به VS Code اعلام می‌کند که افزونه چه فرمان‌هایی ارائه می‌دهد.

برای مثال:

{
  "command": "darvarehAI.explainSelection",
  "title": "Darvareh AI: Explain Selected Code"
}

سپس در TypeScript همان شناسه ثبت می‌شود:

vscode.commands.registerCommand(
  "darvarehAI.explainSelection",
  handler
);

این دو شناسه باید دقیقاً یکسان باشند.

ساخت پرامپت‌های افزونه

فایل src/prompts.ts را ایجاد کنید:

export type TaskType =
  | "explain"
  | "refactor"
  | "documentation"
  | "ask";

export interface PromptInput {
  task: TaskType;
  code: string;
  languageId: string;
  fileName: string;
  question?: string;
}

export function buildPrompt(
  input: PromptInput
): string {
  switch (input.task) {
    case "explain":
      return buildExplainPrompt(input);

    case "refactor":
      return buildRefactorPrompt(input);

    case "documentation":
      return buildDocumentationPrompt(input);

    case "ask":
      return buildQuestionPrompt(input);

    default:
      return assertNever(input.task);
  }
}

function buildExplainPrompt(
  input: PromptInput
): string {
  return `
کد زیر را به‌صورت دقیق و مرحله‌به‌مرحله توضیح بده.

اطلاعات:
- زبان یا نوع فایل: ${input.languageId}
- نام فایل: ${input.fileName}

الزامات:
- هدف کلی کد را ابتدا توضیح بده.
- جریان اجرای کد را بررسی کن.
- توابع، کلاس‌ها و متغیرهای مهم را معرفی کن.
- نکات پیچیده را ساده‌سازی کن.
- اگر درباره بخشی مطمئن نیستی، آن را صریح اعلام کن.
- پاسخ را به زبان فارسی بنویس.
- نام APIها و اصطلاحات فنی را تغییر نده.

کد:
\`\`\`${input.languageId}
${input.code}
\`\`\`
`.trim();
}

function buildRefactorPrompt(
  input: PromptInput
): string {
  return `
کد زیر را بازبینی و Refactor کن.

اطلاعات:
- زبان یا نوع فایل: ${input.languageId}
- نام فایل: ${input.fileName}

الزامات:
- رفتار فعلی کد را حفظ کن.
- خوانایی و قابلیت نگه‌داری را بهبود بده.
- تغییرات غیرضروری ایجاد نکن.
- ابتدا مشکلات و فرصت‌های بهبود را فهرست کن.
- سپس نسخه پیشنهادی کد را ارائه بده.
- بعد از کد، تغییرات مهم را توضیح بده.
- وابستگی جدید فقط در صورت ضرورت پیشنهاد شود.
- کد را مستقیماً در فایل اصلی اعمال نکن.

کد:
\`\`\`${input.languageId}
${input.code}
\`\`\`
`.trim();
}

function buildDocumentationPrompt(
  input: PromptInput
): string {
  return `
برای کد زیر مستندات فنی تولید کن.

اطلاعات:
- زبان یا نوع فایل: ${input.languageId}
- نام فایل: ${input.fileName}

الزامات:
- هدف کد را توضیح بده.
- ورودی‌ها و خروجی‌ها را مشخص کن.
- خطاها یا حالت‌های مهم را بنویس.
- یک نمونه استفاده ارائه بده.
- اگر زبان از Docstring یا Documentation Comment
  پشتیبانی می‌کند، نسخه قابل‌استفاده در کد را نیز بساز.
- پاسخ دقیق و بدون ادعای تأییدنشده باشد.

کد:
\`\`\`${input.languageId}
${input.code}
\`\`\`
`.trim();
}

function buildQuestionPrompt(
  input: PromptInput
): string {
  return `
فقط بر اساس فایل زیر به سؤال برنامه‌نویس پاسخ بده.

اطلاعات:
- زبان یا نوع فایل: ${input.languageId}
- نام فایل: ${input.fileName}

سؤال:
${input.question}

الزامات:
- پاسخ دقیق و فنی باشد.
- اگر اطلاعات کافی در فایل وجود ندارد، اعلام کن.
- بخشی از کد که پاسخ را پشتیبانی می‌کند مشخص کن.
- پاسخ را به زبان فارسی بنویس.

محتوای فایل:
\`\`\`${input.languageId}
${input.code}
\`\`\`
`.trim();
}

function assertNever(value: never): never {
  throw new Error(
    `Unsupported task: ${String(value)}`
  );
}

ساخت Client اتصال به درواره

فایل src/darvareh-client.ts:

import * as vscode from "vscode";

interface StreamEvent {
  choices?: Array<{
    delta?: {
      content?: string;
    };
  }>;
}

interface DarvarehClientOptions {
  secrets: vscode.SecretStorage;
}

export class DarvarehClient {
  private static readonly apiKeyName =
    "darvareh.apiKey";

  private static readonly baseUrl =
    "https://api.darvareh.ir/v1";

  private readonly secrets: vscode.SecretStorage;

  constructor(
    options: DarvarehClientOptions
  ) {
    this.secrets = options.secrets;
  }

  async setApiKey(
    apiKey: string
  ): Promise<void> {
    const cleaned = apiKey.trim();

    if (!cleaned) {
      throw new Error(
        "API Key cannot be empty."
      );
    }

    await this.secrets.store(
      DarvarehClient.apiKeyName,
      cleaned
    );
  }

  async clearApiKey(): Promise<void> {
    await this.secrets.delete(
      DarvarehClient.apiKeyName
    );
  }

  async hasApiKey(): Promise<boolean> {
    const apiKey = await this.getApiKey();
    return Boolean(apiKey);
  }

  async streamChat(
    prompt: string,
    onToken: (token: string) => void,
    signal?: AbortSignal
  ): Promise<string> {
    const apiKey = await this.getApiKey();

    if (!apiKey) {
      throw new Error(
        "API Key has not been configured."
      );
    }

    const configuration =
      vscode.workspace.getConfiguration(
        "darvarehAI"
      );

    const model = configuration.get<string>(
      "model",
      "YOUR_MODEL_ID"
    );

    if (
      !model ||
      model === "YOUR_MODEL_ID"
    ) {
      throw new Error(
        "Model ID has not been configured."
      );
    }

    const temperature =
      configuration.get<number>(
        "temperature",
        0.2
      );

    const maxTokens =
      configuration.get<number>(
        "maxTokens",
        2000
      );

    const response = await fetch(
      `${DarvarehClient.baseUrl}/chat/completions`,
      {
        method: "POST",
        headers: {
          "Authorization": `Bearer ${apiKey}`,
          "Content-Type": "application/json",
          "Accept": "text/event-stream"
        },
        body: JSON.stringify({
          model,
          messages: [
            {
              role: "system",
              content: [
                "تو یک دستیار برنامه‌نویسی دقیق هستی.",
                "کد را بدون حدس غیرضروری تحلیل کن.",
                "پاسخ را فارسی بنویس، اما نام APIها،",
                "کتابخانه‌ها و اصطلاحات فنی را حفظ کن."
              ].join(" ")
            },
            {
              role: "user",
              content: prompt
            }
          ],
          temperature,
          max_tokens: maxTokens,
          stream: true
        }),
        signal
      }
    );

    if (!response.ok) {
      const errorBody =
        await response.text();

      throw new Error(
        this.createHttpErrorMessage(
          response.status,
          errorBody
        )
      );
    }

    if (!response.body) {
      throw new Error(
        "Streaming response body is empty."
      );
    }

    const reader =
      response.body.getReader();

    const decoder =
      new TextDecoder();

    let buffer = "";
    let completeText = "";

    while (true) {
      const { value, done } =
        await reader.read();

      if (done) {
        break;
      }

      buffer += decoder.decode(value, {
        stream: true
      });

      const events =
        buffer.split(/\r?\n\r?\n/);

      buffer = events.pop() ?? "";

      for (const event of events) {
        const dataLines = event
          .split(/\r?\n/)
          .filter(line =>
            line.startsWith("data:")
          )
          .map(line =>
            line.slice(5).trim()
          );

        for (const data of dataLines) {
          if (!data) {
            continue;
          }

          if (data === "[DONE]") {
            return completeText;
          }

          let parsed: StreamEvent;

          try {
            parsed = JSON.parse(data)
              as StreamEvent;
          } catch {
            continue;
          }

          const token =
            parsed
              .choices?.[0]
              ?.delta
              ?.content;

          if (!token) {
            continue;
          }

          completeText += token;
          onToken(token);
        }
      }
    }

    return completeText;
  }

  private async getApiKey():
    Promise<string | undefined> {
    return this.secrets.get(
      DarvarehClient.apiKeyName
    );
  }

  private createHttpErrorMessage(
    status: number,
    body: string
  ): string {
    switch (status) {
      case 400:
        return (
          "درخواست API معتبر نیست. " +
          "Model ID و پارامترها را بررسی کنید."
        );

      case 401:
        return (
          "API Key معتبر نیست یا دسترسی رد شده است."
        );

      case 403:
        return (
          "اجازه استفاده از این مدل وجود ندارد."
        );

      case 404:
        return (
          "مدل یا Endpoint پیدا نشد."
        );

      case 429:
        return (
          "تعداد درخواست‌ها از محدودیت مجاز عبور کرده است."
        );

      default:
        return (
          `API error ${status}: ` +
          body.slice(0, 500)
        );
    }
  }
}

ساخت فایل اصلی افزونه

فایل src/extension.ts:

import * as path from "node:path";
import * as vscode from "vscode";

import {
  DarvarehClient
} from "./darvareh-client";

import {
  buildPrompt,
  TaskType
} from "./prompts";


let outputChannel:
  vscode.OutputChannel;


export function activate(
  context: vscode.ExtensionContext
): void {
  outputChannel =
    vscode.window.createOutputChannel(
      "Darvareh AI"
    );

  const client = new DarvarehClient({
    secrets: context.secrets
  });

  const statusBar =
    vscode.window.createStatusBarItem(
      vscode.StatusBarAlignment.Right,
      100
    );

  statusBar.text = "$(sparkle) Darvareh AI";
  statusBar.tooltip =
    "Open Darvareh AI commands";

  statusBar.command =
    "darvarehAI.explainSelection";

  statusBar.show();

  context.subscriptions.push(
    outputChannel,
    statusBar,

    vscode.commands.registerCommand(
      "darvarehAI.setApiKey",
      () => setApiKey(client)
    ),

    vscode.commands.registerCommand(
      "darvarehAI.clearApiKey",
      () => clearApiKey(client)
    ),

    vscode.commands.registerCommand(
      "darvarehAI.explainSelection",
      () => runSelectionTask(
        client,
        "explain"
      )
    ),

    vscode.commands.registerCommand(
      "darvarehAI.refactorSelection",
      () => runSelectionTask(
        client,
        "refactor"
      )
    ),

    vscode.commands.registerCommand(
      "darvarehAI.generateDocs",
      () => runSelectionTask(
        client,
        "documentation"
      )
    ),

    vscode.commands.registerCommand(
      "darvarehAI.askCurrentFile",
      () => askAboutCurrentFile(client)
    )
  );
}


async function setApiKey(
  client: DarvarehClient
): Promise<void> {
  const apiKey =
    await vscode.window.showInputBox({
      title: "Darvareh API Key",
      prompt:
        "API Key خود را وارد کنید",
      password: true,
      ignoreFocusOut: true,
      placeHolder:
        "YOUR_DARVAREH_API_KEY",
      validateInput: value => {
        if (!value.trim()) {
          return "API Key نمی‌تواند خالی باشد.";
        }

        return undefined;
      }
    });

  if (!apiKey) {
    return;
  }

  await client.setApiKey(apiKey);

  await vscode.window
    .showInformationMessage(
      "API Key درواره ذخیره شد."
    );
}


async function clearApiKey(
  client: DarvarehClient
): Promise<void> {
  const answer =
    await vscode.window
      .showWarningMessage(
        "API Key ذخیره‌شده حذف شود؟",
        {
          modal: true
        },
        "حذف"
      );

  if (answer !== "حذف") {
    return;
  }

  await client.clearApiKey();

  await vscode.window
    .showInformationMessage(
      "API Key حذف شد."
    );
}


async function runSelectionTask(
  client: DarvarehClient,
  task: Exclude<TaskType, "ask">
): Promise<void> {
  const editor =
    vscode.window.activeTextEditor;

  if (!editor) {
    await vscode.window.showWarningMessage(
      "هیچ فایل فعالی باز نیست."
    );
    return;
  }

  if (editor.selection.isEmpty) {
    await vscode.window.showWarningMessage(
      "ابتدا بخشی از کد را انتخاب کنید."
    );
    return;
  }

  const code = editor.document.getText(
    editor.selection
  );

  const inputLimit =
    getMaximumInputCharacters();

  if (code.length > inputLimit) {
    await vscode.window.showWarningMessage(
      `کد انتخاب‌شده بیشتر از ${inputLimit.toLocaleString()} کاراکتر است.`
    );
    return;
  }

  const prompt = buildPrompt({
    task,
    code,
    languageId:
      editor.document.languageId,
    fileName: path.basename(
      editor.document.fileName
    )
  });

  await executePrompt(
    client,
    prompt,
    getTaskTitle(task)
  );
}


async function askAboutCurrentFile(
  client: DarvarehClient
): Promise<void> {
  const editor =
    vscode.window.activeTextEditor;

  if (!editor) {
    await vscode.window.showWarningMessage(
      "هیچ فایل فعالی باز نیست."
    );
    return;
  }

  const question =
    await vscode.window.showInputBox({
      title:
        "Ask Darvareh AI About Current File",
      prompt:
        "سؤال خود را درباره فایل فعال بنویسید",
      ignoreFocusOut: true,
      validateInput: value => {
        if (!value.trim()) {
          return "سؤال نمی‌تواند خالی باشد.";
        }

        if (value.length > 1000) {
          return (
            "سؤال نباید بیشتر از " +
            "۱۰۰۰ کاراکتر باشد."
          );
        }

        return undefined;
      }
    });

  if (!question) {
    return;
  }

  const fileContent =
    editor.document.getText();

  const inputLimit =
    getMaximumInputCharacters();

  if (fileContent.length > inputLimit) {
    await vscode.window.showWarningMessage(
      [
        "فایل از محدودیت ورودی بزرگ‌تر است.",
        "بخشی از کد را انتخاب و از",
        "Explain Selected Code استفاده کنید."
      ].join(" ")
    );
    return;
  }

  const prompt = buildPrompt({
    task: "ask",
    code: fileContent,
    languageId:
      editor.document.languageId,
    fileName: path.basename(
      editor.document.fileName
    ),
    question
  });

  await executePrompt(
    client,
    prompt,
    "پاسخ درباره فایل"
  );
}


async function executePrompt(
  client: DarvarehClient,
  prompt: string,
  title: string
): Promise<void> {
  if (!(await client.hasApiKey())) {
    const action =
      await vscode.window
        .showWarningMessage(
          "ابتدا API Key درواره را تنظیم کنید.",
          "تنظیم API Key"
        );

    if (action === "تنظیم API Key") {
      await vscode.commands.executeCommand(
        "darvarehAI.setApiKey"
      );
    }

    return;
  }

  outputChannel.clear();
  outputChannel.show(true);

  outputChannel.appendLine(
    `${title}\n`
  );

  const abortController =
    new AbortController();

  try {
    const result =
      await vscode.window
        .withProgress(
          {
            location:
              vscode.ProgressLocation.Notification,
            title:
              `Darvareh AI: ${title}`,
            cancellable: true
          },
          async (
            progress,
            cancellationToken
          ) => {
            cancellationToken
              .onCancellationRequested(() => {
                abortController.abort();
              });

            progress.report({
              message:
                "در حال دریافت پاسخ..."
            });

            return client.streamChat(
              prompt,
              token => {
                outputChannel.append(token);
              },
              abortController.signal
            );
          }
        );

    if (!result.trim()) {
      throw new Error(
        "مدل پاسخ متنی تولید نکرد."
      );
    }

    await openMarkdownResult(
      title,
      result
    );
  } catch (error) {
    if (
      error instanceof Error &&
      error.name === "AbortError"
    ) {
      outputChannel.appendLine(
        "\n\nدرخواست توسط کاربر متوقف شد."
      );

      return;
    }

    const message =
      error instanceof Error
        ? error.message
        : "خطای پیش‌بینی‌نشده";

    outputChannel.appendLine(
      `\n\nخطا: ${message}`
    );

    await vscode.window.showErrorMessage(
      `Darvareh AI: ${message}`
    );
  }
}


async function openMarkdownResult(
  title: string,
  content: string
): Promise<void> {
  const document =
    await vscode.workspace
      .openTextDocument({
        language: "markdown",
        content:
          `# ${title}\n\n${content}`
      });

  await vscode.window.showTextDocument(
    document,
    {
      viewColumn:
        vscode.ViewColumn.Beside,
      preview: true,
      preserveFocus: false
    }
  );
}


function getMaximumInputCharacters():
  number {
  return vscode.workspace
    .getConfiguration("darvarehAI")
    .get<number>(
      "maxInputCharacters",
      30000
    );
}


function getTaskTitle(
  task: Exclude<TaskType, "ask">
): string {
  switch (task) {
    case "explain":
      return "توضیح کد";

    case "refactor":
      return "پیشنهاد Refactor";

    case "documentation":
      return "مستندات کد";
  }
}


export function deactivate(): void {
  outputChannel?.dispose();
}

چرا نتیجه را مستقیم روی فایل اعمال نکردیم؟

مدل هوش مصنوعی ممکن است:

  • بخشی از منطق کد را اشتباه برداشت کند.
  • وابستگی ناموجود پیشنهاد دهد.
  • رفتاری را ناخواسته تغییر دهد.
  • تست‌ها و قراردادهای پروژه را نداند.
  • بخشی از کد را حذف کند.

به همین دلیل، نسخه اول افزونه نتیجه Refactor را در یک سند Markdown جداگانه باز می‌کند. برنامه‌نویس می‌تواند تغییرات را بررسی و سپس بخش‌های موردنظر را اعمال کند.

در نسخه‌های بعدی می‌توانید:

  • Diff Editor باز کنید.
  • تغییرات را به‌صورت Patch نمایش دهید.
  • برای هر تغییر دکمه Accept و Reject بسازید.
  • فقط پس از تأیید کاربر فایل را ویرایش کنید.
  • پیش از اعمال تغییر، نسخه فعلی را نگه دارید.

تنظیم Model ID

در VS Code کلیدهای زیر را بزنید:

Ctrl + Shift + P

فرمان زیر را اجرا کنید:

Preferences: Open User Settings (JSON)

تنظیمات افزونه:

{
  "darvarehAI.model": "YOUR_MODEL_ID",
  "darvarehAI.temperature": 0.2,
  "darvarehAI.maxTokens": 2000,
  "darvarehAI.maxInputCharacters": 30000
}

مقدار YOUR_MODEL_ID را با شناسه مدل موجود در صفحه مدل‌های درواره جایگزین کنید.

اجرای افزونه در حالت Development

در پنجره اصلی VS Code کلید F5 را بزنید.

یک پنجره جدید با عنوان Extension Development Host باز می‌شود.

در پنجره جدید:

  1. یک پروژه برنامه‌نویسی باز کنید.
  2. بخشی از کد را انتخاب کنید.
  3. Ctrl + Shift + P را بزنید.
  4. عبارت Darvareh AI را جست‌وجو کنید.
  5. ابتدا Set API Key را اجرا کنید.
  6. سپس Explain Selected Code را اجرا کنید.

پاسخ هنگام تولید در پنل Output نمایش داده می‌شود و پس از تکمیل در یک سند Markdown باز خواهد شد.

اجرای Command از منوی کلیک راست

بخشی از کد را انتخاب و روی آن کلیک راست کنید.

گزینه‌های زیر نمایش داده می‌شوند:

Darvareh AI: Explain Selected Code
Darvareh AI: Refactor Selected Code
Darvareh AI: Generate Documentation

شرط زیر باعث می‌شود Commandها فقط هنگام وجود Selection ظاهر شوند:

"when": "editorHasSelection"

نحوه عملکرد Streaming

درخواست افزونه شامل مقدار زیر است:

{
  "stream": true
}

پاسخ در قالب SSE دریافت می‌شود:

data: {"choices":[{"delta":{"content":"این"}}]}

data: {"choices":[{"delta":{"content":" تابع"}}]}

data: [DONE]

Client هر رویداد را Parse می‌کند و مقدار content را به Output Channel اضافه می‌کند:

outputChannel.append(token);

Streaming باعث می‌شود کاربر برای مشاهده اولین بخش پاسخ منتظر تکمیل کل متن نماند.

توقف درخواست توسط کاربر

در withProgress گزینه زیر را فعال کردیم:

cancellable: true

با لغو Progress، AbortController درخواست Fetch را متوقف می‌کند:

cancellationToken
  .onCancellationRequested(() => {
    abortController.abort();
  });

این قابلیت برای فایل‌های بزرگ یا درخواست‌های طولانی مفید است.

افزودن میانبر صفحه‌کلید

در بخش contributes فایل package.json می‌توانید میانبر اضافه کنید:

"keybindings": [
  {
    "command": "darvarehAI.explainSelection",
    "key": "ctrl+alt+e",
    "mac": "cmd+alt+e",
    "when": "editorTextFocus && editorHasSelection"
  },
  {
    "command": "darvarehAI.refactorSelection",
    "key": "ctrl+alt+r",
    "mac": "cmd+alt+r",
    "when": "editorTextFocus && editorHasSelection"
  }
]

پس از تغییر package.json، Extension Development Host را Reload کنید.

افزودن Command تولید تست

به TaskType مقدار جدید اضافه کنید:

export type TaskType =
  | "explain"
  | "refactor"
  | "documentation"
  | "tests"
  | "ask";

پرامپت:

function buildTestPrompt(
  input: PromptInput
): string {
  return `
برای کد زیر تست مناسب تولید کن.

اطلاعات:
- زبان: ${input.languageId}
- فایل: ${input.fileName}

الزامات:
- از Test Framework رایج همان پروژه استفاده کن.
- حالت عادی و Edge Caseها را پوشش بده.
- وابستگی‌ها را مشخص کن.
- تست‌ها قابل‌اجرا باشند.
- فرضیات را صریح بنویس.

کد:
\`\`\`${input.languageId}
${input.code}
\`\`\`
`.trim();
}

سپس Command جدید را در package.json و extension.ts ثبت کنید.

افزودن انتخاب مدل از Quick Pick

به‌جای ویرایش دستی Settings، می‌توانید فهرست مدل‌ها را با Quick Pick نمایش دهید:

async function selectModel():
  Promise<void> {
  const models = [
    {
      label: "مدل سریع",
      description: "مناسب کارهای روزمره",
      id: "YOUR_MODEL_ID"
    },
    {
      label: "مدل دقیق",
      description: "مناسب تحلیل پیچیده",
      id: "YOUR_MODEL_ID"
    }
  ];

  const selected =
    await vscode.window.showQuickPick(
      models,
      {
        title:
          "انتخاب مدل هوش مصنوعی",
        matchOnDescription: true
      }
    );

  if (!selected) {
    return;
  }

  await vscode.workspace
    .getConfiguration("darvarehAI")
    .update(
      "model",
      selected.id,
      vscode.ConfigurationTarget.Global
    );
}

بهتر است شناسه مدل‌ها را بر اساس مدل‌های واقعی موجود در درواره تنظیم کنید و برای قیمت روز به صفحه مدل‌ها ارجاع دهید.

محدودکردن ورودی فایل

ارسال فایل بسیار بزرگ می‌تواند:

  • زمان پاسخ را افزایش دهد.
  • تعداد توکن ورودی را بالا ببرد.
  • هزینه بیشتری ایجاد کند.
  • از Context Window مدل عبور کند.
  • کیفیت تحلیل را کاهش دهد.

در افزونه محدودیت زیر را تعریف کردیم:

{
  "darvarehAI.maxInputCharacters": 30000
}

برای فایل‌های بزرگ بهتر است:

  • فقط تابع یا کلاس مرتبط ارسال شود.
  • Symbol فعلی از Document Symbol Provider دریافت شود.
  • Importهای مرتبط اضافه شوند.
  • فایل به بخش‌های کوچک تقسیم شود.
  • ابتدا خلاصه ساختاری فایل تولید شود.
  • فقط بخش‌های لازم وارد Context شوند.

انتخاب Context مناسب برای تحلیل کد

ارسال تمام Workspace به مدل معمولاً روش مناسبی نیست. Context باید بر اساس Task انتخاب شود.

TaskContext پیشنهادی
توضیح تابعتابع انتخاب‌شده و Typeهای مرتبط
Refactorتابع، Importها و Interfaceهای وابسته
تولید تستتابع، وابستگی‌ها و تست‌های موجود
تحلیل خطاStack Trace، فایل مرتبط و تنظیمات لازم
مستندسازیSymbol انتخاب‌شده و Signature
سؤال درباره فایلمحتوای فایل فعال
تحلیل پروژهساختار پوشه و فایل‌های منتخب

Context بیشتر همیشه به معنی پاسخ بهتر نیست. اطلاعات نامرتبط می‌تواند تمرکز مدل را کاهش دهد.

افزودن زبان و نام فایل به پرامپت

افزونه این اطلاعات را از Editor می‌خواند:

const languageId =
  editor.document.languageId;

const fileName = path.basename(
  editor.document.fileName
);

این داده‌ها به مدل کمک می‌کنند تشخیص دهد کد مربوط به چه زبان یا فایلی است.

برای مثال:

languageId: typescript
fileName: user.service.ts

مدل می‌تواند بر اساس این اطلاعات، پیشنهاد متناسب‌تری ارائه کند.

ساخت Output Channel

Output Channel برای نمایش پاسخ Streaming مناسب است:

const outputChannel =
  vscode.window.createOutputChannel(
    "Darvareh AI"
  );

نمایش پنل:

outputChannel.show(true);

افزودن Token:

outputChannel.append(token);

برای رابط کاربری پیشرفته‌تر می‌توانید از Webview یا Sidebar استفاده کنید. مستندات VS Code توصیه می‌کند Webview فقط زمانی استفاده شود که APIهای استاندارد رابط کاربری کافی نیستند؛ زیرا Webview منابع بیشتری مصرف می‌کند. جزئیات در راهنمای رسمی Webview API موجود است.

ساخت Sidebar برای چت

برای نسخه بعدی می‌توانید یک Webview View در Sidebar بسازید که شامل این بخش‌ها باشد:

  • فهرست پیام‌ها
  • فیلد ورود پرامپت
  • دکمه ارسال
  • انتخاب مدل
  • افزودن فایل فعال
  • افزودن Selection
  • توقف پاسخ
  • پاک‌کردن تاریخچه

ارتباط Webview و Extension باید با Message Passing انجام شود:

webview.postMessage({
  type: "token",
  value: token
});

دریافت پیام از Webview:

webview.onDidReceiveMessage(
  async message => {
    if (message.type === "sendPrompt") {
      // اجرای درخواست
    }
  }
);

بهتر است Webview مستقیماً API را فراخوانی نکند. درخواست شبکه در Extension Host اجرا شود و فقط نتیجه از طریق پیام به Webview برسد.

سازگاری با Remote SSH و Dev Containers

افزونه‌های VS Code می‌توانند روی سیستم محلی یا Extension Host راه دور اجرا شوند.

اگر کاربر پروژه را با این ابزارها باز کند:

  • Remote SSH
  • Dev Containers
  • WSL
  • GitHub Codespaces

ممکن است Extension در محیط Remote اجرا شود. APIهای استاندارد VS Code معمولاً این تفاوت را مدیریت می‌کنند، اما استفاده مستقیم از فایل‌های سیستم‌عامل، Processها یا مسیرهای محلی می‌تواند مشکل ایجاد کند.

افزونه این مقاله از APIهای استاندارد VS Code و Fetch استفاده می‌کند و وابستگی Native ندارد. بااین‌حال باید آن را در محیط‌های Remote نیز آزمایش کنید. جزئیات معماری در راهنمای رسمی Remote Extensions توضیح داده شده است.

کنترل هزینه افزونه

هر Command می‌تواند یک درخواست مدل ایجاد کند. برای مدیریت هزینه:

  • طول Selection را محدود کنید.
  • برای Task ساده مدل اقتصادی‌تر انتخاب کنید.
  • max_tokens را متناسب تنظیم کنید.
  • درخواست‌های لغوشده را متوقف کنید.
  • از ارسال کل Workspace خودداری کنید.
  • پاسخ‌های تکراری را Cache کنید.
  • مصرف هر Task را بررسی کنید.
  • مدل‌های مختلف را با مجموعه تست ثابت مقایسه کنید.
  • System Prompt را بی‌دلیل طولانی نکنید.

قیمت مدل‌ها ثابت نیست؛ بنابراین برای اطلاعات به‌روز به صفحه مدل‌های درواره مراجعه کنید.

انتخاب Temperature مناسب

کاربردTemperature پیشنهادی
توضیح کد0.1 تا 0.3
Refactor محافظه‌کارانه0.1 تا 0.3
تولید مستندات0.2 تا 0.4
تولید تست0.1 تا 0.4
ایده‌پردازی معماری0.4 تا 0.7
نام‌گذاری و پیشنهادهای خلاقانه0.6 تا 0.9

برای تغییر مقدار:

{
  "darvarehAI.temperature": 0.2
}

ثبت لاگ کنترل‌شده

یک Output Channel جداگانه برای Log بسازید:

const logChannel =
  vscode.window.createOutputChannel(
    "Darvareh AI Logs",
    {
      log: true
    }
  );

ثبت اطلاعات غیرحساس:

logChannel.info(
  `Task started: explain`
);

logChannel.info(
  `Input characters: ${code.length}`
);

API Key یا محتوای محرمانه پروژه را در Log ثبت نکنید.

مدیریت خطاهای رایج API

وضعیتپیام پیشنهادی
400مدل و پارامترها را بررسی کنید
401API Key معتبر نیست
403دسترسی به مدل مجاز نیست
404Model ID پیدا نشد
429محدودیت درخواست یا مصرف
500 تا 599خطای موقت سرویس
Timeoutپاسخ در زمان تعیین‌شده دریافت نشد
AbortErrorدرخواست توسط کاربر متوقف شد

پیام فنی کامل را می‌توانید در Log بنویسید، اما اعلان اصلی باید کوتاه و قابل‌فهم باشد.

نوشتن تست برای Prompt Builder

فایل src/test/prompts.test.ts:

import * as assert from "node:assert";

import {
  buildPrompt
} from "../prompts";


suite(
  "Prompt Builder",
  () => {
    test(
      "builds explanation prompt",
      () => {
        const prompt = buildPrompt({
          task: "explain",
          code:
            "function sum(a, b) { return a + b; }",
          languageId: "javascript",
          fileName: "math.js"
        });

        assert.ok(
          prompt.includes("math.js")
        );

        assert.ok(
          prompt.includes("javascript")
        );

        assert.ok(
          prompt.includes("function sum")
        );
      }
    );

    test(
      "includes user question",
      () => {
        const prompt = buildPrompt({
          task: "ask",
          code:
            "export const port = 3000;",
          languageId: "typescript",
          fileName: "config.ts",
          question:
            "مقدار پورت چیست؟"
        });

        assert.ok(
          prompt.includes(
            "مقدار پورت چیست؟"
          )
        );
      }
    );
  }
);

تست‌ها نباید در هر بار اجرا به API واقعی متصل شوند. Client را Mock و فقط تعداد محدودی Integration Test واقعی اجرا کنید.

ارزیابی کیفیت افزونه

یک مجموعه کد آزمایشی تهیه کنید:

  • تابع کوتاه JavaScript
  • کلاس TypeScript
  • View در Django
  • Controller در ASP.NET Core
  • تابع Go
  • Query SQL
  • کد دارای خطای منطقی
  • کد طولانی
  • کد دارای Comment فارسی
  • فایل بدون Selection

معیارها:

  • پاسخ با زبان فایل سازگار باشد.
  • مدل کد غیرواقعی تولید نکند.
  • توضیح به بخش انتخاب‌شده مربوط باشد.
  • Refactor رفتار اصلی را تغییر ندهد.
  • مستندات ورودی و خروجی را درست تشخیص دهند.
  • زمان دریافت اولین Token قابل‌قبول باشد.
  • مصرف توکن از سقف عبور نکند.

آماده‌سازی README

فایل README.md باید حداقل شامل این بخش‌ها باشد:

# Darvareh AI Assistant

افزونه هوش مصنوعی برای توضیح، بازنویسی و مستندسازی کد.

## Features

- Explain selected code
- Refactor selected code
- Generate documentation
- Ask questions about current file
- Streaming responses
- Multiple AI models through Darvareh

## Setup

1. Install the extension.
2. Run `Darvareh AI: Set API Key`.
3. Set `darvarehAI.model` in VS Code Settings.
4. Select code and run a Darvareh AI command.

## Configuration

- `darvarehAI.model`
- `darvarehAI.temperature`
- `darvarehAI.maxTokens`
- `darvarehAI.maxInputCharacters`

برای Marketplace بهتر است Screenshot، نمونه Commandها، روش تنظیم API Key و فهرست تنظیمات نیز اضافه شوند.

تنظیم .vscodeignore

فایل .vscodeignore مشخص می‌کند چه فایل‌هایی وارد VSIX نشوند:

.vscode/**
.vscode-test/**
src/**
**/*.map
**/*.ts
tsconfig.json
eslint.config.mjs
esbuild.js
node_modules/**
.gitignore

اگر Bundler تمام وابستگی‌های Runtime را داخل dist/extension.js قرار دهد، می‌توانید node_modules را از VSIX حذف کنید.

قبل از انتشار، محتوای فایل VSIX را بررسی کنید تا فایل‌های موردنیاز Runtime حذف نشده باشند.

ساخت فایل VSIX

ابزار رسمی بسته‌بندی را اجرا کنید:

npx @vscode/vsce package

ابتدا Script مربوط به vscode:prepublish اجرا می‌شود و سپس فایل VSIX ساخته خواهد شد.

نمونه نام خروجی:

darvareh-ai-assistant-0.1.0.vsix

طبق مستندات رسمی انتشار افزونه VS Code، فایل VSIX برای آزمایش، توزیع خصوصی یا نصب افزونه بدون انتشار در Marketplace قابل‌استفاده است.

نصب فایل VSIX

نصب از رابط VS Code

  1. بخش Extensions را باز کنید.
  2. روی منوی سه‌نقطه کلیک کنید.
  3. گزینه Install from VSIX... را انتخاب کنید.
  4. فایل VSIX را انتخاب کنید.
  5. در صورت درخواست، VS Code را Reload کنید.

نصب با Command Line

code --install-extension \
  darvareh-ai-assistant-0.1.0.vsix

حذف افزونه:

code --uninstall-extension \
  your-publisher-id.darvareh-ai-assistant

بررسی محتوای VSIX

قبل از توزیع، Package را فهرست کنید:

npx @vscode/vsce ls

موارد زیر نباید وارد VSIX شوند:

  • .env
  • API Key
  • فایل‌های تست حجیم
  • اطلاعات داخلی پروژه
  • Source Map غیرضروری
  • پوشه Git
  • فایل‌های موقت
  • Logهای محلی

افزایش نسخه افزونه

قبل از ساخت نسخه جدید، مقدار version را در package.json تغییر دهید:

{
  "version": "0.1.1"
}

الگوی Semantic Versioning:

MAJOR.MINOR.PATCH

مثال:

  • 0.1.0: نسخه اولیه
  • 0.1.1: رفع خطا
  • 0.2.0: قابلیت جدید
  • 1.0.0: نسخه پایدار اصلی

تغییرات را در CHANGELOG.md ثبت کنید.

انتشار در Visual Studio Marketplace

فرایند کلی انتشار:

  1. یک Publisher در Visual Studio Marketplace ایجاد کنید.
  2. ابزار vsce را نصب یا با npx اجرا کنید.
  3. اطلاعات Publisher را در package.json قرار دهید.
  4. README، آیکون، License و Changelog را تکمیل کنید.
  5. Package را آزمایش کنید.
  6. افزونه را Publish کنید.

دستور انتشار:

npx @vscode/vsce publish

برای افزایش خودکار Patch:

npx @vscode/vsce publish patch

برای انتشار عمومی باید فرایند احراز هویت Marketplace و تنظیمات Publisher را مطابق مستندات رسمی انجام دهید.

افزودن آیکون افزونه

یک تصویر PNG با ابعاد مناسب در پروژه قرار دهید:

images/icon.png

در package.json:

{
  "icon": "images/icon.png"
}

آیکون باید در ابعاد کوچک واضح باشد و پس‌زمینه یا کنتراست مناسبی برای Marketplace داشته باشد.

افزودن قابلیت جایگزینی کد با تأیید کاربر

پس از دریافت Refactor می‌توانید یک Command جداگانه برای اعمال نتیجه بسازید؛ اما ابتدا باید خروجی مدل را از توضیحات جدا کنید.

راه بهتر استفاده از Structured Output است:

{
  "summary": "توضیح تغییرات",
  "replacement": "کد پیشنهادی",
  "warnings": []
}

پس از Parse و اعتبارسنجی، تغییر با WorkspaceEdit اعمال می‌شود:

const edit =
  new vscode.WorkspaceEdit();

edit.replace(
  editor.document.uri,
  editor.selection,
  replacementCode
);

const confirmation =
  await vscode.window.showWarningMessage(
    "کد انتخاب‌شده با نسخه پیشنهادی جایگزین شود؟",
    {
      modal: true
    },
    "اعمال تغییر"
  );

if (confirmation === "اعمال تغییر") {
  await vscode.workspace.applyEdit(edit);
}

این قابلیت را فقط پس از نمایش Diff، اعتبارسنجی خروجی و تأیید صریح کاربر اضافه کنید.

ایده‌های توسعه نسخه بعدی

می‌توانید افزونه را با قابلیت‌های زیر گسترش دهید:

  • Sidebar Chat
  • انتخاب چند فایل
  • تحلیل خطاهای Problems Panel
  • تولید Unit Test
  • تولید Commit Message
  • توضیح Git Diff
  • تولید Pull Request Description
  • تبدیل کد میان زبان‌ها
  • تولید Type و Interface
  • ساخت Regex
  • تولید SQL
  • تحلیل Log
  • تولید Documentation Comment
  • انتخاب مدل برای هر Task
  • نمایش میزان توکن
  • تاریخچه درخواست‌ها
  • کش پاسخ
  • Diff Viewer
  • Accept و Reject تغییرات
  • اتصال به RAG پروژه
  • ساخت Tool یا Agent اختصاصی

چک‌لیست آماده‌سازی افزونه

پیش از توزیع بررسی کنید:

  • تمام Commandها در package.json تعریف شده‌اند.
  • شناسه Commandها با کد TypeScript یکسان است.
  • API Key با Secret Storage ذخیره می‌شود.
  • هیچ کلید واقعی در Repository وجود ندارد.
  • Model ID قابل‌تنظیم است.
  • طول ورودی محدود شده است.
  • درخواست قابل‌لغو است.
  • Streaming درست کار می‌کند.
  • پاسخ در سند جداگانه نمایش داده می‌شود.
  • تغییر کد بدون تأیید انجام نمی‌شود.
  • خطاهای 400، 401، 404 و 429 مدیریت می‌شوند.
  • فایل بزرگ به‌طور کامل ارسال نمی‌شود.
  • Output Channel ایجاد شده است.
  • افزونه در Extension Development Host تست شده است.
  • Remote SSH و WSL بررسی شده‌اند.
  • README و Changelog تکمیل شده‌اند.
  • فایل‌های اضافی از VSIX حذف شده‌اند.
  • نسخه افزونه به‌روزرسانی شده است.
  • فایل VSIX روی یک VS Code دیگر آزمایش شده است.

خطاهای رایج

Command در Command Palette نمایش داده نمی‌شود

موارد زیر را بررسی کنید:

  • Command در contributes.commands وجود داشته باشد.
  • شناسه Command صحیح باشد.
  • Extension Development Host Reload شده باشد.
  • مقدار engines.vscode با نسخه نصب‌شده سازگار باشد.
  • خطای Compile وجود نداشته باشد.

خطای API Key has not been configured

Command زیر را اجرا کنید:

Darvareh AI: Set API Key

API Key در Secret Storage ذخیره می‌شود و نیازی به قراردادن آن در Settings نیست.

خطای Model ID has not been configured

در settings.json مقدار زیر را تنظیم کنید:

{
  "darvarehAI.model": "YOUR_MODEL_ID"
}

مدل را از صفحه مدل‌های درواره انتخاب کنید.

خطای 401

API Key را حذف و دوباره وارد کنید:

Darvareh AI: Clear API Key
Darvareh AI: Set API Key

پاسخ Streaming نمایش داده نمی‌شود

موارد زیر را بررسی کنید:

  • مدل از Streaming پشتیبانی کند.
  • مقدار stream برابر true باشد.
  • پاسخ دارای text/event-stream باشد.
  • ساختار SSE درست Parse شود.
  • Output Channel باز باشد.
  • Proxy یا شبکه پاسخ را Buffer نکند.

TypeScript تابع fetch را نمی‌شناسد

نسخه Node Types و Target پروژه را بررسی کنید:

npm install --save-dev @types/node@latest

در tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16"
  }
}

تنظیمات دقیق را با ساختار تولیدشده توسط Extension Generator هماهنگ کنید.

فایل VSIX ساخته نمی‌شود

ابتدا خطاهای TypeScript و Lint را بررسی کنید:

npm run check-types
npm run lint
npm run package

سپس:

npx @vscode/vsce package

افزونه در Remote SSH رفتار متفاوتی دارد

فرمان زیر را اجرا کنید:

Developer: Show Running Extensions

بررسی کنید Extension روی سیستم محلی اجرا شده یا Remote Extension Host. از مسیرهای ثابت سیستم‌عامل و وابستگی‌های Native غیرضروری استفاده نکنید.

پرسش‌های متداول

آیا می‌توان با TypeScript افزونه VS Code ساخت؟

بله. TypeScript روش پیشنهادی برای توسعه افزونه‌های VS Code است و Typeهای رسمی API تجربه توسعه بهتری فراهم می‌کنند.

آیا برای ساخت افزونه به React نیاز داریم؟

خیر. Command، Output Channel، Quick Pick و Editor API بدون React قابل‌استفاده‌اند. React بیشتر برای Webviewهای پیچیده کاربرد دارد.

فایل VSIX چیست؟

VSIX بسته قابل‌نصب افزونه VS Code است. می‌توانید آن را بدون انتشار در Marketplace روی VS Code نصب یا برای اعضای تیم ارسال کنید.

آیا API Key در فایل VSIX قرار می‌گیرد؟

در معماری این مقاله خیر. کاربر پس از نصب افزونه، API Key خود را وارد می‌کند و کلید با Secret Storage ذخیره می‌شود.

آیا می‌توان API درواره را مستقیماً از Extension فراخوانی کرد؟

بله. افزونه دسکتاپ در Extension Host اجرا می‌شود و کاربر می‌تواند API Key خودش را با Secret Storage نگه دارد. برای افزونه عمومی سازمانی می‌توانید یک Backend واسط و سیستم حساب کاربری نیز بسازید.

آیا افزونه می‌تواند فایل‌های پروژه را بخواند؟

با VS Code API می‌تواند فایل‌های Workspace را بخواند؛ اما بهتر است فقط Context لازم و قابل‌مشاهده برای کاربر ارسال شود.

آیا افزونه می‌تواند کد را خودکار تغییر دهد؟

بله، با WorkspaceEdit امکان ویرایش فایل وجود دارد؛ اما بهتر است تغییرات ابتدا در Diff نمایش داده و فقط با تأیید کاربر اعمال شوند.

آیا می‌توان پاسخ را Stream کرد؟

بله. در این آموزش پاسخ SSE به‌صورت Token‌به‌Token خوانده و در Output Channel نمایش داده می‌شود.

بهترین مدل برای افزونه برنامه‌نویسی چیست؟

انتخاب مدل به زبان برنامه‌نویسی، اندازه Context، دقت، سرعت و هزینه بستگی دارد. فهرست مدل‌ها و قیمت به‌روز در صفحه مدل‌های درواره قرار دارد.

چگونه هزینه افزونه را کاهش دهیم؟

فقط کد مرتبط را ارسال کنید، طول ورودی و خروجی را محدود کنید، برای Taskهای ساده مدل اقتصادی‌تر انتخاب کنید و از ارسال کل Workspace خودداری کنید.

آیا افزونه در Cursor یا سایر Forkهای VS Code کار می‌کند؟

بسیاری از افزونه‌های VS Code در ویرایشگرهای سازگار قابل‌نصب‌اند؛ اما سازگاری کامل به APIهای پشتیبانی‌شده همان ویرایشگر بستگی دارد و باید جداگانه آزمایش شود.

آیا می‌توان افزونه را فقط داخل شرکت توزیع کرد؟

بله. می‌توانید فایل VSIX را به‌صورت خصوصی توزیع کنید و کاربران آن را با گزینه Install from VSIX نصب کنند.

جمع‌بندی

ساخت افزونه VS Code یکی از کاربردی‌ترین روش‌های قراردادن هوش مصنوعی در جریان توسعه نرم‌افزار است. افزونه می‌تواند Context را مستقیماً از Editor دریافت کند و نتیجه را بدون جابه‌جایی میان مرورگر و محیط کدنویسی نمایش دهد.

در این آموزش افزونه‌ای ساختیم که:

  • با TypeScript توسعه داده می‌شود.
  • کد انتخاب‌شده را می‌خواند.
  • کد را توضیح می‌دهد.
  • پیشنهاد Refactor تولید می‌کند.
  • مستندات می‌سازد.
  • درباره فایل فعال پاسخ می‌دهد.
  • API Key را در Secret Storage نگه می‌دارد.
  • از مدل انتخابی درواره استفاده می‌کند.
  • پاسخ را به‌صورت Streaming نمایش می‌دهد.
  • درخواست را با فرمان کاربر متوقف می‌کند.
  • نتیجه را در سند Markdown جداگانه باز می‌کند.
  • به فایل VSIX قابل‌نصب تبدیل می‌شود.

برای شروع، در درواره ثبت‌نام کنید، API Key بسازید و مدل مناسب برنامه‌نویسی را از صفحه مدل‌ها انتخاب کنید.

مقالات مرتبط

برای مطالعه شرایط استفاده و محدودیت‌های مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.

Read more

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی با ترکیب گردش‌کارهای خودکار و مدل‌های هوش مصنوعی، پردازش متن، دسته‌بندی، استخراج اطلاعات و تصمیم‌های پیشنهادی را خودکار می‌کند. در این راهنما، معماری و ساخت نمونه عملی آن با API درواره را می‌آموزید.

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce شیوه‌ای جدید برای خرید اینترنتی است که در آن ایجنت هوش مصنوعی می‌تواند نیاز کاربر را بفهمد، محصولات را جست‌وجو و مقایسه کند و فرایند خرید را پیش ببرد. در این راهنما با معماری، UCP، ACP و پیاده‌سازی آن با API درواره آشنا می‌شوید.