Compare commits
2 Commits
c048cb215f
...
fd3e7fd2cb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd3e7fd2cb | ||
|
|
c167ea4ea5 |
@@ -545,7 +545,7 @@ refactor(store): 统一 Store 数据获取方式
|
||||
| Pipeline DSL | ✅ 稳定 | 04-01 17 个 YAML 模板 + DAG 执行器 |
|
||||
| Hands 系统 | ✅ 稳定 | 9 启用 (Browser/Collector/Researcher/Twitter/Whiteboard/Slideshow/Speech/Quiz/Clip) |
|
||||
| 技能系统 (Skills) | ✅ 稳定 | 75 个 SKILL.md + 语义路由 |
|
||||
| 中间件链 | ✅ 稳定 | 14 层 (含 DataMasking@90, ButlerRouter, TrajectoryRecorder@650) |
|
||||
| 中间件链 | ✅ 稳定 | 15 层 (含 DataMasking@90, ButlerRouter, TrajectoryRecorder@650 — V13注册) |
|
||||
|
||||
### 关键架构模式
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ import type { ProColumns } from '@ant-design/pro-components'
|
||||
import { ProTable } from '@ant-design/pro-components'
|
||||
import { knowledgeService } from '@/services/knowledge'
|
||||
import type { CategoryResponse, KnowledgeItem, SearchResult } from '@/services/knowledge'
|
||||
import type { StructuredSource } from '@/services/knowledge'
|
||||
import { TableOutlined } from '@ant-design/icons'
|
||||
|
||||
const { TextArea } = Input
|
||||
const { Text, Title } = Typography
|
||||
@@ -708,12 +710,138 @@ export default function Knowledge() {
|
||||
icon: <BarChartOutlined />,
|
||||
children: <AnalyticsPanel />,
|
||||
},
|
||||
{
|
||||
key: 'structured',
|
||||
label: '结构化数据',
|
||||
icon: <TableOutlined />,
|
||||
children: <StructuredSourcesPanel />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// === Structured Data Sources Panel ===
|
||||
|
||||
function StructuredSourcesPanel() {
|
||||
const queryClient = useQueryClient()
|
||||
const [viewingRows, setViewingRows] = useState<string | null>(null)
|
||||
|
||||
const { data: sources = [], isLoading } = useQuery({
|
||||
queryKey: ['structured-sources'],
|
||||
queryFn: ({ signal }) => knowledgeService.listStructuredSources(signal),
|
||||
})
|
||||
|
||||
const { data: rows = [], isLoading: rowsLoading } = useQuery({
|
||||
queryKey: ['structured-rows', viewingRows],
|
||||
queryFn: ({ signal }) => knowledgeService.listStructuredRows(viewingRows!, signal),
|
||||
enabled: !!viewingRows,
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: string) => knowledgeService.deleteStructuredSource(id),
|
||||
onSuccess: () => {
|
||||
message.success('数据源已删除')
|
||||
queryClient.invalidateQueries({ queryKey: ['structured-sources'] })
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '删除失败'),
|
||||
})
|
||||
|
||||
const columns: ProColumns<StructuredSource>[] = [
|
||||
{ title: '名称', dataIndex: 'name', key: 'name', width: 200 },
|
||||
{ title: '类型', dataIndex: 'source_type', key: 'source_type', width: 120, render: (v: string) => <Tag>{v}</Tag> },
|
||||
{ title: '行数', dataIndex: 'row_count', key: 'row_count', width: 80 },
|
||||
{
|
||||
title: '列',
|
||||
dataIndex: 'columns',
|
||||
key: 'columns',
|
||||
width: 250,
|
||||
render: (cols: string[]) => (
|
||||
<Space size={[4, 4]} wrap>
|
||||
{(cols ?? []).slice(0, 5).map((c) => (
|
||||
<Tag key={c} color="blue">{c}</Tag>
|
||||
))}
|
||||
{(cols ?? []).length > 5 && <Tag>+{(cols as string[]).length - 5}</Tag>}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 160,
|
||||
render: (v: string) => new Date(v).toLocaleString('zh-CN'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 140,
|
||||
render: (_: unknown, record: StructuredSource) => (
|
||||
<Space>
|
||||
<Button type="link" size="small" onClick={() => setViewingRows(record.id)}>
|
||||
查看数据
|
||||
</Button>
|
||||
<Popconfirm title="确认删除此数据源?" onConfirm={() => deleteMutation.mutate(record.id)}>
|
||||
<Button type="link" size="small" danger>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
// Dynamically generate row columns from the first row's keys
|
||||
const rowColumns = rows.length > 0
|
||||
? Object.keys(rows[0].row_data).map((key) => ({
|
||||
title: key,
|
||||
dataIndex: ['row_data', key],
|
||||
key,
|
||||
ellipsis: true,
|
||||
render: (v: unknown) => String(v ?? ''),
|
||||
}))
|
||||
: []
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{viewingRows ? (
|
||||
<Card
|
||||
title="数据行"
|
||||
extra={<Button onClick={() => setViewingRows(null)}>返回列表</Button>}
|
||||
>
|
||||
{rowsLoading ? (
|
||||
<Spin />
|
||||
) : rows.length === 0 ? (
|
||||
<Empty description="暂无数据" />
|
||||
) : (
|
||||
<Table
|
||||
dataSource={rows}
|
||||
columns={rowColumns}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
scroll={{ x: true }}
|
||||
pagination={{ pageSize: 20 }}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
) : (
|
||||
<ProTable<StructuredSource>
|
||||
dataSource={sources}
|
||||
columns={columns}
|
||||
loading={isLoading}
|
||||
rowKey="id"
|
||||
search={false}
|
||||
pagination={{ pageSize: 20 }}
|
||||
toolBarRender={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// === 辅助函数 ===
|
||||
|
||||
// === 辅助函数 ===
|
||||
|
||||
function flattenCategories(cats: CategoryResponse[]): { id: string; name: string }[] {
|
||||
|
||||
@@ -62,6 +62,33 @@ export interface ListItemsResponse {
|
||||
page_size: number
|
||||
}
|
||||
|
||||
// === Structured Data Sources ===
|
||||
|
||||
export interface StructuredSource {
|
||||
id: string
|
||||
account_id: string
|
||||
name: string
|
||||
source_type: string
|
||||
row_count: number
|
||||
columns: string[]
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface StructuredRow {
|
||||
id: string
|
||||
source_id: string
|
||||
row_data: Record<string, unknown>
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface StructuredQueryResult {
|
||||
row_id: string
|
||||
source_name: string
|
||||
row_data: Record<string, unknown>
|
||||
score: number
|
||||
}
|
||||
|
||||
// === Service ===
|
||||
|
||||
export const knowledgeService = {
|
||||
@@ -159,4 +186,23 @@ export const knowledgeService = {
|
||||
// 导入
|
||||
importItems: (data: { category_id: string; files: Array<{ content: string; title?: string; keywords?: string[]; tags?: string[] }> }) =>
|
||||
request.post('/knowledge/items/import', data).then((r) => r.data),
|
||||
|
||||
// === Structured Data Sources ===
|
||||
listStructuredSources: (signal?: AbortSignal) =>
|
||||
request.get<StructuredSource[]>('/structured/sources', withSignal({}, signal))
|
||||
.then((r) => r.data),
|
||||
|
||||
getStructuredSource: (id: string, signal?: AbortSignal) =>
|
||||
request.get<StructuredSource>(`/structured/sources/${id}`, withSignal({}, signal))
|
||||
.then((r) => r.data),
|
||||
|
||||
deleteStructuredSource: (id: string) =>
|
||||
request.delete(`/structured/sources/${id}`).then((r) => r.data),
|
||||
|
||||
listStructuredRows: (sourceId: string, signal?: AbortSignal) =>
|
||||
request.get<StructuredRow[]>(`/structured/sources/${sourceId}/rows`, withSignal({}, signal))
|
||||
.then((r) => r.data),
|
||||
|
||||
queryStructured: (data: { source_id?: string; query?: string; limit?: number }) =>
|
||||
request.post<StructuredQueryResult[]>('/structured/query', data).then((r) => r.data),
|
||||
}
|
||||
|
||||
@@ -353,6 +353,14 @@ impl Kernel {
|
||||
chain.register(Arc::new(mw));
|
||||
}
|
||||
|
||||
// Trajectory recorder — record agent loop events for Hermes analysis
|
||||
{
|
||||
use std::sync::Arc;
|
||||
let tstore = zclaw_memory::trajectory_store::TrajectoryStore::new(self.memory.pool());
|
||||
let mw = zclaw_runtime::middleware::trajectory_recorder::TrajectoryRecorderMiddleware::new(Arc::new(tstore));
|
||||
chain.register(Arc::new(mw));
|
||||
}
|
||||
|
||||
// Only return Some if we actually registered middleware
|
||||
if chain.is_empty() {
|
||||
None
|
||||
|
||||
@@ -21,6 +21,14 @@ impl MemoryStore {
|
||||
Ok(store)
|
||||
}
|
||||
|
||||
/// Get a clone of the underlying SQLite pool.
|
||||
///
|
||||
/// Used by subsystems (e.g. `TrajectoryStore`) that need to share the
|
||||
/// same database connection pool for their own tables.
|
||||
pub fn pool(&self) -> SqlitePool {
|
||||
self.pool.clone()
|
||||
}
|
||||
|
||||
/// Ensure the parent directory for the database file exists
|
||||
fn ensure_database_dir(database_url: &str) -> Result<()> {
|
||||
// Parse SQLite URL to extract file path
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
-- NOTE: DEPRECATED — These tables are defined but NOT consumed by any Rust code.
|
||||
-- Kept for schema compatibility. Will be removed in a future cleanup pass.
|
||||
-- See: V13 audit FIX-04
|
||||
|
||||
-- Webhook subscriptions: external endpoints that receive event notifications
|
||||
CREATE TABLE IF NOT EXISTS webhook_subscriptions (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -26,3 +30,10 @@ CREATE TABLE IF NOT EXISTS webhook_deliveries (
|
||||
CREATE INDEX IF NOT EXISTS idx_webhook_subscriptions_account ON webhook_subscriptions(account_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_webhook_subscriptions_events ON webhook_subscriptions USING gin(events);
|
||||
CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_pending ON webhook_deliveries(subscription_id) WHERE delivered_at IS NULL;
|
||||
|
||||
-- === DOWN MIGRATION ===
|
||||
-- DROP INDEX IF EXISTS idx_webhook_deliveries_pending;
|
||||
-- DROP INDEX IF EXISTS idx_webhook_subscriptions_events;
|
||||
-- DROP INDEX IF EXISTS idx_webhook_subscriptions_account;
|
||||
-- DROP TABLE IF EXISTS webhook_deliveries;
|
||||
-- DROP TABLE IF EXISTS webhook_subscriptions;
|
||||
|
||||
@@ -143,7 +143,11 @@ pub struct PersistentMemoryStore {
|
||||
conn: Arc<Mutex<SqliteConnection>>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // Legacy: operations migrated to VikingStorage, kept for backward compat
|
||||
#[allow(dead_code)]
|
||||
// Migration status (V13 audit FIX-06):
|
||||
// - ACTIVE: new(), configure_embedding_client() — embedding config path for chat memory search
|
||||
// - LEGACY: store(), get(), search(), delete(), stats(), export_all(), import_batch() — data ops moved to VikingStorage
|
||||
// - Full removal requires migrating embedding config to VikingStorage (~3h, tracked in AUDIT_TRACKER)
|
||||
impl PersistentMemoryStore {
|
||||
/// Create a new persistent memory store
|
||||
pub async fn new(app_handle: &tauri::AppHandle) -> Result<Self, String> {
|
||||
@@ -587,7 +591,7 @@ fn sanitize_fts_query(query: &str) -> String {
|
||||
}
|
||||
|
||||
/// Generate a unique memory ID
|
||||
#[allow(dead_code)] // Legacy: kept for potential migration use
|
||||
#[allow(dead_code)] // Legacy: VikingStorage generates its own URIs
|
||||
pub fn generate_memory_id() -> String {
|
||||
let uuid_str = Uuid::new_v4().to_string().replace("-", "");
|
||||
let short_uuid = &uuid_str[..8];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useButlerInsights } from '../../hooks/useButlerInsights';
|
||||
import { useChatStore } from '../../store/chatStore';
|
||||
import { useIndustryStore } from '../../store/industryStore';
|
||||
import { InsightsSection } from './InsightsSection';
|
||||
import { ProposalsSection } from './ProposalsSection';
|
||||
import { MemorySection } from './MemorySection';
|
||||
@@ -12,8 +13,16 @@ interface ButlerPanelProps {
|
||||
export function ButlerPanel({ agentId }: ButlerPanelProps) {
|
||||
const { painPoints, proposals, loading, error, refresh } = useButlerInsights(agentId);
|
||||
const messageCount = useChatStore((s) => s.messages.length);
|
||||
const { accountIndustries, configs, lastSynced, isLoading: industryLoading, fetchIndustries } = useIndustryStore();
|
||||
const [analyzing, setAnalyzing] = useState(false);
|
||||
|
||||
// Auto-fetch industry configs once per session
|
||||
useEffect(() => {
|
||||
if (accountIndustries.length === 0 && !industryLoading) {
|
||||
fetchIndustries().catch(() => {/* SaaS unavailable — ignore */});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const hasData = (painPoints?.length ?? 0) > 0 || (proposals?.length ?? 0) > 0;
|
||||
const canAnalyze = messageCount >= 2;
|
||||
|
||||
@@ -100,6 +109,45 @@ export function ButlerPanel({ agentId }: ButlerPanelProps) {
|
||||
</h3>
|
||||
<MemorySection agentId={agentId} />
|
||||
</div>
|
||||
|
||||
{/* Industry section */}
|
||||
{accountIndustries.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
||||
行业专长
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{accountIndustries.map((item) => {
|
||||
const config = configs[item.industry_id];
|
||||
const keywords = config?.keywords ?? [];
|
||||
return (
|
||||
<div key={item.industry_id} className="rounded-lg border border-gray-200 dark:border-gray-700 p-2.5">
|
||||
<div className="text-xs font-medium text-gray-800 dark:text-gray-200">
|
||||
{item.industry_name || item.industry_id}
|
||||
</div>
|
||||
{keywords.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-1.5">
|
||||
{keywords.slice(0, 8).map((kw) => (
|
||||
<span key={kw} className="inline-block text-[10px] px-1.5 py-0.5 rounded bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400">
|
||||
{kw}
|
||||
</span>
|
||||
))}
|
||||
{keywords.length > 8 && (
|
||||
<span className="text-[10px] text-gray-400">+{keywords.length - 8}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{lastSynced && (
|
||||
<div className="text-[10px] text-gray-400 dark:text-gray-500">
|
||||
同步于 {new Date(lastSynced).toLocaleString('zh-CN')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
FileText,
|
||||
Database,
|
||||
Sparkles,
|
||||
BookOpen,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
getVikingStatus,
|
||||
@@ -22,6 +23,9 @@ import {
|
||||
storeWithSummaries,
|
||||
} from '../lib/viking-client';
|
||||
import type { VikingStatus, VikingFindResult } from '../lib/viking-client';
|
||||
import { saasClient } from '../lib/saas-client';
|
||||
import type { KnowledgeSearchResult } from '../lib/saas-client';
|
||||
import { useSaaSStore } from '../store/saasStore';
|
||||
|
||||
export function VikingPanel() {
|
||||
const [status, setStatus] = useState<VikingStatus | null>(null);
|
||||
@@ -38,6 +42,12 @@ export function VikingPanel() {
|
||||
const [summaryUri, setSummaryUri] = useState('');
|
||||
const [summaryContent, setSummaryContent] = useState('');
|
||||
|
||||
// SaaS knowledge search state
|
||||
const [kbQuery, setKbQuery] = useState('');
|
||||
const [kbResults, setKbResults] = useState<KnowledgeSearchResult[]>([]);
|
||||
const [isKbSearching, setIsKbSearching] = useState(false);
|
||||
const saasReady = useSaaSStore((s) => s.isLoggedIn);
|
||||
|
||||
const loadStatus = async () => {
|
||||
setIsLoading(true);
|
||||
setMessage(null);
|
||||
@@ -88,6 +98,19 @@ export function VikingPanel() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleKbSearch = async () => {
|
||||
if (!kbQuery.trim()) return;
|
||||
setIsKbSearching(true);
|
||||
try {
|
||||
const results = await saasClient.searchKnowledge(kbQuery, { limit: 10 });
|
||||
setKbResults(results);
|
||||
} catch {
|
||||
setKbResults([]);
|
||||
} finally {
|
||||
setIsKbSearching(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExpandL2 = async (uri: string) => {
|
||||
if (expandedUri === uri) {
|
||||
setExpandedUri(null);
|
||||
@@ -299,6 +322,68 @@ export function VikingPanel() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* SaaS Knowledge Base Search */}
|
||||
{saasReady && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 mb-6 shadow-sm">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-white mb-3 flex items-center gap-2">
|
||||
<BookOpen className="w-4 h-4 text-indigo-500" />
|
||||
知识库搜索
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
搜索 SaaS 端共享知识库
|
||||
</p>
|
||||
<div className="flex gap-2 mb-3">
|
||||
<input
|
||||
type="text"
|
||||
value={kbQuery}
|
||||
onChange={(e) => setKbQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleKbSearch()}
|
||||
placeholder="搜索知识库..."
|
||||
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
/>
|
||||
<button
|
||||
onClick={handleKbSearch}
|
||||
disabled={isKbSearching || !kbQuery.trim()}
|
||||
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 flex items-center gap-2 text-sm"
|
||||
>
|
||||
{isKbSearching ? (
|
||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Search className="w-4 h-4" />
|
||||
)}
|
||||
搜索
|
||||
</button>
|
||||
</div>
|
||||
{kbResults.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{kbResults.map((r) => (
|
||||
<div key={r.chunk_id} className="p-2.5 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-xs font-medium text-gray-800 dark:text-gray-200 truncate">
|
||||
{r.item_title}
|
||||
</span>
|
||||
{r.category_name && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400">
|
||||
{r.category_name}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[10px] text-blue-500">{Math.round(r.score * 100)}%</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-gray-600 dark:text-gray-300 line-clamp-2">{r.content}</p>
|
||||
{r.keywords.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{r.keywords.slice(0, 5).map((kw) => (
|
||||
<span key={kw} className="text-[10px] px-1 py-0.5 rounded bg-cyan-50 dark:bg-cyan-900/20 text-cyan-600 dark:text-cyan-400">{kw}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Summary Generation */}
|
||||
{status?.available && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 mb-6 shadow-sm">
|
||||
|
||||
@@ -70,6 +70,7 @@ export type {
|
||||
IndustryInfo,
|
||||
IndustryFullConfig,
|
||||
AccountIndustryItem,
|
||||
KnowledgeSearchResult,
|
||||
} from './saas-types';
|
||||
|
||||
export { SaaSApiError } from './saas-errors';
|
||||
@@ -114,6 +115,7 @@ import { installPromptMethods } from './saas-prompt';
|
||||
import { installTelemetryMethods } from './saas-telemetry';
|
||||
import { installBillingMethods } from './saas-billing';
|
||||
import { installIndustryMethods } from './saas-industry';
|
||||
import { installKnowledgeMethods } from './saas-knowledge';
|
||||
export type { UsageIncrementResult } from './saas-billing';
|
||||
|
||||
// Re-export billing types for convenience
|
||||
@@ -448,6 +450,7 @@ installPromptMethods(SaaSClient);
|
||||
installTelemetryMethods(SaaSClient);
|
||||
installBillingMethods(SaaSClient);
|
||||
installIndustryMethods(SaaSClient);
|
||||
installKnowledgeMethods(SaaSClient);
|
||||
export { installBillingMethods };
|
||||
|
||||
// === API Method Type Declarations ===
|
||||
@@ -511,6 +514,9 @@ export interface SaaSClient {
|
||||
getIndustryFullConfig(industryId: string): Promise<import('./saas-types').IndustryFullConfig>;
|
||||
getMyIndustries(): Promise<import('./saas-types').AccountIndustryItem[]>;
|
||||
getAccountIndustries(accountId: string): Promise<import('./saas-types').AccountIndustryItem[]>;
|
||||
|
||||
// --- Knowledge (saas-knowledge.ts) ---
|
||||
searchKnowledge(query: string, opts?: { category_id?: string; limit?: number }): Promise<import('./saas-types').KnowledgeSearchResult[]>;
|
||||
}
|
||||
|
||||
// === Singleton ===
|
||||
|
||||
25
desktop/src/lib/saas-knowledge.ts
Normal file
25
desktop/src/lib/saas-knowledge.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* SaaS Knowledge Methods — Mixin
|
||||
*
|
||||
* Installs knowledge-search methods onto SaaSClient.prototype.
|
||||
*/
|
||||
|
||||
import type { KnowledgeSearchResult } from './saas-types';
|
||||
|
||||
export function installKnowledgeMethods(ClientClass: { prototype: any }): void {
|
||||
const proto = ClientClass.prototype;
|
||||
|
||||
/**
|
||||
* Search the SaaS knowledge base.
|
||||
*/
|
||||
proto.searchKnowledge = async function (
|
||||
this: { request<T>(method: string, path: string, body?: unknown): Promise<T> },
|
||||
query: string,
|
||||
opts?: { category_id?: string; limit?: number },
|
||||
): Promise<KnowledgeSearchResult[]> {
|
||||
return this.request('POST', '/api/v1/knowledge/search', {
|
||||
query,
|
||||
...opts,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -272,6 +272,17 @@ export interface AccountIndustryItem {
|
||||
industry_icon: string;
|
||||
}
|
||||
|
||||
/** Knowledge search result from POST /api/v1/knowledge/search */
|
||||
export interface KnowledgeSearchResult {
|
||||
chunk_id: string;
|
||||
item_id: string;
|
||||
item_title: string;
|
||||
category_name: string;
|
||||
content: string;
|
||||
score: number;
|
||||
keywords: string[];
|
||||
}
|
||||
|
||||
/** Provider info from GET /api/v1/providers */
|
||||
export interface ProviderInfo {
|
||||
id: string;
|
||||
|
||||
@@ -332,17 +332,17 @@
|
||||
|
||||
| ID | 问题 | 状态 | 验证方法 |
|
||||
|----|------|------|----------|
|
||||
| V13-GAP-01 | TrajectoryRecorderMiddleware 未注册到中间件链 | **OPEN** | `grep trajectory crates/zclaw-kernel/src/kernel/mod.rs` — 零匹配。中间件代码存在 (`trajectory_recorder.rs`),有测试,但 `create_middleware_chain()` 中未注册。Schema v4 `trajectory_events` 表已创建但零数据流入。 |
|
||||
| V13-GAP-02 | industryStore 未被任何组件导入 | **OPEN** | `grep from.*industryStore\|useIndustryStore desktop/src/` — 仅自身文件。Store 存在且 `viking_load_industry_keywords` 后端链路正常,但桌面端无 UI 展示行业配置状态。 |
|
||||
| V13-GAP-03 | 桌面端未接入 Knowledge Search API | **OPEN** | `grep knowledge/search desktop/src/` — 零匹配。SaaS 有 `POST /api/v1/knowledge/search` 端点,Admin-v2 已消费,但桌面端不搜索知识库。 |
|
||||
| V13-GAP-01 | TrajectoryRecorderMiddleware 未注册到中间件链 | **FIXED** | `grep trajectory crates/zclaw-kernel/src/kernel/mod.rs` — 已注册 @650 优先级。通过 MemoryStore.pool() 创建 TrajectoryStore。 |
|
||||
| V13-GAP-02 | industryStore 未被任何组件导入 | **FIXED** | ButlerPanel/index.tsx 导入 useIndustryStore,展示行业专长卡片 + 关键词标签,自动拉取。 |
|
||||
| V13-GAP-03 | 桌面端未接入 Knowledge Search API | **FIXED** | saas-knowledge.ts mixin + VikingPanel SaaS KB 搜索 UI (登录后可见)。 |
|
||||
|
||||
### P2: 高优先级
|
||||
|
||||
| ID | 问题 | 状态 | 验证方法 |
|
||||
|----|------|------|----------|
|
||||
| V13-GAP-04 | Webhook 孤儿迁移 (代码已删但表仍在) | **OPEN** | `crates/zclaw-saas/migrations/20260403000002_webhooks.sql` 存在,但 `crates/zclaw-saas/src/` 中零 webhook 代码 |
|
||||
| V13-GAP-05 | Structured Data Source 5 路由无 Admin UI | **OPEN** | `grep structured admin-v2/src/services/knowledge.ts` — 零匹配 |
|
||||
| V13-GAP-06 | PersistentMemoryStore 遗留模块 | **OPEN** | `desktop/src-tauri/src/memory/persistent.rs` 有 `#[allow(dead_code)]`,数据流已统一到 VikingStorage |
|
||||
| V13-GAP-04 | Webhook 孤儿迁移 (代码已删但表仍在) | **FIXED** | 标注 deprecated + 添加 down migration 注释,schema 兼容保留 |
|
||||
| V13-GAP-05 | Structured Data Source 5 路由无 Admin UI | **FIXED** | Knowledge.tsx 新增"结构化数据"Tab,CRUD + 行浏览 |
|
||||
| V13-GAP-06 | PersistentMemoryStore 遗留模块 | **PARTIALLY_FIXED** | 精化 dead_code 标注 (活跃: embedding config / 遗留: data ops)。完整迁移至 VikingStorage 留后续 (~3h) |
|
||||
|
||||
### V13 修正 V12 错误认知
|
||||
|
||||
@@ -365,4 +365,10 @@
|
||||
| 2026-04-12 | V13-GAP-04 | NEW | Webhook 孤儿迁移 |
|
||||
| 2026-04-12 | V13-GAP-05 | NEW | Structured Data Source 无 Admin UI |
|
||||
| 2026-04-12 | V13-GAP-06 | NEW | PersistentMemoryStore 遗留 |
|
||||
| 2026-04-13 | V13-GAP-01 | FIXED | 注册 TrajectoryRecorderMiddleware @650 + MemoryStore.pool() getter |
|
||||
| 2026-04-13 | V13-GAP-02 | FIXED | ButlerPanel 行业专长展示 + auto-fetch |
|
||||
| 2026-04-13 | V13-GAP-03 | FIXED | saas-knowledge mixin + VikingPanel SaaS KB UI |
|
||||
| 2026-04-13 | V13-GAP-04 | FIXED | deprecated 标注 + down migration 注释 |
|
||||
| 2026-04-13 | V13-GAP-05 | FIXED | Knowledge.tsx 结构化数据 Tab |
|
||||
| 2026-04-13 | V13-GAP-06 | PARTIALLY_FIXED | dead_code 精化标注,完整迁移留后续 |
|
||||
| 2026-04-12 | - | V13 修正 | Butler/MCP/Gateway/Presentation 已接通,非孤儿命令 |
|
||||
|
||||
@@ -34,7 +34,7 @@ status: active
|
||||
| Zustand Store | 17 文件 + chat/4子store = 21 | `ls desktop/src/store/` |
|
||||
| React 组件 | 104 个 (.tsx/.ts) | `find desktop/src/components/` |
|
||||
| Admin V2 页面 | 15 个 (.tsx) | `ls admin-v2/src/pages/` |
|
||||
| 中间件 | 14 层 runtime + 6 层 SaaS HTTP | `kernel/mod.rs` + `zclaw-saas middleware` |
|
||||
| 中间件 | 15 层 runtime + 6 层 SaaS HTTP | `kernel/mod.rs` + `zclaw-saas middleware` |
|
||||
| 前端 lib/ | 85 个 .ts | `find desktop/src/lib/` |
|
||||
| TODO/FIXME | 前端 4 + Rust 4 = 8 | `grep TODO/FIXME` |
|
||||
|
||||
|
||||
12
wiki/log.md
12
wiki/log.md
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: 变更日志
|
||||
updated: 2026-04-12
|
||||
updated: 2026-04-13
|
||||
status: active
|
||||
tags: [log, history]
|
||||
---
|
||||
@@ -191,7 +191,15 @@ Phase 5 — 主动行为激活 (3 files, 152 insertions):
|
||||
|
||||
> 更新规则: 每次重大变更后追加一条,最新在最上面
|
||||
|
||||
### [2026-04-11] 详情面板 7 问题修复
|
||||
### [2026-04-13] V13 审计 6 项修复全部完成
|
||||
|
||||
- FIX-01 (P1): TrajectoryRecorderMiddleware 注册到 create_middleware_chain() @650,Hermes 轨迹数据开始流入
|
||||
- FIX-02 (P1): industryStore 接入 ButlerPanel,桌面端展示行业专长卡片 + 自动拉取
|
||||
- FIX-03 (P1): 桌面端知识库搜索 — saas-knowledge mixin + VikingPanel SaaS KB 搜索 UI
|
||||
- FIX-04 (P2): Webhook 孤儿迁移标注 deprecated + down migration 注释
|
||||
- FIX-05 (P2): Admin Knowledge 新增"结构化数据"Tab (CRUD + 行浏览)
|
||||
- FIX-06 (P2): PersistentMemoryStore dead_code 精化标注 (完整迁移留后续)
|
||||
- 文件: 11 个 (Rust 4 + TS 7), 358 行新增, 1 次提交
|
||||
|
||||
- P0: memory_search 空查询 min_similarity 默认值; hand_trigger null→handAutoTrigger; 重启后 chat 路由竞态修复
|
||||
- P1: AgentInfo 扩展 UserProfile 桥接; 反思阈值降低 5→3; 反思 state restore peek+pop 竞态修复
|
||||
|
||||
@@ -20,7 +20,7 @@ tags: [module, middleware, runtime]
|
||||
|
||||
## 代码逻辑
|
||||
|
||||
### 14 层中间件(注册顺序见 `kernel/mod.rs:190-345`)
|
||||
### 15 层中间件(注册顺序见 `kernel/mod.rs:190-360`)
|
||||
|
||||
| # | 中间件 | 文件 | 职责 | 注册条件 |
|
||||
|---|--------|------|------|----------|
|
||||
@@ -37,7 +37,7 @@ tags: [module, middleware, runtime]
|
||||
| 11 | ToolOutputGuard | `middleware/tool_output_guard.rs` | 工具输出安全检查 | 始终 |
|
||||
| 12 | Guardrail | `middleware/guardrail.rs` | shell_exec/file_write/web_fetch 安全规则 | 始终 |
|
||||
| 13 | SubagentLimit | `middleware/subagent_limit.rs` | 限制并发子 agent | 始终 |
|
||||
| 14 | TrajectoryRecorder | `middleware/trajectory_recorder.rs` | 轨迹记录 + 压缩 | 始终 |
|
||||
| 14 | TrajectoryRecorder | `middleware/trajectory_recorder.rs` | 轨迹记录 + 压缩 | 始终 (V13-FIX-01 注册) |
|
||||
|
||||
### 优先级分类(来自 `middleware.rs` 头注释)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user