DeepSeek Harness: ممارسة: بناء قدرة قابلة للاستبدال
آخر تحديث: 2026-08-31
فهمت الأدوار الثلاثة نظرياً، لكن النظرية وحدها لا تكفي. هذا الدرس يتناول مشروعاً عملياً كاملاً — تنفيذ قدرة إحصائيات ملفات قابلة للاستبدال من الصفر: تعريف الواجهة، كتابة تنفيذ محلي، كتابة تنفيذ sandbox، استهلاك القدرة، وتبديل Providers. سير عمل كامل، من البداية للنهاية.
📋 المتطلبات المسبقة: أكمل 22-capability.md، تفهم أدوار القدرات الثلاثة
1. ما ستتعلمه
- مثال كامل: Definition → Provider → Consumer
- تعريف قدرة إحصائيات الملفات
- تنفيذ Provider محلي
- تنفيذ Provider لـ sandbox بعيد
- استهلاك القدرة في أداة
- آثار تبديل Providers
2. هيكل المشروع
(1) ▶ مثال 1
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
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
// 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) تصريحات الأنواع
// types.ts
import { FileStatsCapability } from './definition'
declare module '@deepseek-ai/cordis' {
interface Context {
'file-stats': FileStatsCapability
}
}
4. تنفيذ Provider المحلي
(1) الكود
// 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
plugins:
file-stats:
$insert: /home/alice/dev/file-stats-capability/providers/local
5. تنفيذ Provider لـ Sandbox البعيد
(1) التصميم
Provider الـ sandbox يُفوض عمليات الملفات لخدمة بعيدة:
graph LR
TOOL[Consumer] -->|يستدعي| CAP[قدرة file-stats]
CAP -->|HTTP| SANDBOX[خدمة Sandbox<br/>sandbox:8080]
SANDBOX -->|يعمل على| FS[نظام ملفات معزول]
(2) الكود
// 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
plugins:
file-stats:
$replace: /home/alice/dev/file-stats-capability/providers/sandbox
config:
endpoint: http://sandbox:8080
timeout: 15000
6. استهلاك القدرة في أداة
(1) كود إضافة Consumer
// 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 المحلي
# cordis.yml — تطوير محلي
plugins:
file-stats:
$insert: /home/alice/dev/file-stats-capability/providers/local
الوكيل يستدعي أداة file_info:
👤 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
# cordis.yml — بيئة sandbox
plugins:
file-stats:
$replace: /home/alice/dev/file-stats-capability/providers/sandbox
config:
endpoint: http://sandbox:8080
بعد إعادة التشغيل، الوكيل يستدعي نفس الأداة:
👤 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 | نفسه | نفسه |
| صيغة الإرجاع | نفسها | نفسها |
❓ أسئلة شائعة
yaml file-stats: $insert: ./providers/sandbox config: endpoint: http://sandbox:8080 # إعدادات خاصة بـ Provider الـ sandbox providerInfo() لـ Definition؛ كل Provider يُعيد معلوماته الخاصة.📖 ملخص
- التدفق الكامل: Definition → Provider → Consumer → التحقق من التبديل
- Definition يُصرّح بواجهة
FileStatsCapabilityمع countFiles/calcSize/analyze/exists - Provider المحلي يستخدم fs API مباشرة؛ Provider الـ sandbox يُفوض عبر HTTP لخدمة بعيدة
- Consumer يعتمد فقط على الواجهة — لا يعرف ولا يهتم بـ Provider المُحدد
- تبديل Providers يتطلب فقط تغيير إعدادات cordis.yml؛ كود Consumer صفر تعديلات
- جميع Providers يجب أن يعيدوا النتائج بنفس الصيغة — هذا هو القيد الأساسي لـ Definition
📝 تمارين
1. ⭐ أساسي: اتبع خطوات هذا الدرس لإنشاء Definition و Provider محلي لقدرة FileStats، سجّلها، وتحقق عبر استدعاءات أداة Consumer.
2. ⭐⭐ متوسط: نفّذ InMemoryProvider — بيانات الملفات مُخزّنة مسبقاً في Map بالذاكرة (بدون وصول حقيقي لنظام الملفات)، مناسبة لاختبار الوحدات. تحقق أن أداة Consumer تعمل بشكل طبيعي مع InMemoryProvider.
3. ⭐⭐⭐ تحدٍ: نفّذ CachedProvider — نمط زخرفي، إضافة طبقة ذاكرة مؤقتة أمام Provider آخر. خزّن آخر N نتيجة تحليل؛ نفس المسارات تعيد نتائج مُخزّنة مباشرة. ملاحظة: CachedProvider نفسه هو أيضاً Provider؛ يُفوض داخلياً لـ Provider آخر.