ابنِ قناة من الصفر
تربط القناة وولف فيش بمنصة اتصال خارجية. واجهة سطح المكتب، Telegram، و WhatsApp كلها قنوات - تستقبل الرسائل من مصادر مختلفة لكنها توجهها عبر نفس خط أنابيب الدماغ.
يرشدك هذا الدليل خطوة بخطوة لتنفيذ قناة جديدة من الصفر.
ما هي القناة؟
القناة هي تنفيذ لواجهة TurnSink. تستقبل الأجزاء (النص المتدفق، استدعاءات الأدوات، النتائج) وتعرضها للمستخدم بأي تنسيق تدعمه المنصة. يتعامل TurnRunner مع خط أنابيب الوكيل - قناتك تحتاج فقط إلى:
- قبول الرسائل الواردة من منصتك
- إرسالها إلى خط أنابيب الوكيل
- عرض استجابة الوكيل للمستخدم
- التعامل مع طلبات الموافقة (لاستدعاءات الأدوات الخطرة)
واجهة TurnSink
interface TurnSink {
channelId: string
onSegment(segment: Segment): void
onTurnEvent(event: CorpusEvent): void
onApprovalRequest(req: ApprovalRequest): Promise<'approved' | 'denied'>
onDone(): void
onError(error: Error): void
onCredentialBlocked(variable: string): void
}
شرح الدوال
| الدالة | الغرض |
|---|
onSegment | تستقبل المحتوى المتدفق - أجزاء النص، استدعاءات الأدوات، نتائج الأدوات، نهاية الدور |
onTurnEvent | أحداث corpus المُرحّلة أثناء الدور (لمؤشرات الحالة مثل “جارٍ التفكير…”) |
onApprovalRequest | يجب أن تُرجع 'approved' أو 'denied' - نفّذها بالطريقة التي تطلب بها منصتك من المستخدمين |
onDone | تُستدعى عند اكتمال الدور بنجاح |
onError | تُستدعى عند فشل الدور بخطأ غير قابل للاسترداد |
onCredentialBlocked | تُستدعى عندما تحتاج أداة لمتغير غير موجود |
أنواع الأجزاء
type Segment =
| { type: 'text'; delta: string } // Streaming text chunk
| { type: 'tool_call'; call: ToolCallInfo } // Tool invocation starting
| { type: 'tool_result'; result: ToolResult } // Tool execution output
| { type: 'turn_end'; reason: StopReason } // Final marker
- text: أجزاء متدفقة - اجمعها لبناء الاستجابة الكاملة
- tool_call: الوكيل يستدعي أداة (اعرض مؤشر حالة)
- tool_result: الأداة انتهت (اعرض المخرجات أو ملخصًا)
- turn_end: الدور اكتمل مع سبب التوقف (
end_turn، tool_use، max_tokens)
التنفيذ خطوة بخطوة
الخطوة 1: إنشاء ملف القناة
touch src/main/channels/my-channel.ts
الخطوة 2: تنفيذ TurnSink
import { TurnSink, Segment, ApprovalRequest, CorpusEvent } from '../types'
import { TurnRunner } from '../runtime/turn-runner'
export class MyChannel implements TurnSink {
channelId = 'my-channel'
private turnRunner: TurnRunner
private responseBuffer = ''
constructor(turnRunner: TurnRunner) {
this.turnRunner = turnRunner
}
onSegment(segment: Segment): void {
switch (segment.type) {
case 'text':
this.responseBuffer += segment.delta
this.sendPartialUpdate(this.responseBuffer)
break
case 'tool_call':
this.sendStatus(`Using tool: ${segment.call.name}`)
break
case 'tool_result':
if (!segment.result.success) {
this.sendStatus(`Tool failed: ${segment.result.error}`)
}
break
case 'turn_end':
// Final response is ready
break
}
}
onTurnEvent(event: CorpusEvent): void {
// Optional: show status indicators
// e.g., "context.built" → "Thinking..."
}
async onApprovalRequest(req: ApprovalRequest): Promise<'approved' | 'denied'> {
// Implement however your platform prompts users
// Must return 'approved' or 'denied'
const userResponse = await this.promptUser(
`Wolffish wants to run: ${req.toolName}\nArgs: ${JSON.stringify(req.args)}`
)
return userResponse ? 'approved' : 'denied'
}
onDone(): void {
this.sendFinalMessage(this.responseBuffer)
this.responseBuffer = ''
}
onError(error: Error): void {
this.sendErrorMessage(`Error: ${error.message}`)
this.responseBuffer = ''
}
onCredentialBlocked(variable: string): void {
this.sendErrorMessage(
`Missing credential: ${variable}. Add it in Settings > Variables.`
)
}
// Platform-specific methods
private sendPartialUpdate(text: string) { /* ... */ }
private sendStatus(status: string) { /* ... */ }
private sendFinalMessage(text: string) { /* ... */ }
private sendErrorMessage(text: string) { /* ... */ }
private promptUser(message: string): Promise<boolean> { /* ... */ }
}
الخطوة 3: التعامل مع الرسائل الواردة
عندما تستقبل منصتك رسالة، حمّل أو أنشئ محادثة وأرسلها عبر TurnRunner:
async handleIncomingMessage(externalMessage: ExternalMessage) {
// Map external thread to a Wolffish conversation
const conversationId = this.resolveConversation(externalMessage.threadId)
// Load conversation history
const history = await this.loadHistory(conversationId)
// Send through the agent pipeline
await this.turnRunner.send({
content: externalMessage.text,
history,
conversationId,
sink: this // This channel is the TurnSink
})
}
الخطوة 4: تسجيل القناة
أضف قناتك إلى تدفق بدء التشغيل في src/main/index.ts:
import { MyChannel } from './channels/my-channel'
// During app initialization, after TurnRunner is created:
const myChannel = new MyChannel(turnRunner)
myChannel.start() // Begin listening for incoming messages
يُشارك TurnRunner عبر جميع القنوات. يقوم بتسلسل الأدوار - يعمل دور واحد فقط في كل مرة، بغض النظر عن القناة التي بدأته.
التسلسل عبر القنوات
يقوم TurnRunner بوضع الأدوار في طابور. إذا وصلت رسالة على Telegram بينما واجهة سطح المكتب في منتصف دور، تنتظر حتى يكتمل الدور الحالي. هذا يمنع حالات السباق في الذاكرة وتجميع السياق.
Desktop: "Summarize my emails" → [running]
Telegram: "What's the weather?" → [queued]
→ [running after desktop turn ends]
الأدوار طويلة التشغيل (المهام متعددة الخطوات مع استدعاءات أدوات كثيرة) تحجب القنوات الأخرى. إذا كان هذا مصدر قلق، فكر في تنفيذ نظام أولويات أو السماح بالإلغاء من أي قناة.
ربط المحادثات
قناتك تقرر كيف تُربط الخيوط الخارجية بمحادثات وولف فيش:
private resolveConversation(externalThreadId: string): string {
// Option A: One conversation per external thread
return `my-channel-${externalThreadId}`
// Option B: Single persistent conversation for the channel
return `my-channel-main`
// Option C: New conversation per session (with /new command to reset)
return this.activeConversation || this.createNew()
}
تستخدم قناة Telegram الخيار C - محادثة نشطة واحدة تستمر حتى يرسل المستخدم /new.
مرجع: نمط قناة Telegram
توضح قناة Telegram النمط الكامل:
// Simplified from src/main/channels/telegram.ts
class TelegramChannel implements TurnSink {
channelId = 'telegram'
async onMessage(msg: TelegramMessage) {
// 1. Check for slash commands
if (msg.text.startsWith('/')) {
await this.handleCommand(msg)
return
}
// 2. Load or create conversation
const conversationId = this.activeConversation
|| await this.createConversation()
// 3. Build history from stored messages
const history = await this.hippocampus.loadEpisode(conversationId)
// 4. Send through pipeline
await this.turnRunner.send({
content: msg.text,
history,
conversationId,
sink: this
})
}
async onApprovalRequest(req: ApprovalRequest) {
// Send inline keyboard with Approve/Deny buttons
const response = await this.sendInlineKeyboard(
`Wolffish wants to execute:\n ${req.toolName}: ${JSON.stringify(req.args)}`,
['Approve', 'Deny']
)
return response === 'Approve' ? 'approved' : 'denied'
}
}
قائمة التحقق للقنوات الجديدة
قبل نشر قناتك: