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

- 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:
iven
2026-04-03 15:10:13 +08:00
parent ea00c32c08
commit 1048901665
6 changed files with 80 additions and 27 deletions

View File

@@ -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 脆弱性)

View File

@@ -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- 始终建议用户咨询专业医生",
"[\"临床辅助\",\"文献检索\",\"诊断建议\",\"用药参考\"]",

View File

@@ -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,

View File

@@ -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

View File

@@ -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;

View File

@@ -194,13 +194,37 @@ export const useAgentStore = create<AgentStore>((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);
}
}
}