fix(saas): industry template audit fixes + pgvector optional + relay timeout
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
- Fix seed template tools to match actual runtime tool names (file_read/file_write/shell_exec/web_fetch) - Persist system_prompt/temperature/max_tokens via identity system in agentStore.createFromTemplate() - Fire-and-forget assignTemplate() in AgentOnboardingWizard - Fix saas-relay-client unused variable warning - Make pgvector extension optional in knowledge_base migration - Increase StreamBridge timeout from 30s to 90s for thinking models
This commit is contained in:
@@ -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,
|
-- 5 tables: knowledge_categories, knowledge_items, knowledge_chunks,
|
||||||
-- knowledge_versions, knowledge_usage
|
-- knowledge_versions, knowledge_usage
|
||||||
|
--
|
||||||
-- Enable pgvector extension
|
-- pgvector is optional: if the extension is not installed, the embedding
|
||||||
CREATE EXTENSION IF NOT EXISTS vector;
|
-- column and HNSW index are skipped. All other tables work normally.
|
||||||
|
|
||||||
-- 行业分类树
|
-- 行业分类树
|
||||||
CREATE TABLE IF NOT EXISTS knowledge_categories (
|
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);
|
CREATE INDEX IF NOT EXISTS idx_ki_keywords ON knowledge_items USING GIN(keywords);
|
||||||
|
|
||||||
-- 知识分块(RAG 检索核心)
|
-- 知识分块(RAG 检索核心)
|
||||||
|
-- 基础表不含 embedding 列;若 pgvector 可用则后续通过 DO 块添加
|
||||||
CREATE TABLE IF NOT EXISTS knowledge_chunks (
|
CREATE TABLE IF NOT EXISTS knowledge_chunks (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
item_id TEXT NOT NULL REFERENCES knowledge_items(id) ON DELETE CASCADE,
|
item_id TEXT NOT NULL REFERENCES knowledge_items(id) ON DELETE CASCADE,
|
||||||
chunk_index INT NOT NULL,
|
chunk_index INT NOT NULL,
|
||||||
content TEXT NOT NULL,
|
content TEXT NOT NULL,
|
||||||
embedding vector(1536),
|
|
||||||
keywords TEXT[] DEFAULT '{}',
|
keywords TEXT[] DEFAULT '{}',
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
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_item ON knowledge_chunks(item_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_kchunks_keywords ON knowledge_chunks USING GIN(keywords);
|
CREATE INDEX IF NOT EXISTS idx_kchunks_keywords ON knowledge_chunks USING GIN(keywords);
|
||||||
|
|
||||||
-- 向量相似度索引(HNSW,无需预填充数据)
|
-- 条件添加 embedding 列和 HNSW 索引(仅当 pgvector 可用时)
|
||||||
-- 仅在有数据后创建此索引可提升性能,这里预创建
|
DO $$ BEGIN
|
||||||
CREATE INDEX IF NOT EXISTS idx_kchunks_embedding ON knowledge_chunks
|
PERFORM set_config('client_min_messages', 'warning', true);
|
||||||
USING hnsw (embedding vector_cosine_ops)
|
CREATE EXTENSION IF NOT EXISTS vector;
|
||||||
WITH (m = 16, ef_construction = 128);
|
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 (
|
CREATE TABLE IF NOT EXISTS knowledge_versions (
|
||||||
@@ -89,7 +103,6 @@ CREATE TABLE IF NOT EXISTS knowledge_usage (
|
|||||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_ku_item ON knowledge_usage(item_id) WHERE item_id IS NOT NULL;
|
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);
|
CREATE INDEX IF NOT EXISTS idx_ku_created_brin ON knowledge_usage USING brin(created_at);
|
||||||
|
|
||||||
-- 权限种子数据(使用 jsonb 操作避免 REPLACE 脆弱性)
|
-- 权限种子数据(使用 jsonb 操作避免 REPLACE 脆弱性)
|
||||||
|
|||||||
@@ -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",
|
("demo-agent-coder", "Code Assistant", "A helpful coding assistant that can write, review, and debug code",
|
||||||
"coding", "gpt-4o",
|
"coding", "gpt-4o",
|
||||||
"You are an expert coding assistant. Help users write clean, efficient code.",
|
"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,
|
0.3, 8192,
|
||||||
"你是一位资深全栈工程师,擅长代码编写、评审和调试。你追求简洁高效的代码风格,注重可读性和可维护性。",
|
"你是一位资深全栈工程师,擅长代码编写、评审和调试。你追求简洁高效的代码风格,注重可读性和可维护性。",
|
||||||
"[\"代码编写\",\"代码审查\",\"Bug调试\",\"架构设计\"]",
|
"[\"代码编写\",\"代码审查\",\"Bug调试\",\"架构设计\"]",
|
||||||
@@ -587,7 +587,7 @@ async fn seed_demo_data(pool: &PgPool) -> SaasResult<()> {
|
|||||||
("demo-agent-writer", "Content Writer", "Creative writing and content generation agent",
|
("demo-agent-writer", "Content Writer", "Creative writing and content generation agent",
|
||||||
"creative", "claude-sonnet-4-20250514",
|
"creative", "claude-sonnet-4-20250514",
|
||||||
"You are a skilled content writer. Create engaging, well-structured content.",
|
"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,
|
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",
|
("demo-agent-analyst", "Data Analyst", "Data analysis and visualization specialist",
|
||||||
"analytics", "gpt-4o",
|
"analytics", "gpt-4o",
|
||||||
"You are a data analysis expert. Help users analyze data and create visualizations.",
|
"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,
|
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",
|
("demo-agent-researcher", "Research Agent", "Deep research and information synthesis agent",
|
||||||
"research", "gemini-2.5-pro",
|
"research", "gemini-2.5-pro",
|
||||||
"You are a research specialist. Conduct thorough research and synthesize findings.",
|
"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,
|
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",
|
("demo-agent-medical", "医疗助手", "Clinical decision support and medical literature assistant",
|
||||||
"healthcare", "gpt-4o",
|
"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.",
|
"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,
|
0.2, 16384,
|
||||||
"你是一位医疗AI助手,具备丰富的临床知识。你辅助临床决策、文献检索和用药参考。\n\n重要提示:\n- 你的建议仅供医疗专业人员参考\n- 不能替代正式的医疗诊断\n- 涉及患者安全的问题需格外谨慎\n- 始终建议用户咨询专业医生",
|
"你是一位医疗AI助手,具备丰富的临床知识。你辅助临床决策、文献检索和用药参考。\n\n重要提示:\n- 你的建议仅供医疗专业人员参考\n- 不能替代正式的医疗诊断\n- 涉及患者安全的问题需格外谨慎\n- 始终建议用户咨询专业医生",
|
||||||
"[\"临床辅助\",\"文献检索\",\"诊断建议\",\"用药参考\"]",
|
"[\"临床辅助\",\"文献检索\",\"诊断建议\",\"用药参考\"]",
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ use super::types::*;
|
|||||||
/// 上游无数据时,发送 SSE 心跳注释行的间隔
|
/// 上游无数据时,发送 SSE 心跳注释行的间隔
|
||||||
const STREAMBRIDGE_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(15);
|
const STREAMBRIDGE_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(15);
|
||||||
|
|
||||||
/// 上游无数据时,丢弃连接的超时阈值
|
/// 上游无数据时,丢弃连接的超时阈值(90s = 6 个心跳,给 thinking 模型更多时间)
|
||||||
const STREAMBRIDGE_TIMEOUT: Duration = Duration::from_secs(30);
|
const STREAMBRIDGE_TIMEOUT: Duration = Duration::from_secs(90);
|
||||||
|
|
||||||
/// 流结束后延迟清理的时间窗口
|
/// 流结束后延迟清理的时间窗口
|
||||||
const STREAMBRIDGE_CLEANUP_DELAY: Duration = Duration::from_secs(60);
|
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(),
|
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.
|
// terminate the stream to prevent connection leaks.
|
||||||
if idle_heartbeats >= 2 {
|
if idle_heartbeats >= 6 {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"[StreamBridge] Timeout ({:?}) no real data, closing stream for task {}",
|
"[StreamBridge] Timeout ({:?}) no real data, closing stream for task {}",
|
||||||
STREAMBRIDGE_TIMEOUT,
|
STREAMBRIDGE_TIMEOUT,
|
||||||
|
|||||||
@@ -134,6 +134,12 @@ export function AgentOnboardingWizard({ isOpen, onClose, onSuccess }: AgentOnboa
|
|||||||
personality: full.personality || prev.personality,
|
personality: full.personality || prev.personality,
|
||||||
scenarios: full.scenarios.length > 0 ? full.scenarios : prev.scenarios,
|
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);
|
setCurrentStep(1);
|
||||||
} catch {
|
} catch {
|
||||||
// If fetch fails, still allow manual creation
|
// If fetch fails, still allow manual creation
|
||||||
|
|||||||
@@ -157,6 +157,16 @@ export function createSaaSRelayGatewayClient(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(data);
|
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];
|
const choices = parsed.choices?.[0];
|
||||||
if (!choices) continue;
|
if (!choices) continue;
|
||||||
|
|
||||||
|
|||||||
@@ -194,13 +194,37 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
|
|||||||
|
|
||||||
const cloneId = result?.clone?.id;
|
const cloneId = result?.clone?.id;
|
||||||
|
|
||||||
// Persist SOUL.md via identity system
|
if (cloneId) {
|
||||||
if (cloneId && template.soul_content) {
|
// Persist SOUL.md via identity system
|
||||||
try {
|
if (template.soul_content) {
|
||||||
const { intelligenceClient } = await import('../lib/intelligence-client');
|
try {
|
||||||
await intelligenceClient.identity.updateFile(cloneId, 'soul', template.soul_content);
|
const { intelligenceClient } = await import('../lib/intelligence-client');
|
||||||
} catch (e) {
|
await intelligenceClient.identity.updateFile(cloneId, 'soul', template.soul_content);
|
||||||
console.warn('Failed to persist soul_content:', e);
|
} 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user