DeepSeek Harness: ممارسة: بناء قدرة قابلة للاستبدال

آخر تحديث: 2026-08-31

فهمت الأدوار الثلاثة نظرياً، لكن النظرية وحدها لا تكفي. هذا الدرس يتناول مشروعاً عملياً كاملاً — تنفيذ قدرة إحصائيات ملفات قابلة للاستبدال من الصفر: تعريف الواجهة، كتابة تنفيذ محلي، كتابة تنفيذ sandbox، استهلاك القدرة، وتبديل Providers. سير عمل كامل، من البداية للنهاية.

💡 نصيحة: النقطة الأساسية في هذا المشروع هي "كود Consumer لا يتغير عند تبديل Providers" — إذا كان Consumer يحتاج تغيير كود، تجريد Definition ليس جيداً بما فيه الكفاية.

📋 المتطلبات المسبقة: أكمل 22-capability.md، تفهم أدوار القدرات الثلاثة

1. ما ستتعلمه

بنية MyCap


2. هيكل المشروع

(1) ▶ مثال 1

TEXT 📖 للعرض فقط
file-stats-capability/
├── definition.ts            ← Definition
├── providers/
│   ├── local.ts             ← Provider محلي
│   └── sandbox.ts           ← Provider لـ Sandbox بعيد
├── consumer/
│   └── file-info-tool.ts    ← إضافة Consumer
├── types.ts                 ← تصريحات الأنواع
└── index.ts                 ← التصديرات

(2) ▶ مثال 2

100%
graph TB
    DEF[definition.ts] --> LOCAL[providers/local.ts]
    DEF --> SANDBOX[providers/sandbox.ts]
    DEF --> TOOL[consumer/file-info-tool.ts]

3. تعريف قدرة إحصائيات الملفات

(1) تحليل المتطلبات

قدرة إحصائيات الملفات تحتاج توفير:

(2) ▶ مثال 2

TYPESCRIPT
// definition.ts
import { defineCapability } from '@deepseek-ai/cordis'

export interface FileStatsResult {
  totalFiles: number
  totalDirs: number
  totalSize: number
  byExtension: Record<string, number>
}

export interface FileStatsCapability {
  countFiles(dir: string, recursive?: boolean): Promise<number>
  calcSize(dir: string): Promise<number>
  analyze(dir: string): Promise<FileStatsResult>
  exists(path: string): Promise<boolean>
}

export const FileStats = defineCapability({
  name: 'file-stats',
  description: 'File statistics and analysis capability',
  interface: {} as FileStatsCapability
})

(3) تصريحات الأنواع

TYPESCRIPT
// types.ts
import { FileStatsCapability } from './definition'

declare module '@deepseek-ai/cordis' {
  interface Context {
    'file-stats': FileStatsCapability
  }
}

4. تنفيذ Provider المحلي

(1) الكود

TYPESCRIPT
// providers/local.ts
import { Service, Context } from '@deepseek-ai/cordis'
import { FileStats, FileStatsResult } from '../definition'
import { readdir, stat } from 'fs/promises'
import { join } from 'path'

export default class LocalFileStatsProvider extends Service {
  static inject = ['fs']

  constructor(ctx: Context) {
    super(ctx, 'file-stats')
    
    ctx.implement(FileStats, {
      async countFiles(dir: string, recursive = false): Promise<number> {
        const result = await this._walk(dir, recursive)
        return result.totalFiles
      },

      async calcSize(dir: string): Promise<number> {
        const result = await this._walk(dir, true)
        return result.totalSize
      },

      async analyze(dir: string): Promise<FileStatsResult> {
        return await this._walk(dir, true)
      },

      async exists(path: string): Promise<boolean> {
        try {
          await stat(path)
          return true
        } catch {
          return false
        }
      }
    })
  }

  private async _walk(dir: string, recursive: boolean): Promise<FileStatsResult> {
    let totalFiles = 0
    let totalDirs = 0
    let totalSize = 0
    const byExtension: Record<string, number> = {}

    await this._walkInner(dir, recursive, (fileStat) => {
      totalFiles++
      totalSize += fileStat.size
      const ext = fileStat.name.includes('.')
        ? '.' + fileStat.name.split('.').pop()!.toLowerCase()
        : '(no extension)'
      byExtension[ext] = (byExtension[ext] || 0) + 1
    }, () => {
      totalDirs++
    })

    return { totalFiles, totalDirs, totalSize, byExtension }
  }

  private async _walkInner(
    dir: string,
    recursive: boolean,
    onFile: (f: { name: string; size: number }) => void,
    onDir: () => void
  ): Promise<void> {
    const entries = await readdir(dir, { withFileTypes: true })
    for (const entry of entries) {
      if (entry.isFile()) {
        const s = await stat(join(dir, entry.name))
        onFile({ name: entry.name, size: s.size })
      } else if (entry.isDirectory()) {
        onDir()
        if (recursive) {
          await this._walkInner(join(dir, entry.name), recursive, onFile, onDir)
        }
      }
    }
  }
}

(2) التسجيل في cordis.yml

YAML
plugins:
  file-stats:
    $insert: /home/alice/dev/file-stats-capability/providers/local

5. تنفيذ Provider لـ Sandbox البعيد

(1) التصميم

Provider الـ sandbox يُفوض عمليات الملفات لخدمة بعيدة:

100%
graph LR
    TOOL[Consumer] -->|يستدعي| CAP[قدرة file-stats]
    CAP -->|HTTP| SANDBOX[خدمة Sandbox<br/>sandbox:8080]
    SANDBOX -->|يعمل على| FS[نظام ملفات معزول]

(2) الكود

TYPESCRIPT
// providers/sandbox.ts
import { Service, Context } from '@deepseek-ai/cordis'
import { FileStats, FileStatsResult } from '../definition'

export const Config = Schema.object({
  endpoint: Schema.string().default('http://sandbox:8080').description('Sandbox API endpoint'),
  timeout: Schema.number().default(30000).description('Request timeout in ms')
})

export default class SandboxFileStatsProvider extends Service {
  static inject = []

  private endpoint: string
  private timeout: number

  constructor(ctx: Context) {
    super(ctx, 'file-stats')
    this.endpoint = ctx.config.endpoint
    this.timeout = ctx.config.timeout
    
    ctx.implement(FileStats, {
      countFiles: (dir, recursive) => 
        this._call('count-files', { dir, recursive }).then(r => r.count),
      
      calcSize: (dir) => 
        this._call('calc-size', { dir }).then(r => r.size),
      
      analyze: (dir) => 
        this._call<FileStatsResult>('analyze', { dir }),
      
      exists: (path) => 
        this._call('exists', { path }).then(r => r.exists)
    })
  }

  private async _call<T = any>(action: string, params: Record<string, any>): Promise<T> {
    const controller = new AbortController()
    const timer = setTimeout(() => controller.abort(), this.timeout)

    try {
      const response = await fetch(`${this.endpoint}/file-stats/${action}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(params),
        signal: controller.signal
      })

      if (!response.ok) {
        throw new Error(`sandbox error: ${response.status} ${response.statusText}`)
      }

      return await response.json() as T
    } finally {
      clearTimeout(timer)
    }
  }
}

(3) التسجيل في cordis.yml

YAML
plugins:
  file-stats:
    $replace: /home/alice/dev/file-stats-capability/providers/sandbox
    config:
      endpoint: http://sandbox:8080
      timeout: 15000

6. استهلاك القدرة في أداة

(1) كود إضافة Consumer

TYPESCRIPT
// consumer/file-info-tool.ts
import { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh'

export const name = 'tool-file-info'
export const inject = ['tools', 'file-stats']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'file_info',
    description: 'Get detailed file statistics for a directory. Returns file count, total size, and breakdown by extension.',
    parameters: {
      type: 'object',
      properties: {
        path: {
          type: 'string',
          description: 'Directory path to analyze'
        },
        recursive: {
          type: 'boolean',
          description: 'Include subdirectories in analysis',
          default: true
        }
      },
      required: ['path']
    },
    async execute({ path, recursive }, ctx) {
      try {
        const exists = await ctx['file-stats'].exists(path)
        if (!exists) {
          return { error: true, message: `Path does not exist: ${path}` }
        }

        const stats = await ctx['file-stats'].analyze(path)
        
        return {
          path,
          recursive,
          totalFiles: stats.totalFiles,
          totalDirs: stats.totalDirs,
          totalSize: stats.totalSize,
          totalSizeHuman: formatSize(stats.totalSize),
          byExtension: stats.byExtension
        }
      } catch (error: any) {
        return {
          error: true,
          message: `Failed to analyze directory: ${error.message}`
        }
      }
    }
  }))
}

function formatSize(bytes: number): string {
  if (bytes < 1024) return `${bytes} B`
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
  if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`
  return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`
}

(2) ملاحظة أساسية

كود Consumer لا يحتوي أي ذكر لـ local أو sandbox — يعتمد فقط على واجهة قدرة file-stats. هذه هي قيمة نظام القدرات.


7. آثار تبديل Providers

(1) استخدام Provider المحلي

YAML
# cordis.yml — تطوير محلي
plugins:
  file-stats:
    $insert: /home/alice/dev/file-stats-capability/providers/local

الوكيل يستدعي أداة file_info:

TEXT 📖 للعرض فقط
👤 Alice: Analyze the /home/alice/project directory

🤖 Agent:
🔧 Using tool: file_info
  → path: /home/alice/project
  
  Result: {
    path: "/home/alice/project",
    totalFiles: 42,
    totalDirs: 5,
    totalSize: 245760,
    totalSizeHuman: "240.0 KB",
    byExtension: { ".ts": 28, ".js": 10, ".json": 4 }
  }

(2) التبديل لـ Provider الـ Sandbox

YAML
# cordis.yml — بيئة sandbox
plugins:
  file-stats:
    $replace: /home/alice/dev/file-stats-capability/providers/sandbox
    config:
      endpoint: http://sandbox:8080

بعد إعادة التشغيل، الوكيل يستدعي نفس الأداة:

TEXT 📖 للعرض فقط
👤 Alice: Analyze the /workspace/project directory

🤖 Agent:
🔧 Using tool: file_info
  → path: /workspace/project
  
  Result: {
    path: "/workspace/project",
    totalFiles: 42,
    totalDirs: 5,
    totalSize: 245760,
    totalSizeHuman: "240.0 KB",
    byExtension: { ".ts": 28, ".js": 10, ".json": 4 }
  }

كود Consumer لم يتغير تماماً، صيغة النتيجة متطابقة — فقط التنفيذ الأساسي تغير من نظام ملفات محلي لـ API sandbox بعيد.

(3) المقارنة

البُعد Provider محلي Provider Sandbox
التنفيذ استدعاءات fs مباشرة استدعاءات HTTP API
نظام الملفات قرص محلي بيئة معزولة
زمن الاستجابة < 1ms 50-200ms
الأمان وصول مباشر عزل sandbox
كود Consumer نفسه نفسه
صيغة الإرجاع نفسها نفسها

❓ أسئلة شائعة

س هل يجب أن يعيد كلا Providerين نفس الصيغة بالضبط؟
ج نعم. هذا هو القيد الأساسي لـ Definition — جميع Providers يجب أن يتبعوا نفس الواجهة. صيغ إرجاع مختلفة تُسبب أخطاء تحليل في Consumer.
س Provider الـ sandbox له زمن استجابة أعلى — هل يحتاج Consumer للتعامل مع هذا؟
ج لا. زمن الاستجابة شفاف بالنسبة لـ Consumer. إذا احتجت التحسين، أضف تخزيناً مؤقتاً في طبقة Consumer أو استخدم مؤشرات غير متزامنة.
س كيف أضمن أن Provider يُنفّذ جميع الدوال؟
ج فحص TypeScript وقت الترجمة. إذا كان Provider مفقوداً دوالاً من Definition، الترجمة تفشل.
س هل يمكن استخدام نظام القدرات في إعدادات الإضافة؟
ج نعم. Providers مختلفة يمكن أن يكون لها Config مختلف: yaml file-stats: $insert: ./providers/sandbox config: endpoint: http://sandbox:8080 # إعدادات خاصة بـ Provider الـ sandbox
س كيف يعرف Consumer أي Provider نشط حالياً؟
ج عادة لا يحتاج. إذا احتجت فعلاً، أضف دالة providerInfo() لـ Definition؛ كل Provider يُعيد معلوماته الخاصة.

📖 ملخص


📝 تمارين

1. ⭐ أساسي: اتبع خطوات هذا الدرس لإنشاء Definition و Provider محلي لقدرة FileStats، سجّلها، وتحقق عبر استدعاءات أداة Consumer.

2. ⭐⭐ متوسط: نفّذ InMemoryProvider — بيانات الملفات مُخزّنة مسبقاً في Map بالذاكرة (بدون وصول حقيقي لنظام الملفات)، مناسبة لاختبار الوحدات. تحقق أن أداة Consumer تعمل بشكل طبيعي مع InMemoryProvider.

3. ⭐⭐⭐ تحدٍ: نفّذ CachedProvider — نمط زخرفي، إضافة طبقة ذاكرة مؤقتة أمام Provider آخر. خزّن آخر N نتيجة تحليل؛ نفس المسارات تعيد نتائج مُخزّنة مباشرة. ملاحظة: CachedProvider نفسه هو أيضاً Provider؛ يُفوض داخلياً لـ Provider آخر.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%