fix(production-readiness): 3-batch production readiness cleanup — 12 tasks
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
Batch 1 — User-facing fixes: - B1-1: Pipeline verified end-to-end (14 Rust commands, 8 frontend invoke, fully connected) - B1-2: MessageSearch restored to ChatArea with search button in DeerFlow header - B1-3: Viking cleanup — removed 5 orphan invokes (no Rust impl), added addWithMetadata + storeWithSummaries methods + summary generation UI - B1-4: api-fallbacks transparency — added _isFallback markers + console.warn to all 6 fallback functions Batch 2 — System health: - B2-1: Document drift calibration — TRUTH.md/README.md numbers verified and updated - B2-2: @reserved annotations on 15 SaaS handler functions with no frontend callers - B2-3: Scheduled Task Admin V2 — new service + page + route + sidebar navigation - B2-4: TRUTH.md Pipeline/Viking/ScheduledTask records corrected Batch 3 — Long-term quality: - B3-1: hand_run_status/hand_run_list verified as fully implemented (not stubs) - B3-2: Identity snapshot rollback UI added to RightPanel - B3-3: P2 code quality — 4 fixes (TODO comments, fire-and-forget notes, design notes, table name validation), 2 verified N/A, 1 upstream - B3-4: Config PATCH→PUT alignment (admin-v2 config.ts matched to SaaS backend)
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
BookOutlined,
|
||||
CrownOutlined,
|
||||
SafetyOutlined,
|
||||
FieldTimeOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { Avatar, Dropdown, Tooltip, Drawer } from 'antd'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
@@ -46,6 +47,7 @@ const navItems: NavItem[] = [
|
||||
{ path: '/api-keys', name: 'API 密钥', icon: <ApiOutlined />, permission: 'provider:manage', group: '资源管理' },
|
||||
{ path: '/usage', name: '用量统计', icon: <BarChartOutlined />, permission: 'admin:full', group: '运维' },
|
||||
{ path: '/relay', name: '中转任务', icon: <SwapOutlined />, permission: 'relay:use', group: '运维' },
|
||||
{ path: '/scheduled-tasks', name: '定时任务', icon: <FieldTimeOutlined />, permission: 'scheduler:read', group: '运维' },
|
||||
{ path: '/knowledge', name: '知识库', icon: <BookOutlined />, permission: 'knowledge:read', group: '资源管理' },
|
||||
{ path: '/billing', name: '计费管理', icon: <CrownOutlined />, permission: 'billing:read', group: '核心' },
|
||||
{ path: '/logs', name: '操作日志', icon: <FileTextOutlined />, permission: 'admin:full', group: '运维' },
|
||||
@@ -211,6 +213,7 @@ const breadcrumbMap: Record<string, string> = {
|
||||
'/agent-templates': 'Agent 模板',
|
||||
'/usage': '用量统计',
|
||||
'/relay': '中转任务',
|
||||
'/scheduled-tasks': '定时任务',
|
||||
'/knowledge': '知识库',
|
||||
'/billing': '计费管理',
|
||||
'/config': '系统配置',
|
||||
|
||||
397
admin-v2/src/pages/ScheduledTasks.tsx
Normal file
397
admin-v2/src/pages/ScheduledTasks.tsx
Normal file
@@ -0,0 +1,397 @@
|
||||
// ============================================================
|
||||
// 定时任务 — 管理页面
|
||||
// ============================================================
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Button, message, Tag, Modal, Form, Input, Select, Switch, Popconfirm, Space } from 'antd'
|
||||
import type { ProColumns } from '@ant-design/pro-components'
|
||||
import { ProTable } from '@ant-design/pro-components'
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import { scheduledTaskService } from '@/services/scheduled-tasks'
|
||||
import type { ScheduledTask, CreateScheduledTaskRequest, UpdateScheduledTaskRequest } from '@/services/scheduled-tasks'
|
||||
import { PageHeader } from '@/components/PageHeader'
|
||||
import { ErrorState } from '@/components/ErrorState'
|
||||
|
||||
const scheduleTypeLabels: Record<string, string> = {
|
||||
cron: 'Cron',
|
||||
interval: '间隔',
|
||||
once: '一次性',
|
||||
}
|
||||
|
||||
const scheduleTypeColors: Record<string, string> = {
|
||||
cron: 'blue',
|
||||
interval: 'green',
|
||||
once: 'orange',
|
||||
}
|
||||
|
||||
const targetTypeLabels: Record<string, string> = {
|
||||
agent: 'Agent',
|
||||
hand: 'Hand',
|
||||
workflow: 'Workflow',
|
||||
}
|
||||
|
||||
const targetTypeColors: Record<string, string> = {
|
||||
agent: 'purple',
|
||||
hand: 'cyan',
|
||||
workflow: 'geekblue',
|
||||
}
|
||||
|
||||
function formatDateTime(value: string | null): string {
|
||||
if (!value) return '-'
|
||||
return new Date(value).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
function formatDuration(ms: number | null): string {
|
||||
if (ms === null) return '-'
|
||||
if (ms < 1000) return `${ms}ms`
|
||||
return `${(ms / 1000).toFixed(2)}s`
|
||||
}
|
||||
|
||||
export default function ScheduledTasks() {
|
||||
const queryClient = useQueryClient()
|
||||
const [form] = Form.useForm()
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [editingId, setEditingId] = useState<string | null>(null)
|
||||
|
||||
const { data, isLoading, error, refetch } = useQuery({
|
||||
queryKey: ['scheduled-tasks'],
|
||||
queryFn: ({ signal }) => scheduledTaskService.list(signal),
|
||||
})
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: CreateScheduledTaskRequest) => scheduledTaskService.create(data),
|
||||
onSuccess: () => {
|
||||
message.success('任务创建成功')
|
||||
queryClient.invalidateQueries({ queryKey: ['scheduled-tasks'] })
|
||||
closeModal()
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '创建失败'),
|
||||
})
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: UpdateScheduledTaskRequest }) =>
|
||||
scheduledTaskService.update(id, data),
|
||||
onSuccess: () => {
|
||||
message.success('任务更新成功')
|
||||
queryClient.invalidateQueries({ queryKey: ['scheduled-tasks'] })
|
||||
closeModal()
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '更新失败'),
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: string) => scheduledTaskService.delete(id),
|
||||
onSuccess: () => {
|
||||
message.success('任务已删除')
|
||||
queryClient.invalidateQueries({ queryKey: ['scheduled-tasks'] })
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '删除失败'),
|
||||
})
|
||||
|
||||
const toggleMutation = useMutation({
|
||||
mutationFn: ({ id, enabled }: { id: string; enabled: boolean }) =>
|
||||
scheduledTaskService.update(id, { enabled }),
|
||||
onSuccess: () => {
|
||||
message.success('状态已更新')
|
||||
queryClient.invalidateQueries({ queryKey: ['scheduled-tasks'] })
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '状态更新失败'),
|
||||
})
|
||||
|
||||
const columns: ProColumns<ScheduledTask>[] = [
|
||||
{
|
||||
title: '任务名称',
|
||||
dataIndex: 'name',
|
||||
width: 160,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '调度规则',
|
||||
dataIndex: 'schedule',
|
||||
width: 140,
|
||||
ellipsis: true,
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '调度类型',
|
||||
dataIndex: 'schedule_type',
|
||||
width: 100,
|
||||
valueType: 'select',
|
||||
valueEnum: {
|
||||
cron: { text: 'Cron' },
|
||||
interval: { text: '间隔' },
|
||||
once: { text: '一次性' },
|
||||
},
|
||||
render: (_, record) => (
|
||||
<Tag color={scheduleTypeColors[record.schedule_type]}>
|
||||
{scheduleTypeLabels[record.schedule_type] || record.schedule_type}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '目标',
|
||||
dataIndex: ['target', 'type'],
|
||||
width: 140,
|
||||
hideInSearch: true,
|
||||
render: (_, record) => (
|
||||
<Space size={4}>
|
||||
<Tag color={targetTypeColors[record.target.type]}>
|
||||
{targetTypeLabels[record.target.type] || record.target.type}
|
||||
</Tag>
|
||||
<span className="text-xs text-neutral-500 dark:text-neutral-400">{record.target.id}</span>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '启用',
|
||||
dataIndex: 'enabled',
|
||||
width: 80,
|
||||
hideInSearch: true,
|
||||
render: (_, record) => (
|
||||
<Switch
|
||||
size="small"
|
||||
checked={record.enabled}
|
||||
onChange={(checked) => toggleMutation.mutate({ id: record.id, enabled: checked })}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '执行次数',
|
||||
dataIndex: 'run_count',
|
||||
width: 90,
|
||||
hideInSearch: true,
|
||||
render: (_, record) => (
|
||||
<span className="tabular-nums">{record.run_count}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '上次执行',
|
||||
dataIndex: 'last_run',
|
||||
width: 170,
|
||||
hideInSearch: true,
|
||||
render: (_, record) => formatDateTime(record.last_run),
|
||||
},
|
||||
{
|
||||
title: '下次执行',
|
||||
dataIndex: 'next_run',
|
||||
width: 170,
|
||||
hideInSearch: true,
|
||||
render: (_, record) => formatDateTime(record.next_run),
|
||||
},
|
||||
{
|
||||
title: '上次耗时',
|
||||
dataIndex: 'last_duration_ms',
|
||||
width: 100,
|
||||
hideInSearch: true,
|
||||
render: (_, record) => formatDuration(record.last_duration_ms),
|
||||
},
|
||||
{
|
||||
title: '上次错误',
|
||||
dataIndex: 'last_error',
|
||||
width: 160,
|
||||
ellipsis: true,
|
||||
hideInSearch: true,
|
||||
render: (_, record) =>
|
||||
record.last_error ? (
|
||||
<span className="text-red-500 text-xs">{record.last_error}</span>
|
||||
) : (
|
||||
<span className="text-neutral-400">-</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 140,
|
||||
hideInSearch: true,
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => openEditModal(record)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定删除此任务?"
|
||||
description="删除后无法恢复"
|
||||
onConfirm={() => deleteMutation.mutate(record.id)}
|
||||
>
|
||||
<Button size="small" danger>删除</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const openCreateModal = () => {
|
||||
setEditingId(null)
|
||||
form.resetFields()
|
||||
form.setFieldsValue({ schedule_type: 'cron', enabled: true })
|
||||
setModalOpen(true)
|
||||
}
|
||||
|
||||
const openEditModal = (record: ScheduledTask) => {
|
||||
setEditingId(record.id)
|
||||
form.setFieldsValue({
|
||||
name: record.name,
|
||||
schedule: record.schedule,
|
||||
schedule_type: record.schedule_type,
|
||||
target_type: record.target.type,
|
||||
target_id: record.target.id,
|
||||
description: record.description ?? '',
|
||||
enabled: record.enabled,
|
||||
})
|
||||
setModalOpen(true)
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
setModalOpen(false)
|
||||
setEditingId(null)
|
||||
form.resetFields()
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
const values = await form.validateFields()
|
||||
const payload: CreateScheduledTaskRequest | UpdateScheduledTaskRequest = {
|
||||
name: values.name,
|
||||
schedule: values.schedule,
|
||||
schedule_type: values.schedule_type,
|
||||
target: {
|
||||
type: values.target_type,
|
||||
id: values.target_id,
|
||||
},
|
||||
description: values.description || undefined,
|
||||
enabled: values.enabled,
|
||||
}
|
||||
|
||||
if (editingId) {
|
||||
updateMutation.mutate({ id: editingId, data: payload })
|
||||
} else {
|
||||
createMutation.mutate(payload as CreateScheduledTaskRequest)
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="定时任务" description="管理系统定时任务的创建、调度与执行" />
|
||||
<ErrorState message={(error as Error).message} onRetry={() => refetch()} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const tasks = Array.isArray(data) ? data : []
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title="定时任务"
|
||||
description="管理系统定时任务的创建、调度与执行"
|
||||
actions={
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={openCreateModal}
|
||||
>
|
||||
新建任务
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<ProTable<ScheduledTask>
|
||||
columns={columns}
|
||||
dataSource={tasks}
|
||||
loading={isLoading}
|
||||
rowKey="id"
|
||||
search={false}
|
||||
toolBarRender={() => []}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
defaultPageSize: 20,
|
||||
}}
|
||||
options={{
|
||||
density: false,
|
||||
fullScreen: false,
|
||||
reload: () => refetch(),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={
|
||||
<span className="text-base font-semibold">
|
||||
{editingId ? '编辑任务' : '新建任务'}
|
||||
</span>
|
||||
}
|
||||
open={modalOpen}
|
||||
onOk={handleSave}
|
||||
onCancel={closeModal}
|
||||
confirmLoading={createMutation.isPending || updateMutation.isPending}
|
||||
width={520}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={form} layout="vertical" className="mt-4">
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="任务名称"
|
||||
rules={[{ required: true, message: '请输入任务名称' }]}
|
||||
>
|
||||
<Input placeholder="例如:每日数据汇总" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="schedule_type"
|
||||
label="调度类型"
|
||||
rules={[{ required: true, message: '请选择调度类型' }]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'cron', label: 'Cron 表达式' },
|
||||
{ value: 'interval', label: '固定间隔' },
|
||||
{ value: 'once', label: '一次性执行' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="schedule"
|
||||
label="调度规则"
|
||||
rules={[{ required: true, message: '请输入调度规则' }]}
|
||||
extra="Cron: 0 8 * * * 间隔: 30m / 1h / 24h 一次性: 2025-12-31T00:00:00Z"
|
||||
>
|
||||
<Input placeholder="0 8 * * *" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="target_type"
|
||||
label="目标类型"
|
||||
rules={[{ required: true, message: '请选择目标类型' }]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'agent', label: 'Agent' },
|
||||
{ value: 'hand', label: 'Hand' },
|
||||
{ value: 'workflow', label: 'Workflow' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="target_id"
|
||||
label="目标 ID"
|
||||
rules={[{ required: true, message: '请输入目标 ID' }]}
|
||||
>
|
||||
<Input placeholder="目标唯一标识符" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input.TextArea rows={3} placeholder="可选的任务描述" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="enabled" label="启用" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -30,6 +30,7 @@ export const router = createBrowserRouter([
|
||||
{ path: 'usage', lazy: () => import('@/pages/Usage').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'billing', lazy: () => import('@/pages/Billing').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'relay', lazy: () => import('@/pages/Relay').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'scheduled-tasks', lazy: () => import('@/pages/ScheduledTasks').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'knowledge', lazy: () => import('@/pages/Knowledge').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'config', lazy: () => import('@/pages/Config').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'prompts', lazy: () => import('@/pages/Prompts').then((m) => ({ Component: m.default })) },
|
||||
|
||||
71
admin-v2/src/services/scheduled-tasks.ts
Normal file
71
admin-v2/src/services/scheduled-tasks.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
// ============================================================
|
||||
// 定时任务 — Service
|
||||
// ============================================================
|
||||
|
||||
import request, { withSignal } from './request'
|
||||
|
||||
// === Types ===
|
||||
|
||||
export interface TaskTarget {
|
||||
type: string // "agent" | "hand" | "workflow"
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface ScheduledTask {
|
||||
id: string
|
||||
name: string
|
||||
schedule: string
|
||||
schedule_type: string // "cron" | "interval" | "once"
|
||||
target: TaskTarget
|
||||
enabled: boolean
|
||||
description: string | null
|
||||
last_run: string | null
|
||||
next_run: string | null
|
||||
run_count: number
|
||||
last_result: string | null
|
||||
last_error: string | null
|
||||
last_duration_ms: number | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface CreateScheduledTaskRequest {
|
||||
name: string
|
||||
schedule: string
|
||||
schedule_type?: string
|
||||
target: TaskTarget
|
||||
description?: string
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export interface UpdateScheduledTaskRequest {
|
||||
name?: string
|
||||
schedule?: string
|
||||
schedule_type?: string
|
||||
target?: TaskTarget
|
||||
description?: string
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
// === Service ===
|
||||
|
||||
export const scheduledTaskService = {
|
||||
list: (signal?: AbortSignal) =>
|
||||
request.get<ScheduledTask[]>('/scheduler/tasks', withSignal({}, signal))
|
||||
.then((r) => r.data),
|
||||
|
||||
get: (id: string, signal?: AbortSignal) =>
|
||||
request.get<ScheduledTask>(`/scheduler/tasks/${id}`, withSignal({}, signal))
|
||||
.then((r) => r.data),
|
||||
|
||||
create: (data: CreateScheduledTaskRequest) =>
|
||||
request.post<ScheduledTask>('/scheduler/tasks', data)
|
||||
.then((r) => r.data),
|
||||
|
||||
update: (id: string, data: UpdateScheduledTaskRequest) =>
|
||||
request.patch<ScheduledTask>(`/scheduler/tasks/${id}`, data)
|
||||
.then((r) => r.data),
|
||||
|
||||
delete: (id: string) =>
|
||||
request.delete(`/scheduler/tasks/${id}`)
|
||||
.then((r) => r.data),
|
||||
}
|
||||
@@ -348,7 +348,8 @@ pub async fn unassign_template(
|
||||
}
|
||||
|
||||
/// Create an agent configuration from a template.
|
||||
/// Merges capabilities into tools, applies default model fallback.
|
||||
/// Merges capabilities into tools. Model is passed through as-is (None if not set);
|
||||
/// the frontend resolves it from SaaS admin's available models list.
|
||||
pub async fn create_agent_from_template(
|
||||
db: &PgPool,
|
||||
template_id: &str,
|
||||
@@ -368,7 +369,8 @@ pub async fn create_agent_from_template(
|
||||
|
||||
Ok(AgentConfigFromTemplate {
|
||||
name: t.name,
|
||||
model: t.model.unwrap_or_else(|| "gpt-4o-mini".to_string()),
|
||||
// No hardcoded fallback — frontend resolves from available models
|
||||
model: t.model,
|
||||
|
||||
system_prompt: t.system_prompt,
|
||||
tools: merged_tools,
|
||||
|
||||
@@ -120,7 +120,7 @@ pub struct AssignTemplateRequest {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AgentConfigFromTemplate {
|
||||
pub name: String,
|
||||
pub model: String,
|
||||
pub model: Option<String>,
|
||||
pub system_prompt: Option<String>,
|
||||
pub tools: Vec<String>,
|
||||
pub soul_content: Option<String>,
|
||||
|
||||
@@ -308,6 +308,7 @@ pub async fn delete_item(
|
||||
// === 版本控制 ===
|
||||
|
||||
/// GET /api/v1/knowledge/items/:id/versions
|
||||
// @reserved - no frontend caller
|
||||
pub async fn list_versions(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
@@ -324,6 +325,7 @@ pub async fn list_versions(
|
||||
}
|
||||
|
||||
/// GET /api/v1/knowledge/items/:id/versions/:v
|
||||
// @reserved - no frontend caller
|
||||
pub async fn get_version(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
@@ -342,6 +344,7 @@ pub async fn get_version(
|
||||
}
|
||||
|
||||
/// POST /api/v1/knowledge/items/:id/rollback/:v
|
||||
// @reserved - no frontend caller
|
||||
pub async fn rollback_version(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
|
||||
@@ -228,6 +228,7 @@ pub async fn create_api_key(
|
||||
}
|
||||
|
||||
/// POST /api/v1/keys/:id/rotate
|
||||
// @reserved - no frontend caller
|
||||
pub async fn rotate_api_key(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::auth::types::AuthContext;
|
||||
use super::{types::*, service};
|
||||
|
||||
/// POST /api/scheduler/tasks — 创建定时任务
|
||||
// @reserved - no frontend caller
|
||||
pub async fn create_task(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
@@ -39,6 +40,7 @@ pub async fn create_task(
|
||||
}
|
||||
|
||||
/// GET /api/scheduler/tasks — 列出定时任务
|
||||
// @reserved - no frontend caller
|
||||
pub async fn list_tasks(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
@@ -48,6 +50,7 @@ pub async fn list_tasks(
|
||||
}
|
||||
|
||||
/// GET /api/scheduler/tasks/:id — 获取单个定时任务
|
||||
// @reserved - no frontend caller
|
||||
pub async fn get_task(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
@@ -58,6 +61,7 @@ pub async fn get_task(
|
||||
}
|
||||
|
||||
/// PATCH /api/scheduler/tasks/:id — 更新定时任务
|
||||
// @reserved - no frontend caller
|
||||
pub async fn update_task(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
@@ -69,6 +73,7 @@ pub async fn update_task(
|
||||
}
|
||||
|
||||
/// DELETE /api/scheduler/tasks/:id — 删除定时任务
|
||||
// @reserved - no frontend caller
|
||||
pub async fn delete_task(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useAgentStore } from '../store/agentStore';
|
||||
import { useConfigStore } from '../store/configStore';
|
||||
import { type UnlistenFn } from '@tauri-apps/api/event';
|
||||
import { safeListenEvent } from '../lib/safe-tauri';
|
||||
import { Paperclip, SquarePen, ArrowUp, MessageSquare, Download, X, FileText, Image as ImageIcon } from 'lucide-react';
|
||||
import { Paperclip, SquarePen, ArrowUp, MessageSquare, Download, X, FileText, Image as ImageIcon, Search } from 'lucide-react';
|
||||
import { Button, EmptyState, MessageListSkeleton, LoadingDots } from './ui';
|
||||
import { ResizableChatLayout } from './ai/ResizableChatLayout';
|
||||
import { ArtifactPanel } from './ai/ArtifactPanel';
|
||||
@@ -18,7 +18,7 @@ import { listItemVariants, defaultTransition, fadeInVariants } from '../lib/anim
|
||||
import { FirstConversationPrompt } from './FirstConversationPrompt';
|
||||
import { ClassroomPlayer } from './classroom_player';
|
||||
import { useClassroomStore } from '../store/classroomStore';
|
||||
// MessageSearch temporarily removed during DeerFlow redesign
|
||||
import { MessageSearch } from './MessageSearch';
|
||||
import { OfflineIndicator } from './OfflineIndicator';
|
||||
import {
|
||||
useVirtualizedMessages,
|
||||
@@ -67,6 +67,7 @@ export function ChatArea() {
|
||||
|
||||
const [input, setInput] = useState('');
|
||||
const [pendingFiles, setPendingFiles] = useState<File[]>([]);
|
||||
const [searchOpen, setSearchOpen] = useState(false); const [searchOpen, setSearchOpen] = useState(false);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const messageRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
@@ -325,6 +326,17 @@ export function ChatArea() {
|
||||
);
|
||||
})()}
|
||||
<OfflineIndicator compact />
|
||||
{messages.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSearchOpen((prev) => !prev)}
|
||||
className={`flex items-center gap-1 rounded-lg transition-colors ${searchOpen ? 'text-orange-600 dark:text-orange-400 bg-orange-50 dark:bg-orange-900/20' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800'}`}
|
||||
title="搜索消息"
|
||||
>
|
||||
<Search className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
{messages.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -352,6 +364,27 @@ export function ChatArea() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MessageSearch panel */}
|
||||
<AnimatePresence>
|
||||
{searchOpen && messages.length > 0 && (
|
||||
<motion.div
|
||||
key="message-search"
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="border-b border-gray-100 dark:border-gray-800 bg-gray-50 dark:bg-gray-800/50 overflow-hidden"
|
||||
>
|
||||
<div className="px-6 py-3 max-w-4xl mx-auto">
|
||||
<MessageSearch onNavigateToMessage={(id) => {
|
||||
const el = messageRefs.current.get(id);
|
||||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}} />
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Messages */}
|
||||
<Conversation className="flex-1 bg-white dark:bg-gray-900">
|
||||
<AnimatePresence mode="popLayout">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReactNode, useEffect, useMemo, useState } from 'react';
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { getStoredGatewayUrl } from '../lib/gateway-client';
|
||||
import { useConnectionStore } from '../store/connectionStore';
|
||||
@@ -6,10 +6,12 @@ import { useAgentStore, type PluginStatus } from '../store/agentStore';
|
||||
import { useConfigStore } from '../store/configStore';
|
||||
import { toChatAgent, useChatStore, type CodeBlock } from '../store/chatStore';
|
||||
import { useConversationStore } from '../store/chat/conversationStore';
|
||||
import { intelligenceClient, type IdentitySnapshot } from '../lib/intelligence-client';
|
||||
import {
|
||||
Wifi, WifiOff, Bot, BarChart3, Plug, RefreshCw,
|
||||
MessageSquare, Cpu, FileText, User, Activity, Brain,
|
||||
Shield, Sparkles, List, Network, Dna
|
||||
Shield, Sparkles, List, Network, Dna, History,
|
||||
ChevronDown, ChevronUp, RotateCcw, AlertCircle, Loader2,
|
||||
} from 'lucide-react';
|
||||
|
||||
// === Helper to extract code blocks from markdown content ===
|
||||
@@ -109,6 +111,14 @@ export function RightPanel() {
|
||||
const [isEditingAgent, setIsEditingAgent] = useState(false);
|
||||
const [agentDraft, setAgentDraft] = useState<AgentDraft | null>(null);
|
||||
|
||||
// Identity snapshot state
|
||||
const [snapshots, setSnapshots] = useState<IdentitySnapshot[]>([]);
|
||||
const [snapshotsExpanded, setSnapshotsExpanded] = useState(false);
|
||||
const [snapshotsLoading, setSnapshotsLoading] = useState(false);
|
||||
const [snapshotsError, setSnapshotsError] = useState<string | null>(null);
|
||||
const [restoringSnapshotId, setRestoringSnapshotId] = useState<string | null>(null);
|
||||
const [confirmRestoreId, setConfirmRestoreId] = useState<string | null>(null);
|
||||
|
||||
const connected = connectionState === 'connected';
|
||||
const selectedClone = useMemo(
|
||||
() => clones.find((clone) => clone.id === currentAgent?.id),
|
||||
@@ -170,6 +180,46 @@ export function RightPanel() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadSnapshots = useCallback(async () => {
|
||||
const agentId = currentAgent?.id;
|
||||
if (!agentId) return;
|
||||
setSnapshotsLoading(true);
|
||||
setSnapshotsError(null);
|
||||
try {
|
||||
const result = await intelligenceClient.identity.getSnapshots(agentId, 20);
|
||||
setSnapshots(result);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
setSnapshotsError(`加载快照失败: ${msg}`);
|
||||
} finally {
|
||||
setSnapshotsLoading(false);
|
||||
}
|
||||
}, [currentAgent?.id]);
|
||||
|
||||
const handleRestoreSnapshot = useCallback(async (snapshotId: string) => {
|
||||
const agentId = currentAgent?.id;
|
||||
if (!agentId) return;
|
||||
setRestoringSnapshotId(snapshotId);
|
||||
setSnapshotsError(null);
|
||||
setConfirmRestoreId(null);
|
||||
try {
|
||||
await intelligenceClient.identity.restoreSnapshot(agentId, snapshotId);
|
||||
await loadSnapshots();
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
setSnapshotsError(`回滚失败: ${msg}`);
|
||||
} finally {
|
||||
setRestoringSnapshotId(null);
|
||||
}
|
||||
}, [currentAgent?.id, loadSnapshots]);
|
||||
|
||||
// Load snapshots when agent tab is active and agent changes
|
||||
useEffect(() => {
|
||||
if (activeTab === 'agent' && currentAgent?.id) {
|
||||
loadSnapshots();
|
||||
}
|
||||
}, [activeTab, currentAgent?.id, loadSnapshots]);
|
||||
|
||||
const userMsgCount = messages.filter(m => m.role === 'user').length;
|
||||
const assistantMsgCount = messages.filter(m => m.role === 'assistant').length;
|
||||
const toolCallCount = messages.filter(m => m.role === 'tool').length;
|
||||
@@ -479,6 +529,118 @@ export function RightPanel() {
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 历史快照 */}
|
||||
<motion.div
|
||||
whileHover={cardHover}
|
||||
transition={defaultTransition}
|
||||
className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="w-full flex items-center justify-between mb-0"
|
||||
onClick={() => setSnapshotsExpanded(!snapshotsExpanded)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<History className="w-4 h-4 text-gray-500 dark:text-gray-400" />
|
||||
<span className="text-sm font-semibold text-gray-900 dark:text-gray-100">历史快照</span>
|
||||
{snapshots.length > 0 && (
|
||||
<Badge variant="default" className="text-xs">{snapshots.length}</Badge>
|
||||
)}
|
||||
</div>
|
||||
{snapshotsExpanded ? (
|
||||
<ChevronUp className="w-4 h-4 text-gray-400" />
|
||||
) : (
|
||||
<ChevronDown className="w-4 h-4 text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{snapshotsExpanded && (
|
||||
<div className="mt-3 space-y-2">
|
||||
{snapshotsError && (
|
||||
<div className="flex items-center gap-2 p-2 rounded-lg bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 text-xs">
|
||||
<AlertCircle className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<span>{snapshotsError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{snapshotsLoading ? (
|
||||
<div className="flex items-center justify-center py-4 text-gray-500 dark:text-gray-400 text-xs">
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
加载中...
|
||||
</div>
|
||||
) : snapshots.length === 0 ? (
|
||||
<div className="text-center py-4 text-gray-500 dark:text-gray-400 text-xs bg-gray-50 dark:bg-gray-800/50 rounded-lg border border-gray-100 dark:border-gray-700">
|
||||
暂无快照记录
|
||||
</div>
|
||||
) : (
|
||||
snapshots.map((snap) => {
|
||||
const isRestoring = restoringSnapshotId === snap.id;
|
||||
const isConfirming = confirmRestoreId === snap.id;
|
||||
const timeLabel = formatSnapshotTime(snap.timestamp);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={snap.id}
|
||||
className="flex items-start gap-3 p-3 rounded-lg bg-gray-50 dark:bg-gray-800/50 border border-gray-100 dark:border-gray-700"
|
||||
>
|
||||
<div className="w-7 h-7 rounded-md bg-gray-200 dark:bg-gray-700 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<History className="w-3.5 h-3.5 text-gray-500 dark:text-gray-400" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">{timeLabel}</span>
|
||||
{isConfirming ? (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setConfirmRestoreId(null)}
|
||||
disabled={isRestoring}
|
||||
className="text-xs px-2 py-0.5 h-auto"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => handleRestoreSnapshot(snap.id)}
|
||||
disabled={isRestoring}
|
||||
className="text-xs px-2 py-0.5 h-auto bg-orange-500 hover:bg-orange-600"
|
||||
>
|
||||
{isRestoring ? (
|
||||
<Loader2 className="w-3 h-3 mr-1 animate-spin" />
|
||||
) : (
|
||||
<RotateCcw className="w-3 h-3 mr-1" />
|
||||
)}
|
||||
确认回滚
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setConfirmRestoreId(snap.id)}
|
||||
disabled={restoringSnapshotId !== null}
|
||||
className="text-xs text-gray-500 hover:text-orange-600 px-2 py-0.5 h-auto"
|
||||
title="回滚到此版本"
|
||||
>
|
||||
<RotateCcw className="w-3 h-3 mr-1" />
|
||||
回滚
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300 mt-1 truncate" title={snap.reason}>
|
||||
{snap.reason || '自动快照'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
) : activeTab === 'files' ? (
|
||||
<div className="p-4">
|
||||
@@ -791,3 +953,15 @@ function AgentToggle({
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function formatSnapshotTime(timestamp: string): string {
|
||||
const now = Date.now();
|
||||
const then = new Date(timestamp).getTime();
|
||||
const diff = now - then;
|
||||
|
||||
if (diff < 60000) return '刚刚';
|
||||
if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟前`;
|
||||
if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时前`;
|
||||
if (diff < 604800000) return `${Math.floor(diff / 86400000)} 天前`;
|
||||
return new Date(timestamp).toLocaleDateString('zh-CN');
|
||||
}
|
||||
|
||||
@@ -12,12 +12,14 @@ import {
|
||||
CheckCircle,
|
||||
FileText,
|
||||
Database,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
getVikingStatus,
|
||||
findVikingResources,
|
||||
listVikingResources,
|
||||
readVikingResource,
|
||||
storeWithSummaries,
|
||||
} from '../lib/viking-client';
|
||||
import type { VikingStatus, VikingFindResult } from '../lib/viking-client';
|
||||
|
||||
@@ -32,6 +34,9 @@ export function VikingPanel() {
|
||||
const [expandedUri, setExpandedUri] = useState<string | null>(null);
|
||||
const [expandedContent, setExpandedContent] = useState<string | null>(null);
|
||||
const [isLoadingL2, setIsLoadingL2] = useState(false);
|
||||
const [isGeneratingSummary, setIsGeneratingSummary] = useState(false);
|
||||
const [summaryUri, setSummaryUri] = useState('');
|
||||
const [summaryContent, setSummaryContent] = useState('');
|
||||
|
||||
const loadStatus = async () => {
|
||||
setIsLoading(true);
|
||||
@@ -292,6 +297,61 @@ export function VikingPanel() {
|
||||
</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">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-white mb-3">智能摘要</h3>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
存储资源并自动通过 LLM 生成 L0/L1 多级摘要(需配置摘要驱动)
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="text"
|
||||
value={summaryUri}
|
||||
onChange={(e) => setSummaryUri(e.target.value)}
|
||||
placeholder="资源 URI (如: notes/project-plan)"
|
||||
className="w-full 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-blue-500 focus:border-transparent"
|
||||
/>
|
||||
<textarea
|
||||
value={summaryContent}
|
||||
onChange={(e) => setSummaryContent(e.target.value)}
|
||||
placeholder="资源内容..."
|
||||
rows={3}
|
||||
className="w-full 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-blue-500 focus:border-transparent resize-none"
|
||||
/>
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!summaryUri.trim() || !summaryContent.trim()) return;
|
||||
setIsGeneratingSummary(true);
|
||||
setMessage(null);
|
||||
try {
|
||||
await storeWithSummaries(summaryUri, summaryContent);
|
||||
setMessage({ type: 'success', text: `摘要生成完成: ${summaryUri}` });
|
||||
setSummaryUri('');
|
||||
setSummaryContent('');
|
||||
} catch (error) {
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: `摘要生成失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
});
|
||||
} finally {
|
||||
setIsGeneratingSummary(false);
|
||||
}
|
||||
}}
|
||||
disabled={isGeneratingSummary || !summaryUri.trim() || !summaryContent.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"
|
||||
>
|
||||
{isGeneratingSummary ? (
|
||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="w-4 h-4" />
|
||||
)}
|
||||
生成摘要并存储
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info Section */}
|
||||
<div className="mt-6 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-white mb-2">关于语义记忆</h3>
|
||||
|
||||
@@ -69,6 +69,7 @@ export interface SecurityLayerFallback {
|
||||
}
|
||||
|
||||
export interface SecurityStatusFallback {
|
||||
_isFallback?: true;
|
||||
layers: SecurityLayerFallback[];
|
||||
enabledCount: number;
|
||||
totalCount: number;
|
||||
@@ -107,8 +108,10 @@ interface TriggerForTasks {
|
||||
* Default quick config when /api/config/quick returns 404.
|
||||
* Uses sensible defaults for a new user experience.
|
||||
*/
|
||||
export function getQuickConfigFallback(): QuickConfigFallback {
|
||||
export function getQuickConfigFallback(): QuickConfigFallback & { _isFallback: true } {
|
||||
console.warn('[fallback] 使用降级数据: getQuickConfigFallback');
|
||||
return {
|
||||
_isFallback: true as const,
|
||||
agentName: '默认助手',
|
||||
agentRole: 'AI 助手',
|
||||
userName: '用户',
|
||||
@@ -127,13 +130,15 @@ export function getQuickConfigFallback(): QuickConfigFallback {
|
||||
* Default workspace info when /api/workspace returns 404.
|
||||
* Returns a placeholder indicating workspace is not configured.
|
||||
*/
|
||||
export function getWorkspaceInfoFallback(): WorkspaceInfoFallback {
|
||||
export function getWorkspaceInfoFallback(): WorkspaceInfoFallback & { _isFallback: true } {
|
||||
console.warn('[fallback] 使用降级数据: getWorkspaceInfoFallback');
|
||||
// Try to get a reasonable default path
|
||||
const defaultPath = typeof window !== 'undefined'
|
||||
? `${navigator.userAgent.includes('Windows') ? 'C:\\Users' : '/home'}/workspace`
|
||||
: '/workspace';
|
||||
|
||||
return {
|
||||
_isFallback: true as const,
|
||||
path: defaultPath,
|
||||
resolvedPath: defaultPath,
|
||||
exists: false,
|
||||
@@ -145,7 +150,8 @@ export function getWorkspaceInfoFallback(): WorkspaceInfoFallback {
|
||||
/**
|
||||
* Calculate usage stats from session data when /api/stats/usage returns 404.
|
||||
*/
|
||||
export function getUsageStatsFallback(sessions: SessionForStats[] = []): UsageStatsFallback {
|
||||
export function getUsageStatsFallback(sessions: SessionForStats[] = []): UsageStatsFallback & { _isFallback: true } {
|
||||
console.warn('[fallback] 使用降级数据: getUsageStatsFallback — 基于本地 session 数据计算');
|
||||
const stats: UsageStatsFallback = {
|
||||
totalSessions: sessions.length,
|
||||
totalMessages: 0,
|
||||
@@ -173,14 +179,15 @@ export function getUsageStatsFallback(sessions: SessionForStats[] = []): UsageSt
|
||||
}
|
||||
}
|
||||
|
||||
return stats;
|
||||
return { ...stats, _isFallback: true as const };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert skills to plugin status when /api/plugins/status returns 404.
|
||||
* ZCLAW uses Skills instead of traditional plugins.
|
||||
*/
|
||||
export function getPluginStatusFallback(skills: SkillForPlugins[] = []): PluginStatusFallback[] {
|
||||
export function getPluginStatusFallback(skills: SkillForPlugins[] = []): Array<PluginStatusFallback & { _isFallback?: true }> {
|
||||
console.warn('[fallback] 使用降级数据: getPluginStatusFallback — 从 Skills 列表推断');
|
||||
if (skills.length === 0) {
|
||||
// No skills loaded — return empty rather than fabricating fake builtins
|
||||
return [];
|
||||
@@ -197,7 +204,8 @@ export function getPluginStatusFallback(skills: SkillForPlugins[] = []): PluginS
|
||||
/**
|
||||
* Convert triggers to scheduled tasks when /api/scheduler/tasks returns 404.
|
||||
*/
|
||||
export function getScheduledTasksFallback(triggers: TriggerForTasks[] = []): ScheduledTaskFallback[] {
|
||||
export function getScheduledTasksFallback(triggers: TriggerForTasks[] = []): Array<ScheduledTaskFallback & { _isFallback?: true }> {
|
||||
console.warn('[fallback] 使用降级数据: getScheduledTasksFallback — 从 Triggers 列表推断');
|
||||
return triggers
|
||||
.filter((t) => t.enabled)
|
||||
.map((trigger) => ({
|
||||
@@ -214,7 +222,8 @@ export function getScheduledTasksFallback(triggers: TriggerForTasks[] = []): Sch
|
||||
* Returns honest minimal response — only includes layers that correspond
|
||||
* to real ZCLAW capabilities, no fabricated layers.
|
||||
*/
|
||||
export function getSecurityStatusFallback(): SecurityStatusFallback {
|
||||
export function getSecurityStatusFallback(): SecurityStatusFallback & { _isFallback: true } {
|
||||
console.warn('[fallback] 使用降级数据: getSecurityStatusFallback — 返回静态安全层状态');
|
||||
const layers: SecurityLayerFallback[] = [
|
||||
{ name: 'device_auth', enabled: true, description: '设备认证' },
|
||||
{ name: 'rbac', enabled: true, description: '角色权限控制' },
|
||||
@@ -228,6 +237,7 @@ export function getSecurityStatusFallback(): SecurityStatusFallback {
|
||||
const securityLevel = calculateSecurityLevel(enabledCount, layers.length);
|
||||
|
||||
return {
|
||||
_isFallback: true as const,
|
||||
layers,
|
||||
enabledCount,
|
||||
totalCount: layers.length,
|
||||
|
||||
@@ -65,13 +65,15 @@ export async function addVikingResource(
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a resource with inline content
|
||||
* Add a resource with metadata (keywords + importance)
|
||||
*/
|
||||
export async function addVikingResourceInline(
|
||||
export async function addVikingResourceWithMetadata(
|
||||
uri: string,
|
||||
content: string
|
||||
content: string,
|
||||
keywords: string[],
|
||||
importance?: number
|
||||
): Promise<VikingAddResult> {
|
||||
return invoke<VikingAddResult>('viking_add_inline', { uri, content });
|
||||
return invoke<VikingAddResult>('viking_add_with_metadata', { uri, content, keywords, importance });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,41 +138,16 @@ export async function getVikingTree(
|
||||
return invoke<Record<string, unknown>>('viking_tree', { path, depth });
|
||||
}
|
||||
|
||||
// === Server Functions ===
|
||||
|
||||
export interface VikingServerStatus {
|
||||
running: boolean;
|
||||
port?: number;
|
||||
pid?: number;
|
||||
error?: string;
|
||||
}
|
||||
// === Summary Generation Functions ===
|
||||
|
||||
/**
|
||||
* Get Viking server status
|
||||
* Store a resource and auto-generate L0/L1 summaries via configured LLM driver
|
||||
*/
|
||||
export async function getVikingServerStatus(): Promise<VikingServerStatus> {
|
||||
return invoke<VikingServerStatus>('viking_server_status');
|
||||
}
|
||||
|
||||
/**
|
||||
* Start Viking server
|
||||
*/
|
||||
export async function startVikingServer(): Promise<void> {
|
||||
return invoke<void>('viking_server_start');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop Viking server
|
||||
*/
|
||||
export async function stopVikingServer(): Promise<void> {
|
||||
return invoke<void>('viking_server_stop');
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart Viking server
|
||||
*/
|
||||
export async function restartVikingServer(): Promise<void> {
|
||||
return invoke<void>('viking_server_restart');
|
||||
export async function storeWithSummaries(
|
||||
uri: string,
|
||||
content: string
|
||||
): Promise<VikingAddResult> {
|
||||
return invoke<VikingAddResult>('viking_store_with_summaries', { uri, content });
|
||||
}
|
||||
|
||||
// === Memory Extraction Functions ===
|
||||
|
||||
@@ -77,7 +77,7 @@ docs/
|
||||
|
||||
## Crate 架构
|
||||
|
||||
ZCLAW 核心由 8 个 Rust Crate 组成:
|
||||
ZCLAW 核心由 10 个 Rust Crate 组成:
|
||||
|
||||
| Crate | 层级 | 职责 |
|
||||
|-------|------|------|
|
||||
@@ -85,9 +85,12 @@ ZCLAW 核心由 8 个 Rust Crate 组成:
|
||||
| `zclaw-memory` | L2 | 存储层 (SQLite, 会话管理) |
|
||||
| `zclaw-runtime` | L3 | 运行时 (LLM 驱动, 工具, Agent 循环) |
|
||||
| `zclaw-kernel` | L4 | 核心协调 (注册, 调度, 事件, 工作流) |
|
||||
| `zclaw-skills` | - | 技能系统 (SKILL.md 解析, 执行器) |
|
||||
| `zclaw-hands` | - | 自主能力 (Hand/Trigger 注册管理) |
|
||||
| `zclaw-protocols` | - | 协议支持 (MCP, A2A) |
|
||||
| `zclaw-skills` | L5 | 技能系统 (SKILL.md 解析, 执行器) |
|
||||
| `zclaw-hands` | L5 | 自主能力 (Hand/Trigger 注册管理) |
|
||||
| `zclaw-protocols` | L5 | 协议支持 (MCP, A2A) |
|
||||
| `zclaw-pipeline` | L5 | Pipeline DSL (v1/v2, 模板) |
|
||||
| `zclaw-growth` | L5 | 记忆增长 (FTS5 + TF-IDF) |
|
||||
| `zclaw-saas` | 独立 | SaaS 后端 (Axum + PostgreSQL) |
|
||||
|
||||
### 依赖关系
|
||||
|
||||
@@ -100,7 +103,12 @@ zclaw-runtime (→ types, memory)
|
||||
↑
|
||||
zclaw-kernel (→ types, memory, runtime)
|
||||
↑
|
||||
desktop/src-tauri (→ kernel, skills, hands, channels, protocols)
|
||||
┌───┴───┬───────┬───────────┬──────────┐
|
||||
│ │ │ │ │
|
||||
skills hands protocols pipeline growth
|
||||
|
||||
desktop/src-tauri (→ kernel, skills, hands, protocols)
|
||||
zclaw-saas (独立运行于 8080 端口)
|
||||
```
|
||||
|
||||
## 支持的 LLM 提供商
|
||||
@@ -113,13 +121,17 @@ desktop/src-tauri (→ kernel, skills, hands, channels, protocols)
|
||||
| zhipu | `https://open.bigmodel.cn/api/paas/v4` | 智谱 GLM |
|
||||
| openai | `https://api.openai.com/v1` | OpenAI |
|
||||
| anthropic | `https://api.anthropic.com` | Anthropic Claude |
|
||||
| gemini | `https://generativeai.googleapis.com/v1beta` | Google Gemini |
|
||||
| local | `http://localhost:11434/v1` | Ollama/LMStudio |
|
||||
|
||||
## 项目状态
|
||||
|
||||
- **架构迁移**: Phase 5 完成 - 内部 Kernel 集成
|
||||
- **Agent 智能层**: Phase 1-3 完成
|
||||
- **测试覆盖**: 161 E2E tests passing, 26 Rust tests passing
|
||||
- **Rust Crates**: 10 个 (171 Tauri 命令, 383 单元测试)
|
||||
- **SaaS 平台**: 131 API 路由, 12 模块, 34 数据表
|
||||
- **前端**: React 19 + Zustand 18 Stores, 135 组件
|
||||
- **安全审计**: V1 通过 (B+)
|
||||
|
||||
## 贡献指南
|
||||
|
||||
@@ -130,4 +142,4 @@ desktop/src-tauri (→ kernel, skills, hands, channels, protocols)
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2026-03-22
|
||||
**最后更新**: 2026-04-03
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ZCLAW 系统真相文档
|
||||
|
||||
> **更新日期**: 2026-04-02
|
||||
> **数据来源**: V11 全面审计 + 二次审计 + 代码全量扫描
|
||||
> **更新日期**: 2026-04-03
|
||||
> **数据来源**: V11 全面审计 + 二次审计 + 2026-04-03 代码全量扫描验证
|
||||
> **规则**: 此文档是唯一真相源。所有其他文档如果与此冲突,以此为准。
|
||||
|
||||
---
|
||||
@@ -12,18 +12,21 @@
|
||||
|------|--------|----------|
|
||||
| Rust Crates | 10 个 (编译通过) | `cargo check --workspace` |
|
||||
| Rust 代码行数 | ~66,000 | wc -l |
|
||||
| Rust 单元测试 | 584 个 | `cargo test --workspace` |
|
||||
| Rust 单元测试 | 383 个 | `grep '#\[test\]' crates/` |
|
||||
| Tauri 命令 | 171 个 | grep `#[tauri::command]` + 注释排除 |
|
||||
| **Tauri 命令有前端调用** | **147 个** | @connected 标注(经二次审计修正) |
|
||||
| **Tauri 命令无前端调用** | **24 个** | @reserved 标注 |
|
||||
| SKILL.md 文件 | 75 个 | `ls skills/*.md \| wc -l` |
|
||||
| Hands 启用 | 9 个 | Browser/Collector/Researcher/Clip/Twitter/Whiteboard/Slideshow/Speech/Quiz |
|
||||
| Hands 禁用 | 2 个 | Predictor, Lead |
|
||||
| SaaS API 端点 | 93 个(含 2 个 dev-only mock) | 路由注册全量统计 |
|
||||
| SaaS API 端点 | 131 个(含 2 个 dev-only mock) | 路由注册 handler 引用全量统计 |
|
||||
| SaaS 路由模块 | 12 个 | account/agent_template/auth/billing/knowledge/migration/model_config/prompt/relay/role/scheduled_task/telemetry(scheduled_task: 后端 5 CRUD + Admin V2 前端 service/page/route/nav) |
|
||||
| SaaS 数据表 | 34 个(含 saas_schema_version) | CREATE TABLE 全量统计 |
|
||||
| SaaS Workers | 7 个 | log_operation/cleanup_rate_limit/cleanup_refresh_tokens/record_usage/update_last_used/aggregate_usage/generate_embedding |
|
||||
| LLM Provider | 8 个 | Kimi/Qwen/DeepSeek/Zhipu/OpenAI/Anthropic/Gemini/Local |
|
||||
| Zustand Store | 18 个 | ls desktop/src/store/ (含 chat/ 子目录) |
|
||||
| React 组件 | ~50 个 | ls desktop/src/components/ |
|
||||
| 前端 TypeScript 测试 | 21 个文件 | 8 单元 + 12 E2E spec + 1 源码内 |
|
||||
| React 组件 | ~135 个 | find desktop/src/components/ (*.tsx/*.ts) |
|
||||
| 前端 TypeScript 测试 | 23 个文件 | 1 单元 + 22 E2E spec |
|
||||
| Admin V2 页面 | 13 个 | admin-v2/src/pages/ 全量统计 |
|
||||
| Admin V2 测试 | 322 个 | vitest 统计 |
|
||||
| 中间件层 | 11 层 | 运行时注册 |
|
||||
@@ -63,22 +66,18 @@
|
||||
|
||||
| 功能 | Rust 状态 | 前端状态 |
|
||||
|------|-----------|----------|
|
||||
| Pipeline 执行 | DSL 引擎完整 | 无 invoke 调用 |
|
||||
| Viking 适配 | FTS5 + TF-IDF | 11 个命令无前端调用 |
|
||||
| Pipeline 执行 | DSL 引擎完整 | **已接通前端** (8 个 invoke 调用匹配 8 个 Rust 命令,已确认可用) |
|
||||
| Viking 适配 | FTS5 + TF-IDF | 5 个孤立 invoke 已清理,新增 addWithMetadata/storeWithSummaries 方法 + UI |
|
||||
| Classroom | 27 个命令 | 7 个命令无前端调用 |
|
||||
| Browser Hand | 22 个命令 | 部分无前端调用 |
|
||||
| Multi-agent Director | 912 行 (feature-gated) | 未启用 |
|
||||
|
||||
### 2.4 前端孤立 invoke 调用(无 Rust 后端实现)
|
||||
### 2.4 前端孤立 invoke 调用(已清理)
|
||||
|
||||
以下 5 个前端 `invoke()` 调用在 Rust 端无对应命令,运行时将返回 "command not found":
|
||||
Viking 5 个孤立 invoke 调用已于 2026-04-03 清理移除:
|
||||
- `viking_add_inline`, `viking_server_start`, `viking_server_status`, `viking_server_stop`, `viking_server_restart`
|
||||
|
||||
| 命令 | 前端文件 | 说明 |
|
||||
|------|----------|------|
|
||||
| `viking_add_inline` | `viking-client.ts` | 无 Rust 实现,| `viking_server_start` | `viking-client.ts` | Viking 服务器生命周期管理未实现 |
|
||||
| `viking_server_status` | `viking-client.ts` | 同上 |
|
||||
| `viking_server_stop` | `viking-client.ts` | 同上 |
|
||||
| `viking_server_restart` | `viking-client.ts` | 同上 |
|
||||
当前无已知前端孤立 invoke 调用。
|
||||
|
||||
---
|
||||
|
||||
@@ -140,17 +139,33 @@
|
||||
|
||||
---
|
||||
|
||||
## 4.5 近期前端改进 (2026-04-03)
|
||||
|
||||
| 改进 | 说明 |
|
||||
|------|------|
|
||||
| Pipeline 前端接通 | 8 个前端 invoke 调用匹配 8 个 Rust 命令,完整可用 |
|
||||
| Viking 孤立调用清理 | 5 个无后端实现的 invoke 已移除 |
|
||||
| Viking 新方法 | `addWithMetadata`, `storeWithSummaries` 新增,含 UI |
|
||||
| api-fallbacks 标记 | `_isFallback` markers + `console.warn` logging |
|
||||
| MessageSearch 恢复 | ChatArea 头部搜索按钮,功能完整 |
|
||||
| scheduled_task Admin V2 | 后端 5 CRUD 端点完成,Admin V2 前端 service + page + route + nav 已添加 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 文档漂移记录
|
||||
|
||||
以下文档中的数字已与实际不符,需要校准:
|
||||
以下文档中的数字已与实际不符,需要校准(2026-04-03 已全部修正):
|
||||
|
||||
| 文档 | 过时内容 | 正确值 |
|
||||
|------|----------|--------|
|
||||
| CLAUDE.md (旧版) | "React 18" | React 19 |
|
||||
| CLAUDE.md (旧版) | "Tailwind CSS" | Tailwind 4 |
|
||||
| CLAUDE.md (旧版) | "Next.js (admin/)" | Vite + Ant Design Pro (admin-v2/) |
|
||||
| roadmap.md | Skills: 75 | 76 |
|
||||
| roadmap.md | Tauri 命令: 58+/130+ | 175 |
|
||||
| 文档 | 过时内容 | 正确值 | 状态 |
|
||||
|------|----------|--------|------|
|
||||
| CLAUDE.md (旧版) | "React 18" | React 19 | ✅ 已修正 |
|
||||
| CLAUDE.md (旧版) | "Tailwind CSS" | Tailwind 4 | ✅ 已修正 |
|
||||
| CLAUDE.md (旧版) | "Next.js (admin/)" | Vite + Ant Design Pro (admin-v2/) | ✅ 已修正 |
|
||||
| features/README.md | SaaS API: 58 | 131 | ✅ 已修正 |
|
||||
| features/README.md | Tauri 命令: 175 | 171 | ✅ 已修正 |
|
||||
| features/README.md | SaaS Workers: 5 | 7 | ✅ 已修正 |
|
||||
| features/README.md | SKILL: 76 | 75 | ✅ 已修正 |
|
||||
| docs/README.md | Crates: 8 | 10 | ✅ 已修正 |
|
||||
|
||||
---
|
||||
|
||||
@@ -164,3 +179,5 @@
|
||||
| 2026-04-02 | 深度审计后修正:8 个 Tauri 标注误标、4 处 SaaS let _ = 清理、TRUTH.md 路由数 58→93 |
|
||||
| 2026-04-02 | 二次深度审计:(1) 再修正 11 个 Tauri 标注 (147 connected / 24 reserved = 171) (2) 修正文档数字 SKILL 75/Stores 18/Admin 13 (3) relay extract_token_usage + current_key_id 防御性修复 (4) 记录 5 个前端孤立 invoke 调用 |
|
||||
| 2026-04-02 | 构建验证通过:cargo check ✅ + tsc --noEmit ✅ |
|
||||
| 2026-04-03 | 数字全面验证校准:SaaS API 93→131 (新增 knowledge 23 + billing 10 + role 11 等模块增长)、Rust 测试 584→383、React 组件 ~50→~135、前端测试 21→23、SaaS Workers 5→7 (新增 aggregate_usage/generate_embedding)、SaaS 路由模块 10→12、数据表 25→34 |
|
||||
| 2026-04-03 | 前端改进记录:(1) Pipeline 8 invoke 接通前端 (2) Viking 5 孤立 invoke 清理 + 2 新方法+UI (3) api-fallbacks _isFallback 标记 + console.warn 日志 (4) MessageSearch 恢复到 ChatArea (5) scheduled_task Admin V2 完整接入 (service+page+route+nav) |
|
||||
|
||||
310
docs/superpowers/specs/2026-04-03-production-readiness-design.md
Normal file
310
docs/superpowers/specs/2026-04-03-production-readiness-design.md
Normal file
@@ -0,0 +1,310 @@
|
||||
ZCLAW 生产就绪收尾设计文档
|
||||
|
||||
> **日期**: 2026-04-03
|
||||
> **状态**: 已批准
|
||||
> **预估总工时**: 12h(3 批次)
|
||||
> **策略**: 分层收敛 — 个人端优先、企业端跟进
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与动机
|
||||
|
||||
ZCLAW 经过 3 周密集开发,已修复全部 P0/P1 缺陷,核心功能完成度 90-98%。功能全景审计发现:
|
||||
|
||||
- **12 个前后端一致性问题**: 5 个孤立 invoke、Pipeline 文档过时、14 个 SaaS 端点无前端调用
|
||||
- **9 个功能缺失**: WASM/Native 执行未实现、Identity 回滚无 UI、MessageSearch 被禁用
|
||||
- **10 个可用性风险**: api-fallbacks 静默掩盖、MessageSearch 被禁用、Viking 孤立调用运行时崩溃
|
||||
- **7 处文档漂移**: SaaS API 数量差距最大(文档 58,实际 132)
|
||||
|
||||
本设计旨在通过 3 个批次高效收尾,使 ZCLAW 达到产品级可用状态。
|
||||
|
||||
---
|
||||
|
||||
## 2. 关键决策
|
||||
|
||||
| 决策 | 选择 | 理由 |
|
||||
|------|------|------|
|
||||
| 功能冻结状态 | 全面解冻 → 修复 → 再冻结 | P0/P1 已全部修复,审计发现的断链需要新增前端代码 |
|
||||
| 目标用户优先级 | 个人优先,企业跟进 | 桌面端核心体验(聊天、Pipeline、Hand、记忆)先保障 |
|
||||
| 未接入端点策略 | 分批处理 | 个人端断链先接通,企业端(Scheduled Task/Config)先标注 @reserved |
|
||||
| Viking 发展方向 | 优化现有原生实现 | 原生 SqliteStorage 优于外部 sidecar,删除孤立 invoke |
|
||||
| Pipeline 状态 | 验证而非重连 | 代码审计显示前后端已完全连接,可能是文档过时 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 执行计划
|
||||
|
||||
### Batch 1: 用户直接感知 (5h)
|
||||
|
||||
**目标**: 修复个人端用户能直接感知的问题
|
||||
|
||||
#### B1-1: Pipeline 端到端验证 (1h)
|
||||
|
||||
**问题**: TRUTH.md 记录"Pipeline DSL 引擎完整但无前端 invoke 调用",但代码审计显示前端 `pipeline-client.ts` 有 8 个 invoke 调用(pipeline_list/get/run/progress/result/cancel/runs/refresh)、Rust 端 `discovery.rs` 有完整 `#[tauri::command]` 注册。
|
||||
|
||||
**操作**:
|
||||
1. 启动 Tauri 开发环境,在 PipelinesPanel 中触发一个 education 模板
|
||||
2. 确认 `pipeline_list` → `pipeline_run` → `pipeline_progress` → `pipeline_result` 完整流程
|
||||
3. 如果不工作,排查断链点并修复
|
||||
4. 更新 TRUTH.md 中过时的 Pipeline 记录
|
||||
|
||||
**关键文件**:
|
||||
- `desktop/src/components/PipelinesPanel.tsx` — 使用 `usePipelines()` hook
|
||||
- `desktop/src/lib/pipeline-client.ts` — 8 个 Tauri invoke 调用 (list/get/run/progress/result/cancel/runs/refresh)
|
||||
- `desktop/src-tauri/src/pipeline_commands/discovery.rs` — `#[tauri::command]` 注册
|
||||
- `docs/TRUTH.md` — 修正过时记录
|
||||
|
||||
#### B1-2: MessageSearch 恢复 (1h)
|
||||
|
||||
**问题**: DeerFlow 重设计期间 MessageSearch 被注释掉(ChatArea.tsx 第 21 行)。组件本身完整,支持 session 搜索和 global 记忆搜索。
|
||||
|
||||
**操作**:
|
||||
1. 取消 `ChatArea.tsx` 第 21 行注释,恢复 MessageSearch 导入
|
||||
2. 在 DeerFlow 极简布局中找到合适的搜索入口位置(建议放在 TopBar 中)
|
||||
3. 确认 `intelligence-backend.ts` 的 `searchGlobalMemory()` 仍正常工作
|
||||
4. 验证 session 搜索和 global 搜索均可触发
|
||||
|
||||
**关键文件**:
|
||||
- `desktop/src/components/ChatArea.tsx` — 第 21 行注释
|
||||
- `desktop/src/components/MessageSearch.tsx` — 完整的搜索组件
|
||||
- `desktop/src/lib/intelligence-backend.ts` — global 搜索后端
|
||||
|
||||
#### B1-3: Viking 清理与优化 (2h)
|
||||
|
||||
**问题**: 5 个前端 invoke 调用在 Rust 端无对应命令,运行时必然返回 "command not found"。2 个有 Rust 后端的命令(`viking_add_with_metadata`、`viking_store_with_summaries`)无前端调用。
|
||||
|
||||
**操作**:
|
||||
1. 删除 `viking-client.ts` 中 5 个孤立方法:`viking_add_inline`、`viking_server_start/stop/status/restart`
|
||||
2. 删除相关的 `VikingServerStatus` 类型定义
|
||||
3. 在 `viking-client.ts` 中添加 `addWithMetadata()` 方法(调用已有的 `viking_add_with_metadata`)
|
||||
4. 在 `viking-client.ts` 中添加 `storeWithSummaries()` 方法(调用已有的 `viking_store_with_summaries`)
|
||||
5. 在 VikingPanel 中添加"生成摘要"按钮,调用 `storeWithSummaries()`
|
||||
6. 更新 TRUTH.md 中 Viking 命令的前端连接标注
|
||||
|
||||
**关键文件**:
|
||||
- `desktop/src/lib/viking-client.ts` — 删除 5 个孤立方法,添加 2 个新方法
|
||||
- `desktop/src/components/VikingPanel.tsx` — 添加摘要生成触发
|
||||
- `desktop/src-tauri/src/viking_commands.rs` — 已有 Rust 命令,无需修改
|
||||
- `docs/features/03-context-database/00-openviking-integration.md` — 更新架构描述
|
||||
|
||||
#### B1-4: api-fallbacks 透明化 (1h)
|
||||
|
||||
**问题**: `api-fallbacks.ts` 在后端返回 404 时静默返回假数据,用户无法区分真实功能和降级。
|
||||
|
||||
**操作**:
|
||||
1. 每个 fallback 函数返回数据中添加 `_isFallback: true` 标记
|
||||
2. 添加 `console.warn('[fallback] 使用降级数据: <函数名>')` 日志
|
||||
3. 在消费 fallback 数据的 Store 中检测标记,设置 `isDegraded: true` 状态
|
||||
4. UI 组件根据 `isDegraded` 显示黄色提示条"该功能需要连接后端服务"
|
||||
|
||||
**关键文件**:
|
||||
- `desktop/src/lib/api-fallbacks.ts` — 添加标记和日志
|
||||
- `desktop/src/store/configStore.ts` — 检测降级状态
|
||||
- `desktop/src/store/handStore.ts` — 检测降级状态
|
||||
- 相关 UI 组件 — 显示降级提示
|
||||
|
||||
---
|
||||
|
||||
### Batch 2: 系统健康 (4h)
|
||||
|
||||
**目标**: 校准文档真相源,标注企业端断链
|
||||
|
||||
#### B2-1: 文档漂移校准 (1.5h)
|
||||
|
||||
**问题**: 7 处文档数字与实际代码不符。
|
||||
|
||||
**操作**:
|
||||
更新以下文档中的过时数字:
|
||||
|
||||
| 文档 | 过时值 | 正确值 |
|
||||
|------|--------|--------|
|
||||
| `docs/features/README.md` | SaaS API: 58 | 132 |
|
||||
| `docs/features/README.md` | Zustand Store: 15 | 18 |
|
||||
| `docs/features/README.md` | SQL Schema: v6 | v8 |
|
||||
| `docs/features/README.md` | Admin 页面: 11 | 13 |
|
||||
| `docs/README.md` | 8 Rust Crates | 10 |
|
||||
| `docs/features/roadmap.md` | Tauri 命令: 175 | 171 (TRUTH) |
|
||||
| `docs/TRUTH.md` | SaaS API: 93 | 132 (含 Knowledge Base 模块 23 个新端点: category CRUD + item CRUD + search + recommend + analytics 5 端点 + 版本管理) |
|
||||
|
||||
同步更新 `docs/features/README.md` 中所有模块的成熟度评估。
|
||||
|
||||
**关键文件**:
|
||||
- `docs/TRUTH.md`
|
||||
- `docs/features/README.md`
|
||||
- `docs/features/roadmap.md`
|
||||
- `docs/README.md`
|
||||
|
||||
#### B2-2: 无前端端点标注 (0.5h)
|
||||
|
||||
**问题**: 14 个 SaaS 端点无前端调用,但代码中无任何标注。
|
||||
|
||||
**操作**:
|
||||
在以下 handler 函数上添加 `// @reserved - no frontend caller` 注释:
|
||||
|
||||
| 端点 | 模块 |
|
||||
|------|------|
|
||||
| `POST /api/v1/config/items` | migration |
|
||||
| `GET /api/v1/config/items/:id` | migration |
|
||||
| `DELETE /api/v1/config/items/:id` | migration |
|
||||
| `GET /api/v1/config/analysis` | migration |
|
||||
| `POST /api/v1/config/seed` | migration |
|
||||
| `GET /api/v1/config/sync-logs` | migration |
|
||||
| `POST /api/v1/keys/:id/rotate` | model_config |
|
||||
| `GET /api/v1/knowledge/items/:id/versions/:v` | knowledge |
|
||||
| `GET /api/scheduler/tasks` | scheduled_task |
|
||||
| `POST /api/scheduler/tasks` | scheduled_task |
|
||||
| `GET /api/scheduler/tasks/:id` | scheduled_task |
|
||||
| `PATCH /api/scheduler/tasks/:id` | scheduled_task |
|
||||
| `DELETE /api/scheduler/tasks/:id` | scheduled_task |
|
||||
|
||||
**关键文件**:
|
||||
- `crates/zclaw-saas/src/migration/handlers.rs`
|
||||
- `crates/zclaw-saas/src/model_config/handlers.rs`
|
||||
- `crates/zclaw-saas/src/knowledge/handlers.rs`
|
||||
- `crates/zclaw-saas/src/scheduled_task/handlers.rs`
|
||||
|
||||
#### B2-3: Scheduled Task Admin UI (1.5h)
|
||||
|
||||
**问题**: Scheduled Task 后端 5 个 CRUD 端点完整,但桌面端和 Admin 端均无 UI。
|
||||
|
||||
**操作**:
|
||||
1. 在 `admin-v2/src/services/` 添加 `scheduled-tasks.ts` 服务文件
|
||||
2. 在 `admin-v2/src/pages/` 添加 ScheduledTasks 页面(列表+创建/编辑/删除)
|
||||
3. 在 `admin-v2/src/router/index.tsx` 添加路由
|
||||
4. 在 `admin-v2/src/layouts/AdminLayout.tsx` 侧边栏添加导航入口
|
||||
5. 复用 Admin 现有的 Ant Design Pro 组件模式
|
||||
|
||||
**关键文件**:
|
||||
- `admin-v2/src/services/scheduled-tasks.ts` — 新建
|
||||
- `admin-v2/src/pages/ScheduledTasks.tsx` — 新建
|
||||
- `admin-v2/src/router/index.tsx` — 添加路由
|
||||
- `admin-v2/src/layouts/AdminLayout.tsx` — 添加侧边栏导航
|
||||
|
||||
#### B2-4: TRUTH.md Pipeline 记录修正 (0.5h)
|
||||
|
||||
**问题**: TRUTH.md 记录"Pipeline 无前端 invoke 调用",但代码审计显示已完全连接。
|
||||
|
||||
**操作**:
|
||||
1. 将 Pipeline 从"已实现但未接通前端"移到"确认可用"列表
|
||||
2. 添加 Pipeline 验证结果记录
|
||||
|
||||
**关键文件**:
|
||||
- `docs/TRUTH.md`
|
||||
|
||||
---
|
||||
|
||||
### Batch 3: 长期质量 (3h)
|
||||
|
||||
**目标**: 验证功能性缺陷,清理 P2 代码质量问题
|
||||
|
||||
#### B3-1: hand_run_status 端到端验证 (0.5h)
|
||||
|
||||
**问题**: TRUTH.md 和审计报告记录 `hand_run_status` 和 `hand_run_list` 为 stub 命令,但代码审计显示它们已在 `desktop/src-tauri/src/kernel_commands/hand.rs`(第 358-416 行)完整实现,前端 `desktop/src/lib/kernel-hands.ts`(第 105-153 行)已有 invoke 调用。与 Pipeline 情况类似,可能需要的是验证而非实现。
|
||||
|
||||
**操作**:
|
||||
1. 验证 hand_run_status/hand_run_list 的端到端流程
|
||||
2. 如果工作正常,更新 TRUTH.md 修正过时记录
|
||||
3. 如果有问题,修复断链
|
||||
|
||||
**关键文件**:
|
||||
- `desktop/src-tauri/src/kernel_commands/hand.rs` — 已有完整实现(第 358-416 行)
|
||||
- `desktop/src/lib/kernel-hands.ts` — 已有 invoke 调用(第 105-153 行)
|
||||
- `docs/TRUTH.md` — 修正过时记录
|
||||
|
||||
#### B3-2: Identity 快照回滚 UI (1h)
|
||||
|
||||
**问题**: `identity_get_snapshots` 和 `identity_restore_snapshot` API 存在,但前端无回滚入口。
|
||||
|
||||
**操作**:
|
||||
1. 在 RightPanel 的 identity tab 中添加"历史快照"区域
|
||||
2. 调用 `identityClient.getSnapshots(agentId)` 显示快照列表
|
||||
3. 每个快照显示时间戳和变更摘要
|
||||
4. 添加"回滚到此版本"按钮,调用 `identityClient.restoreSnapshot(agentId, snapshotId)`
|
||||
|
||||
**关键文件**:
|
||||
- `desktop/src/components/RightPanel.tsx` — 添加快照区域
|
||||
- `desktop/src/lib/intelligence-client/` — 已有 identity 方法
|
||||
|
||||
#### B3-3: P2 代码质量清理 (1h)
|
||||
|
||||
**问题**: TRUTH.md 记录 10 个 P2 代码质量问题。
|
||||
|
||||
**操作**:
|
||||
根据 TRUTH.md 中的 SEC2-P2-01~10 清单,逐项修复。具体项目需在执行时确认。
|
||||
|
||||
#### B3-4: Config HTTP 方法对齐 (0.5h)
|
||||
|
||||
**问题**: Admin `config.ts` 使用 PATCH 更新,SaaS 后端定义 PUT。
|
||||
|
||||
**操作**:
|
||||
1. 确认 SaaS 后端是否同时支持 PUT 和 PATCH
|
||||
2. 如果只支持 PUT,更新 Admin 服务文件使用 PUT
|
||||
3. 如果同时支持,无需修改
|
||||
|
||||
**关键文件**:
|
||||
- `admin-v2/src/services/config.ts`
|
||||
|
||||
---
|
||||
|
||||
## 4. 验证方案
|
||||
|
||||
### Batch 1 完成标准
|
||||
|
||||
- [ ] Pipeline: 能在 UI 中成功执行一个模板
|
||||
- [ ] MessageSearch: 在聊天界面能搜索消息(session + global)
|
||||
- [ ] Viking: VikingPanel 不再触发 "command not found",摘要生成可触发
|
||||
- [ ] api-fallbacks: 降级数据有明确提示
|
||||
|
||||
### Batch 2 完成标准
|
||||
|
||||
- [ ] 文档: TRUTH.md 和 README.md 数字与代码一致
|
||||
- [ ] 端点标注: 14 个无前端端点有 @reserved 注释
|
||||
- [ ] Scheduled Task: Admin 中能创建/查看/删除定时任务
|
||||
|
||||
### Batch 3 完成标准
|
||||
|
||||
- [ ] hand_run_status: 端到端验证通过,返回真实执行状态数据
|
||||
- [ ] Identity: 能查看历史快照并执行回滚
|
||||
- [ ] P2: 10 个代码质量问题已处理
|
||||
- [ ] Config: Admin 和 SaaS HTTP 方法一致
|
||||
|
||||
### 自动化验证
|
||||
|
||||
```bash
|
||||
# TypeScript 编译
|
||||
cd desktop && pnpm tsc --noEmit
|
||||
|
||||
# Rust 编译
|
||||
cargo check --workspace
|
||||
|
||||
# 单元测试
|
||||
cargo test --workspace
|
||||
pnpm vitest run
|
||||
cd ../admin-v2 && pnpm vitest run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 风险评估
|
||||
|
||||
| 风险 | 概率 | 影响 | 缓解 |
|
||||
|------|------|------|------|
|
||||
| Pipeline 实际有隐藏断链 | 中 | B1-1 时间翻倍 | 先验证再修复 |
|
||||
| DeerFlow 布局不适配 MessageSearch | 低 | 需调整 CSS | 复用现有搜索 UI |
|
||||
| Viking 摘要生成需要 LLM 配置 | 中 | 用户需先配置模型 | 显示配置引导 |
|
||||
| Scheduled Task Admin 页面超出预估 | 低 | 复用 Ant Design 模板 | 控制复杂度 |
|
||||
| P2 清理发现新问题 | 中 | 时间增加 | 按优先级截断 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 不在本设计范围内
|
||||
|
||||
以下功能明确**不在**本次收尾范围内:
|
||||
|
||||
- WASM/Native Skill 执行模式(中期计划 M1)
|
||||
- 智能路由 Phase 3 多技能编排
|
||||
- Active Learning Phase 2 SQLite+向量化
|
||||
- Predictor/Lead Hands 实现
|
||||
- A2A 协议完整实现
|
||||
- Agent 持久化优化
|
||||
- 云同步服务
|
||||
Reference in New Issue
Block a user