feat(pipeline): implement Pipeline DSL system for automated workflows
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Add complete Pipeline DSL system including:
- Rust backend (zclaw-pipeline crate) with parser, executor, and state management
- Frontend components: PipelinesPanel, PipelineResultPreview, ClassroomPreviewer
- Pipeline recommender for Agent conversation integration
- 5 pipeline templates: education, marketing, legal, research, productivity
- Documentation for Pipeline DSL architecture
Pipeline DSL enables declarative workflow definitions with:
- YAML-based configuration
- Expression resolution (${inputs.topic}, ${steps.step1.output})
- LLM integration, parallel execution, file export
- Agent smart recommendations in conversations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
447
desktop/src/lib/pipeline-client.ts
Normal file
447
desktop/src/lib/pipeline-client.ts
Normal file
@@ -0,0 +1,447 @@
|
||||
/**
|
||||
* Pipeline Client (Tauri)
|
||||
*
|
||||
* Client for discovering, running, and monitoring Pipelines.
|
||||
* Pipelines are DSL-based workflows that orchestrate Skills and Hands.
|
||||
*/
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
||||
|
||||
// Re-export UnlistenFn for external use
|
||||
export type { UnlistenFn };
|
||||
|
||||
// === Types ===
|
||||
|
||||
export interface PipelineInputInfo {
|
||||
name: string;
|
||||
inputType: string;
|
||||
required: boolean;
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
default?: unknown;
|
||||
options: string[];
|
||||
}
|
||||
|
||||
export interface PipelineInfo {
|
||||
id: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
category: string;
|
||||
tags: string[];
|
||||
icon: string;
|
||||
version: string;
|
||||
author: string;
|
||||
inputs: PipelineInputInfo[];
|
||||
}
|
||||
|
||||
export interface RunPipelineRequest {
|
||||
pipelineId: string;
|
||||
inputs: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface RunPipelineResponse {
|
||||
runId: string;
|
||||
pipelineId: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface PipelineRunResponse {
|
||||
runId: string;
|
||||
pipelineId: string;
|
||||
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
|
||||
currentStep?: string;
|
||||
percentage: number;
|
||||
message: string;
|
||||
outputs?: unknown;
|
||||
error?: string;
|
||||
startedAt: string;
|
||||
endedAt?: string;
|
||||
}
|
||||
|
||||
export interface PipelineCompleteEvent {
|
||||
runId: string;
|
||||
pipelineId: string;
|
||||
status: string;
|
||||
outputs?: unknown;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// === Pipeline Client ===
|
||||
|
||||
export class PipelineClient {
|
||||
/**
|
||||
* List all available pipelines
|
||||
*/
|
||||
static async listPipelines(options?: {
|
||||
category?: string;
|
||||
}): Promise<PipelineInfo[]> {
|
||||
try {
|
||||
const pipelines = await invoke<PipelineInfo[]>('pipeline_list', {
|
||||
category: options?.category || null,
|
||||
});
|
||||
return pipelines;
|
||||
} catch (error) {
|
||||
console.error('Failed to list pipelines:', error);
|
||||
throw new Error(`Failed to list pipelines: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific pipeline by ID
|
||||
*/
|
||||
static async getPipeline(pipelineId: string): Promise<PipelineInfo> {
|
||||
try {
|
||||
const pipeline = await invoke<PipelineInfo>('pipeline_get', {
|
||||
pipelineId,
|
||||
});
|
||||
return pipeline;
|
||||
} catch (error) {
|
||||
console.error(`Failed to get pipeline ${pipelineId}:`, error);
|
||||
throw new Error(`Failed to get pipeline: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a pipeline with the given inputs
|
||||
*/
|
||||
static async runPipeline(request: RunPipelineRequest): Promise<RunPipelineResponse> {
|
||||
try {
|
||||
const response = await invoke<RunPipelineResponse>('pipeline_run', {
|
||||
request,
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Failed to run pipeline:', error);
|
||||
throw new Error(`Failed to run pipeline: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the progress of a running pipeline
|
||||
*/
|
||||
static async getProgress(runId: string): Promise<PipelineRunResponse> {
|
||||
try {
|
||||
const progress = await invoke<PipelineRunResponse>('pipeline_progress', {
|
||||
runId,
|
||||
});
|
||||
return progress;
|
||||
} catch (error) {
|
||||
console.error(`Failed to get progress for run ${runId}:`, error);
|
||||
throw new Error(`Failed to get progress: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the result of a completed pipeline run
|
||||
*/
|
||||
static async getResult(runId: string): Promise<PipelineRunResponse> {
|
||||
try {
|
||||
const result = await invoke<PipelineRunResponse>('pipeline_result', {
|
||||
runId,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`Failed to get result for run ${runId}:`, error);
|
||||
throw new Error(`Failed to get result: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a running pipeline
|
||||
*/
|
||||
static async cancel(runId: string): Promise<void> {
|
||||
try {
|
||||
await invoke('pipeline_cancel', { runId });
|
||||
} catch (error) {
|
||||
console.error(`Failed to cancel run ${runId}:`, error);
|
||||
throw new Error(`Failed to cancel run: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all runs
|
||||
*/
|
||||
static async listRuns(): Promise<PipelineRunResponse[]> {
|
||||
try {
|
||||
const runs = await invoke<PipelineRunResponse[]>('pipeline_runs');
|
||||
return runs;
|
||||
} catch (error) {
|
||||
console.error('Failed to list runs:', error);
|
||||
throw new Error(`Failed to list runs: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh pipeline discovery (rescan filesystem)
|
||||
*/
|
||||
static async refresh(): Promise<PipelineInfo[]> {
|
||||
try {
|
||||
const pipelines = await invoke<PipelineInfo[]>('pipeline_refresh');
|
||||
return pipelines;
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh pipelines:', error);
|
||||
throw new Error(`Failed to refresh pipelines: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to pipeline completion events
|
||||
*/
|
||||
static async onComplete(
|
||||
callback: (event: PipelineCompleteEvent) => void
|
||||
): Promise<UnlistenFn> {
|
||||
return listen<PipelineCompleteEvent>('pipeline-complete', (event) => {
|
||||
callback(event.payload);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a pipeline and wait for completion
|
||||
* Returns the final result
|
||||
*/
|
||||
static async runAndWait(
|
||||
request: RunPipelineRequest,
|
||||
onProgress?: (progress: PipelineRunResponse) => void,
|
||||
pollIntervalMs: number = 1000
|
||||
): Promise<PipelineRunResponse> {
|
||||
// Start the pipeline
|
||||
const { runId } = await this.runPipeline(request);
|
||||
|
||||
// Poll for progress until completion
|
||||
let result = await this.getProgress(runId);
|
||||
|
||||
while (result.status === 'running' || result.status === 'pending') {
|
||||
if (onProgress) {
|
||||
onProgress(result);
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
||||
result = await this.getProgress(runId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// === Utility Functions ===
|
||||
|
||||
/**
|
||||
* Format pipeline input type for display
|
||||
*/
|
||||
export function formatInputType(type: string): string {
|
||||
const typeMap: Record<string, string> = {
|
||||
string: '文本',
|
||||
number: '数字',
|
||||
boolean: '布尔值',
|
||||
select: '单选',
|
||||
'multi-select': '多选',
|
||||
file: '文件',
|
||||
text: '多行文本',
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default value for input type
|
||||
*/
|
||||
export function getDefaultForType(type: string): unknown {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
case 'text':
|
||||
return '';
|
||||
case 'number':
|
||||
return 0;
|
||||
case 'boolean':
|
||||
return false;
|
||||
case 'select':
|
||||
return null;
|
||||
case 'multi-select':
|
||||
return [];
|
||||
case 'file':
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate pipeline inputs against schema
|
||||
*/
|
||||
export function validateInputs(
|
||||
inputs: PipelineInputInfo[],
|
||||
values: Record<string, unknown>
|
||||
): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const input of inputs) {
|
||||
const value = values[input.name];
|
||||
|
||||
// Check required
|
||||
if (input.required && (value === undefined || value === null || value === '')) {
|
||||
errors.push(`${input.label || input.name} 是必填项`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip validation if not provided and not required
|
||||
if (value === undefined || value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Type-specific validation
|
||||
switch (input.inputType) {
|
||||
case 'number':
|
||||
if (typeof value !== 'number') {
|
||||
errors.push(`${input.label || input.name} 必须是数字`);
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
if (typeof value !== 'boolean') {
|
||||
errors.push(`${input.label || input.name} 必须是布尔值`);
|
||||
}
|
||||
break;
|
||||
case 'select':
|
||||
if (input.options.length > 0 && !input.options.includes(String(value))) {
|
||||
errors.push(`${input.label || input.name} 必须是有效选项`);
|
||||
}
|
||||
break;
|
||||
case 'multi-select':
|
||||
if (!Array.isArray(value)) {
|
||||
errors.push(`${input.label || input.name} 必须是数组`);
|
||||
} else if (input.options.length > 0) {
|
||||
const invalid = value.filter((v) => !input.options.includes(String(v)));
|
||||
if (invalid.length > 0) {
|
||||
errors.push(`${input.label || input.name} 包含无效选项`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
// === React Hook ===
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
export interface UsePipelineOptions {
|
||||
category?: string;
|
||||
autoRefresh?: boolean;
|
||||
refreshInterval?: number;
|
||||
}
|
||||
|
||||
export function usePipelines(options: UsePipelineOptions = {}) {
|
||||
const [pipelines, setPipelines] = useState<PipelineInfo[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadPipelines = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await PipelineClient.listPipelines({
|
||||
category: options.category,
|
||||
});
|
||||
setPipelines(result);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [options.category]);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await PipelineClient.refresh();
|
||||
// Filter by category if specified
|
||||
const filtered = options.category
|
||||
? result.filter((p) => p.category === options.category)
|
||||
: result;
|
||||
setPipelines(filtered);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [options.category]);
|
||||
|
||||
useEffect(() => {
|
||||
loadPipelines();
|
||||
}, [loadPipelines]);
|
||||
|
||||
useEffect(() => {
|
||||
if (options.autoRefresh && options.refreshInterval) {
|
||||
const interval = setInterval(loadPipelines, options.refreshInterval);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [options.autoRefresh, options.refreshInterval, loadPipelines]);
|
||||
|
||||
return {
|
||||
pipelines,
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
reload: loadPipelines,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UsePipelineRunOptions {
|
||||
onComplete?: (result: PipelineRunResponse) => void;
|
||||
onProgress?: (progress: PipelineRunResponse) => void;
|
||||
}
|
||||
|
||||
export function usePipelineRun(options: UsePipelineRunOptions = {}) {
|
||||
const [running, setRunning] = useState(false);
|
||||
const [progress, setProgress] = useState<PipelineRunResponse | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const run = useCallback(
|
||||
async (pipelineId: string, inputs: Record<string, unknown>) => {
|
||||
setRunning(true);
|
||||
setError(null);
|
||||
setProgress(null);
|
||||
|
||||
try {
|
||||
const result = await PipelineClient.runAndWait(
|
||||
{ pipelineId, inputs },
|
||||
(p) => {
|
||||
setProgress(p);
|
||||
options.onProgress?.(p);
|
||||
}
|
||||
);
|
||||
|
||||
setProgress(result);
|
||||
options.onComplete?.(result);
|
||||
return result;
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||
setError(errorMsg);
|
||||
throw err;
|
||||
} finally {
|
||||
setRunning(false);
|
||||
}
|
||||
},
|
||||
[options]
|
||||
);
|
||||
|
||||
const cancel = useCallback(async () => {
|
||||
if (progress?.runId) {
|
||||
await PipelineClient.cancel(progress.runId);
|
||||
setRunning(false);
|
||||
}
|
||||
}, [progress?.runId]);
|
||||
|
||||
return {
|
||||
run,
|
||||
cancel,
|
||||
running,
|
||||
progress,
|
||||
error,
|
||||
};
|
||||
}
|
||||
297
desktop/src/lib/pipeline-recommender.ts
Normal file
297
desktop/src/lib/pipeline-recommender.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* Pipeline Recommender Service
|
||||
*
|
||||
* Analyzes user messages to recommend relevant Pipelines.
|
||||
* Used by Agent conversation flow to proactively suggest workflows.
|
||||
*/
|
||||
|
||||
import { PipelineInfo, PipelineClient } from './pipeline-client';
|
||||
|
||||
// === Types ===
|
||||
|
||||
export interface PipelineRecommendation {
|
||||
pipeline: PipelineInfo;
|
||||
confidence: number; // 0-1
|
||||
matchedKeywords: string[];
|
||||
suggestedInputs: Record<string, unknown>;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface IntentPattern {
|
||||
keywords: RegExp[];
|
||||
category?: string;
|
||||
pipelineId?: string;
|
||||
minConfidence: number;
|
||||
inputSuggestions?: (message: string) => Record<string, unknown>;
|
||||
}
|
||||
|
||||
// === Intent Patterns ===
|
||||
|
||||
const INTENT_PATTERNS: IntentPattern[] = [
|
||||
// Education - Classroom
|
||||
{
|
||||
keywords: [
|
||||
/课件|教案|备课|课堂|教学|ppt|幻灯片/i,
|
||||
/上课|讲课|教材/i,
|
||||
/生成.*课件|制作.*课件|创建.*课件/i,
|
||||
],
|
||||
category: 'education',
|
||||
pipelineId: 'classroom-generator',
|
||||
minConfidence: 0.75,
|
||||
},
|
||||
|
||||
// Marketing - Campaign
|
||||
{
|
||||
keywords: [
|
||||
/营销|推广|宣传|市场.*方案|营销.*策略/i,
|
||||
/产品.*推广|品牌.*宣传/i,
|
||||
/广告.*方案|营销.*计划/i,
|
||||
/生成.*营销|制作.*营销/i,
|
||||
],
|
||||
category: 'marketing',
|
||||
pipelineId: 'marketing-campaign',
|
||||
minConfidence: 0.72,
|
||||
},
|
||||
|
||||
// Legal - Contract Review
|
||||
{
|
||||
keywords: [
|
||||
/合同.*审查|合同.*风险|合同.*检查/i,
|
||||
/审查.*合同|检查.*合同|分析.*合同/i,
|
||||
/法律.*审查|合规.*检查/i,
|
||||
/合同.*条款|条款.*风险/i,
|
||||
],
|
||||
category: 'legal',
|
||||
pipelineId: 'contract-review',
|
||||
minConfidence: 0.78,
|
||||
},
|
||||
|
||||
// Research - Literature Review
|
||||
{
|
||||
keywords: [
|
||||
/文献.*综述|文献.*分析|文献.*检索/i,
|
||||
/研究.*综述|学术.*综述/i,
|
||||
/论文.*综述|论文.*调研/i,
|
||||
/文献.*搜索|文献.*查找/i,
|
||||
],
|
||||
category: 'research',
|
||||
pipelineId: 'literature-review',
|
||||
minConfidence: 0.73,
|
||||
},
|
||||
|
||||
// Productivity - Meeting Summary
|
||||
{
|
||||
keywords: [
|
||||
/会议.*纪要|会议.*总结|会议.*记录/i,
|
||||
/整理.*会议|总结.*会议/i,
|
||||
/会议.*整理|纪要.*生成/i,
|
||||
/待办.*事项|行动.*项/i,
|
||||
],
|
||||
category: 'productivity',
|
||||
pipelineId: 'meeting-summary',
|
||||
minConfidence: 0.70,
|
||||
},
|
||||
|
||||
// Generic patterns for each category
|
||||
{
|
||||
keywords: [/帮我.*生成|帮我.*制作|帮我.*创建|自动.*生成/i],
|
||||
minConfidence: 0.5,
|
||||
},
|
||||
];
|
||||
|
||||
// === Pipeline Recommender Class ===
|
||||
|
||||
export class PipelineRecommender {
|
||||
private pipelines: PipelineInfo[] = [];
|
||||
private initialized = false;
|
||||
|
||||
/**
|
||||
* Initialize the recommender by loading pipelines
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
|
||||
try {
|
||||
this.pipelines = await PipelineClient.listPipelines();
|
||||
this.initialized = true;
|
||||
} catch (error) {
|
||||
console.error('[PipelineRecommender] Failed to load pipelines:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh pipeline list
|
||||
*/
|
||||
async refresh(): Promise<void> {
|
||||
try {
|
||||
this.pipelines = await PipelineClient.refresh();
|
||||
} catch (error) {
|
||||
console.error('[PipelineRecommender] Failed to refresh pipelines:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze a user message and return pipeline recommendations
|
||||
*/
|
||||
async recommend(message: string): Promise<PipelineRecommendation[]> {
|
||||
if (!this.initialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
const recommendations: PipelineRecommendation[] = [];
|
||||
const messageLower = message.toLowerCase();
|
||||
|
||||
for (const pattern of INTENT_PATTERNS) {
|
||||
const matches = pattern.keywords
|
||||
.map(regex => regex.test(message))
|
||||
.filter(Boolean);
|
||||
|
||||
if (matches.length === 0) continue;
|
||||
|
||||
const confidence = Math.min(
|
||||
pattern.minConfidence + (matches.length - 1) * 0.05,
|
||||
0.95
|
||||
);
|
||||
|
||||
// Find matching pipeline
|
||||
let matchingPipelines: PipelineInfo[] = [];
|
||||
|
||||
if (pattern.pipelineId) {
|
||||
matchingPipelines = this.pipelines.filter(p => p.id === pattern.pipelineId);
|
||||
} else if (pattern.category) {
|
||||
matchingPipelines = this.pipelines.filter(p => p.category === pattern.category);
|
||||
}
|
||||
|
||||
// If no specific pipeline found, recommend based on category or all
|
||||
if (matchingPipelines.length === 0 && !pattern.pipelineId && !pattern.category) {
|
||||
// Generic match - recommend top pipelines
|
||||
matchingPipelines = this.pipelines.slice(0, 3);
|
||||
}
|
||||
|
||||
for (const pipeline of matchingPipelines) {
|
||||
const matchedKeywords = pattern.keywords
|
||||
.filter(regex => regex.test(message))
|
||||
.map(regex => regex.source);
|
||||
|
||||
const suggestion: PipelineRecommendation = {
|
||||
pipeline,
|
||||
confidence,
|
||||
matchedKeywords,
|
||||
suggestedInputs: pattern.inputSuggestions?.(message) ?? {},
|
||||
reason: this.generateReason(pipeline, matchedKeywords, confidence),
|
||||
};
|
||||
|
||||
// Avoid duplicates
|
||||
if (!recommendations.find(r => r.pipeline.id === pipeline.id)) {
|
||||
recommendations.push(suggestion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by confidence and return top recommendations
|
||||
return recommendations
|
||||
.sort((a, b) => b.confidence - a.confidence)
|
||||
.slice(0, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a human-readable reason for the recommendation
|
||||
*/
|
||||
private generateReason(
|
||||
pipeline: PipelineInfo,
|
||||
matchedKeywords: string[],
|
||||
confidence: number
|
||||
): string {
|
||||
const confidenceText =
|
||||
confidence >= 0.8 ? '非常适合' :
|
||||
confidence >= 0.7 ? '适合' :
|
||||
confidence >= 0.6 ? '可能适合' : '或许可以尝试';
|
||||
|
||||
if (matchedKeywords.length > 0) {
|
||||
return `您的需求与【${pipeline.displayName}】${confidenceText}。这个 Pipeline 可以帮助您自动化完成相关任务。`;
|
||||
}
|
||||
|
||||
return `【${pipeline.displayName}】可能对您有帮助。需要我为您启动吗?`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format recommendation for Agent message
|
||||
*/
|
||||
formatRecommendationForAgent(rec: PipelineRecommendation): string {
|
||||
const pipeline = rec.pipeline;
|
||||
let message = `我可以使用【${pipeline.displayName}】为你自动完成这个任务。\n\n`;
|
||||
message += `**功能说明**: ${pipeline.description}\n\n`;
|
||||
|
||||
if (Object.keys(rec.suggestedInputs).length > 0) {
|
||||
message += `**我已识别到以下信息**:\n`;
|
||||
for (const [key, value] of Object.entries(rec.suggestedInputs)) {
|
||||
message += `- ${key}: ${value}\n`;
|
||||
}
|
||||
message += '\n';
|
||||
}
|
||||
|
||||
message += `需要开始吗?`;
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a message might benefit from a pipeline
|
||||
*/
|
||||
mightNeedPipeline(message: string): boolean {
|
||||
const pipelineKeywords = [
|
||||
'生成', '创建', '制作', '分析', '审查', '整理',
|
||||
'总结', '归纳', '提取', '转换', '自动化',
|
||||
'帮我', '请帮我', '能不能', '可以',
|
||||
];
|
||||
|
||||
return pipelineKeywords.some(kw => message.includes(kw));
|
||||
}
|
||||
}
|
||||
|
||||
// === Singleton Instance ===
|
||||
|
||||
export const pipelineRecommender = new PipelineRecommender();
|
||||
|
||||
// === React Hook ===
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
export interface UsePipelineRecommendationOptions {
|
||||
autoInit?: boolean;
|
||||
minConfidence?: number;
|
||||
}
|
||||
|
||||
export function usePipelineRecommendation(options: UsePipelineRecommendationOptions = {}) {
|
||||
const [recommender] = useState(() => new PipelineRecommender());
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (options.autoInit !== false) {
|
||||
recommender.initialize().then(() => setInitialized(true));
|
||||
}
|
||||
}, [recommender, options.autoInit]);
|
||||
|
||||
const recommend = useCallback(async (message: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const results = await recommender.recommend(message);
|
||||
const minConf = options.minConfidence ?? 0.6;
|
||||
return results.filter(r => r.confidence >= minConf);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [recommender, options.minConfidence]);
|
||||
|
||||
return {
|
||||
recommend,
|
||||
initialized,
|
||||
loading,
|
||||
refresh: recommender.refresh.bind(recommender),
|
||||
mightNeedPipeline: recommender.mightNeedPipeline,
|
||||
formatRecommendationForAgent: recommender.formatRecommendationForAgent.bind(recommender),
|
||||
};
|
||||
}
|
||||
|
||||
export default pipelineRecommender;
|
||||
Reference in New Issue
Block a user