diff --git a/crates/zclaw-saas/migrations/20260402000002_knowledge_base.sql b/crates/zclaw-saas/migrations/20260402000002_knowledge_base.sql index b9aff9e..a6a54eb 100644 --- a/crates/zclaw-saas/migrations/20260402000002_knowledge_base.sql +++ b/crates/zclaw-saas/migrations/20260402000002_knowledge_base.sql @@ -1,9 +1,9 @@ --- Migration: Knowledge Base tables with pgvector support +-- Migration: Knowledge Base tables with optional pgvector support -- 5 tables: knowledge_categories, knowledge_items, knowledge_chunks, -- knowledge_versions, knowledge_usage - --- Enable pgvector extension -CREATE EXTENSION IF NOT EXISTS vector; +-- +-- pgvector is optional: if the extension is not installed, the embedding +-- column and HNSW index are skipped. All other tables work normally. -- 行业分类树 CREATE TABLE IF NOT EXISTS knowledge_categories ( @@ -42,12 +42,12 @@ CREATE INDEX IF NOT EXISTS idx_ki_status_updated ON knowledge_items(status, upda CREATE INDEX IF NOT EXISTS idx_ki_keywords ON knowledge_items USING GIN(keywords); -- 知识分块(RAG 检索核心) +-- 基础表不含 embedding 列;若 pgvector 可用则后续通过 DO 块添加 CREATE TABLE IF NOT EXISTS knowledge_chunks ( id TEXT PRIMARY KEY, item_id TEXT NOT NULL REFERENCES knowledge_items(id) ON DELETE CASCADE, chunk_index INT NOT NULL, content TEXT NOT NULL, - embedding vector(1536), keywords TEXT[] DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT NOW() ); @@ -55,11 +55,25 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_kchunks_item_idx ON knowledge_chunks(item_ CREATE INDEX IF NOT EXISTS idx_kchunks_item ON knowledge_chunks(item_id); CREATE INDEX IF NOT EXISTS idx_kchunks_keywords ON knowledge_chunks USING GIN(keywords); --- 向量相似度索引(HNSW,无需预填充数据) --- 仅在有数据后创建此索引可提升性能,这里预创建 -CREATE INDEX IF NOT EXISTS idx_kchunks_embedding ON knowledge_chunks - USING hnsw (embedding vector_cosine_ops) - WITH (m = 16, ef_construction = 128); +-- 条件添加 embedding 列和 HNSW 索引(仅当 pgvector 可用时) +DO $$ BEGIN + PERFORM set_config('client_min_messages', 'warning', true); + CREATE EXTENSION IF NOT EXISTS vector; + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'knowledge_chunks' AND column_name = 'embedding' + ) THEN + ALTER TABLE knowledge_chunks ADD COLUMN embedding vector(1536); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_kchunks_embedding') THEN + CREATE INDEX idx_kchunks_embedding ON knowledge_chunks + USING hnsw (embedding vector_cosine_ops) + WITH (m = 16, ef_construction = 128); + END IF; + RAISE NOTICE 'pgvector enabled for knowledge base'; +EXCEPTION WHEN OTHERS THEN + RAISE NOTICE 'pgvector not available, vector features disabled: %', SQLERRM; +END $$; -- 版本快照 CREATE TABLE IF NOT EXISTS knowledge_versions ( @@ -89,7 +103,6 @@ CREATE TABLE IF NOT EXISTS knowledge_usage ( created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_ku_item ON knowledge_usage(item_id) WHERE item_id IS NOT NULL; --- BRIN 索引:追加写入的时间序列数据比 B-tree 更高效 CREATE INDEX IF NOT EXISTS idx_ku_created_brin ON knowledge_usage USING brin(created_at); -- 权限种子数据(使用 jsonb 操作避免 REPLACE 脆弱性) diff --git a/crates/zclaw-saas/src/db.rs b/crates/zclaw-saas/src/db.rs index 3f41252..95bd13d 100644 --- a/crates/zclaw-saas/src/db.rs +++ b/crates/zclaw-saas/src/db.rs @@ -577,7 +577,7 @@ async fn seed_demo_data(pool: &PgPool) -> SaasResult<()> { ("demo-agent-coder", "Code Assistant", "A helpful coding assistant that can write, review, and debug code", "coding", "gpt-4o", "You are an expert coding assistant. Help users write clean, efficient code.", - "[\"code_search\",\"code_edit\",\"terminal\"]", "[\"code_generation\",\"code_review\",\"debugging\"]", + "[\"file_read\",\"file_write\",\"shell_exec\"]", "[\"code_generation\",\"code_review\",\"debugging\"]", 0.3, 8192, "你是一位资深全栈工程师,擅长代码编写、评审和调试。你追求简洁高效的代码风格,注重可读性和可维护性。", "[\"代码编写\",\"代码审查\",\"Bug调试\",\"架构设计\"]", @@ -587,7 +587,7 @@ async fn seed_demo_data(pool: &PgPool) -> SaasResult<()> { ("demo-agent-writer", "Content Writer", "Creative writing and content generation agent", "creative", "claude-sonnet-4-20250514", "You are a skilled content writer. Create engaging, well-structured content.", - "[\"web_search\",\"document_edit\"]", "[\"writing\",\"editing\",\"summarization\"]", + "[\"web_fetch\",\"file_write\"]", "[\"writing\",\"editing\",\"summarization\"]", 0.7, 4096, "你是一位创意写作专家,擅长各类文案创作、内容编辑和摘要生成。你善于把握文字的节奏和情感表达。", "[\"文章写作\",\"文案创作\",\"内容编辑\",\"摘要生成\"]", @@ -597,7 +597,7 @@ async fn seed_demo_data(pool: &PgPool) -> SaasResult<()> { ("demo-agent-analyst", "Data Analyst", "Data analysis and visualization specialist", "analytics", "gpt-4o", "You are a data analysis expert. Help users analyze data and create visualizations.", - "[\"code_execution\",\"data_access\"]", "[\"data_analysis\",\"visualization\",\"statistics\"]", + "[\"shell_exec\",\"file_read\"]", "[\"data_analysis\",\"visualization\",\"statistics\"]", 0.2, 8192, "你是一位数据分析专家,擅长统计分析、数据可视化和洞察提取。你善于从数据中发现有价值的模式和趋势。", "[\"数据分析\",\"可视化报表\",\"统计建模\",\"趋势预测\"]", @@ -607,7 +607,7 @@ async fn seed_demo_data(pool: &PgPool) -> SaasResult<()> { ("demo-agent-researcher", "Research Agent", "Deep research and information synthesis agent", "research", "gemini-2.5-pro", "You are a research specialist. Conduct thorough research and synthesize findings.", - "[\"web_search\",\"document_access\"]", "[\"research\",\"synthesis\",\"citation\"]", + "[\"web_fetch\",\"file_read\"]", "[\"research\",\"synthesis\",\"citation\"]", 0.4, 16384, "你是一位深度研究专家,擅长信息检索、文献综述和知识综合。你注重信息来源的可靠性和引用的准确性。", "[\"深度研究\",\"文献综述\",\"信息检索\",\"知识综合\"]", @@ -627,7 +627,7 @@ async fn seed_demo_data(pool: &PgPool) -> SaasResult<()> { ("demo-agent-medical", "医疗助手", "Clinical decision support and medical literature assistant", "healthcare", "gpt-4o", "You are a medical AI assistant. Help with clinical decision support, literature retrieval, and medication reference. Always remind users that your suggestions do not replace professional medical advice.", - "[\"web_search\",\"document_access\"]", "[\"clinical_support\",\"literature_search\",\"diagnosis_assist\",\"medication_ref\"]", + "[\"web_fetch\",\"file_read\"]", "[\"clinical_support\",\"literature_search\",\"diagnosis_assist\",\"medication_ref\"]", 0.2, 16384, "你是一位医疗AI助手,具备丰富的临床知识。你辅助临床决策、文献检索和用药参考。\n\n重要提示:\n- 你的建议仅供医疗专业人员参考\n- 不能替代正式的医疗诊断\n- 涉及患者安全的问题需格外谨慎\n- 始终建议用户咨询专业医生", "[\"临床辅助\",\"文献检索\",\"诊断建议\",\"用药参考\"]", diff --git a/crates/zclaw-saas/src/relay/service.rs b/crates/zclaw-saas/src/relay/service.rs index 0c04409..22479f7 100644 --- a/crates/zclaw-saas/src/relay/service.rs +++ b/crates/zclaw-saas/src/relay/service.rs @@ -13,8 +13,8 @@ use super::types::*; /// 上游无数据时,发送 SSE 心跳注释行的间隔 const STREAMBRIDGE_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(15); -/// 上游无数据时,丢弃连接的超时阈值 -const STREAMBRIDGE_TIMEOUT: Duration = Duration::from_secs(30); +/// 上游无数据时,丢弃连接的超时阈值(90s = 6 个心跳,给 thinking 模型更多时间) +const STREAMBRIDGE_TIMEOUT: Duration = Duration::from_secs(90); /// 流结束后延迟清理的时间窗口 const STREAMBRIDGE_CLEANUP_DELAY: Duration = Duration::from_secs(60); @@ -526,9 +526,9 @@ fn build_stream_bridge( idle_heartbeats as u64 * STREAMBRIDGE_HEARTBEAT_INTERVAL.as_secs(), ); - // After 2 consecutive heartbeats without real data (30s), + // After 6 consecutive heartbeats without real data (90s), // terminate the stream to prevent connection leaks. - if idle_heartbeats >= 2 { + if idle_heartbeats >= 6 { tracing::warn!( "[StreamBridge] Timeout ({:?}) no real data, closing stream for task {}", STREAMBRIDGE_TIMEOUT, diff --git a/desktop/src/components/AgentOnboardingWizard.tsx b/desktop/src/components/AgentOnboardingWizard.tsx index 237a94b..9c1fde4 100644 --- a/desktop/src/components/AgentOnboardingWizard.tsx +++ b/desktop/src/components/AgentOnboardingWizard.tsx @@ -134,6 +134,12 @@ export function AgentOnboardingWizard({ isOpen, onClose, onSuccess }: AgentOnboa personality: full.personality || prev.personality, scenarios: full.scenarios.length > 0 ? full.scenarios : prev.scenarios, })); + + // Persist template assignment to SaaS backend (fire-and-forget) + useSaaSStore.getState().assignTemplate(t.id).catch((err: unknown) => { + log.warn('Failed to assign template to account:', err); + }); + setCurrentStep(1); } catch { // If fetch fails, still allow manual creation diff --git a/desktop/src/lib/saas-relay-client.ts b/desktop/src/lib/saas-relay-client.ts index 0256d27..47f09aa 100644 --- a/desktop/src/lib/saas-relay-client.ts +++ b/desktop/src/lib/saas-relay-client.ts @@ -157,6 +157,16 @@ export function createSaaSRelayGatewayClient( try { const parsed = JSON.parse(data); + + // Handle SSE error events from relay (e.g. stream_timeout) + if (parsed.error) { + const errMsg = parsed.message || parsed.error || 'Unknown stream error'; + log.warn('SSE stream error:', errMsg); + callbacks.onError(errMsg); + callbacks.onComplete(); + return { runId }; + } + const choices = parsed.choices?.[0]; if (!choices) continue; diff --git a/desktop/src/store/agentStore.ts b/desktop/src/store/agentStore.ts index 099b889..7067e51 100644 --- a/desktop/src/store/agentStore.ts +++ b/desktop/src/store/agentStore.ts @@ -194,13 +194,37 @@ export const useAgentStore = create((set, get) => ({ const cloneId = result?.clone?.id; - // Persist SOUL.md via identity system - if (cloneId && template.soul_content) { - try { - const { intelligenceClient } = await import('../lib/intelligence-client'); - await intelligenceClient.identity.updateFile(cloneId, 'soul', template.soul_content); - } catch (e) { - console.warn('Failed to persist soul_content:', e); + if (cloneId) { + // Persist SOUL.md via identity system + if (template.soul_content) { + try { + const { intelligenceClient } = await import('../lib/intelligence-client'); + await intelligenceClient.identity.updateFile(cloneId, 'soul', template.soul_content); + } catch (e) { + console.warn('Failed to persist soul_content:', e); + } + } + + // Persist system_prompt via identity system + if (template.system_prompt) { + try { + const { intelligenceClient } = await import('../lib/intelligence-client'); + await intelligenceClient.identity.updateFile(cloneId, 'system', template.system_prompt); + } catch (e) { + console.warn('Failed to persist system_prompt:', e); + } + } + + // Persist temperature / max_tokens if supported + if (template.temperature != null || template.max_tokens != null) { + try { + await client.updateClone(cloneId, { + temperature: template.temperature, + maxTokens: template.max_tokens, + }); + } catch (e) { + console.warn('Failed to persist temperature/max_tokens:', e); + } } }