fix: 修复 AI 回复无法送达发送者 + 重复消息 + action角色泄露 + OS环境支持
广播逻辑重构: - AI 回复 (stream_start/response/stream_segments/multi_message/stream_end) 改用 broadcastToUser 发送给所有客户端 - 用户消息回显保持 broadcastToUserExcept 排除发送者 消息去重与角色修复: - CacheMessage(user) 移至回复生成后,避免本轮 LLM 调用出现重复用户消息 - action 角色消息在 DB 存储时映射为 assistant,DeepSeek 等模型不支持自定义角色 - stream_end defer 机制确保错误路径也会终止客户端思考指示器 OS 完整环境支持: - host 包重构为 HostBackend 接口 + Direct/WSL/Docker 三种后端 - 新增 os_exec/os_file/os_system 工具供 AI 在完整 Linux 环境中自由操作 其他: - 视觉模型注入 + 图片预处理后清空 Images 避免传给 Chat 模型 - 图片 URL 相对路径→绝对 URL 转换 - DevTools 链路追踪页面 + 重启修复 - 记忆搜索模糊匹配增强 - 后台思考定时调度支持 - 管理后台页面 (模型配置/用户管理等) - docs/api 更新广播机制说明 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { useRef, useEffect, useState } from 'react';
|
||||
import { useRef, useEffect, useState, useCallback } from 'react';
|
||||
import { CyreneAvatar } from '@/components/persona/CyreneAvatar';
|
||||
import { MoodIndicator } from '@/components/persona/MoodIndicator';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { usePWA } from '@/hooks/usePWA';
|
||||
import { useNotificationStore } from '@/store/notificationStore';
|
||||
import { useSessionStore } from '@/store/sessionStore';
|
||||
import { getToken } from '@/api/client';
|
||||
import { ReminderPanel } from '@/components/layout/ReminderPanel';
|
||||
import { BriefingPanel } from '@/components/layout/BriefingPanel';
|
||||
import { AutomationPanel } from '@/components/layout/AutomationPanel';
|
||||
@@ -60,12 +61,31 @@ export function Header({ onMenuClick, onSearchClick }: HeaderProps) {
|
||||
// PWA Hook
|
||||
const { isInstallable, isInstalled, hasUpdate, install, update } = usePWA();
|
||||
|
||||
// 下拉面板标签页切换:通知 / 提醒 / 简报
|
||||
// 下拉面板标签页切换
|
||||
const [dropdownTab, setDropdownTab] = useState<'notifications' | 'reminders' | 'briefing' | 'automation' | 'files' | 'knowledge'>('notifications');
|
||||
|
||||
// 获取当前用户 ID
|
||||
const userId = localStorage.getItem('user_id') || '';
|
||||
|
||||
// 健康检查指示器
|
||||
const [apiOnline, setApiOnline] = useState<boolean | null>(null);
|
||||
const checkHealth = useCallback(async () => {
|
||||
try {
|
||||
const API_BASE = import.meta.env.VITE_API_URL || '/api/v1';
|
||||
const resp = await fetch(`${API_BASE}/health`);
|
||||
const data = await resp.json();
|
||||
setApiOnline(data?.status === 'ok');
|
||||
} catch {
|
||||
setApiOnline(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
checkHealth();
|
||||
const interval = setInterval(checkHealth, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, [checkHealth]);
|
||||
|
||||
// 点击外部关闭下拉
|
||||
useEffect(() => {
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
@@ -102,9 +122,19 @@ export function Header({ onMenuClick, onSearchClick }: HeaderProps) {
|
||||
|
||||
<CyreneAvatar size="sm" />
|
||||
<div>
|
||||
<h1 className="text-base font-semibold text-pink-600 dark:text-pink-400">
|
||||
昔涟
|
||||
</h1>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<h1 className="text-base font-semibold text-pink-600 dark:text-pink-400">
|
||||
昔涟
|
||||
</h1>
|
||||
{apiOnline !== null && (
|
||||
<span
|
||||
className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${
|
||||
apiOnline ? 'bg-green-400' : 'bg-red-400 animate-pulse'
|
||||
}`}
|
||||
title={apiOnline ? 'API 在线' : 'API 离线'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<MoodIndicator />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useSession } from '@/hooks/useSession';
|
||||
import { useSessionStore } from '@/store/sessionStore';
|
||||
import { usePageStore } from '@/store/pageStore';
|
||||
import { CyreneAvatar } from '@/components/persona/CyreneAvatar';
|
||||
import { exportSession, type ExportFormat } from '@/api/sessions';
|
||||
import type { Session } from '@/types/session';
|
||||
@@ -60,14 +61,15 @@ export function Sidebar({ onClose, onMemoryClick }: SidebarProps) {
|
||||
|
||||
const handleSelectSession = (id: string) => {
|
||||
setCurrentSession(id);
|
||||
usePageStore.getState().goToChat();
|
||||
if (onClose) onClose();
|
||||
};
|
||||
|
||||
const handleMainSession = async () => {
|
||||
// 找到主对话
|
||||
const mainSession = displaySessions.find((s) => s.is_main);
|
||||
if (mainSession) {
|
||||
setCurrentSession(mainSession.id);
|
||||
usePageStore.getState().goToChat();
|
||||
if (onClose) onClose();
|
||||
}
|
||||
};
|
||||
@@ -278,8 +280,25 @@ export function Sidebar({ onClose, onMemoryClick }: SidebarProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Admin 管理导航 */}
|
||||
{isAdmin && (
|
||||
<div className="px-3 pt-2 border-t border-pink-100 dark:border-pink-900 space-y-1">
|
||||
<p className="px-1 text-[10px] text-gray-400 uppercase tracking-wider">管理</p>
|
||||
<AdminNavButton page="admin-models" icon="🤖" label="模型配置" />
|
||||
<AdminNavButton page="admin-dashboard" icon="📊" label="仪表盘" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 底部:记忆管理 + 一键清空所有对话 */}
|
||||
<div className="p-3 border-t border-pink-100 dark:border-pink-900 space-y-2">
|
||||
{/* 个人资料 */}
|
||||
<button
|
||||
onClick={() => usePageStore.getState().setPage('profile')}
|
||||
className="w-full flex items-center justify-center gap-1.5 px-3 py-2 bg-blue-50 hover:bg-blue-100 dark:bg-blue-900/20 dark:hover:bg-blue-900/30 text-blue-500 hover:text-blue-600 rounded-xl text-xs font-medium transition-colors border border-blue-200 dark:border-blue-800"
|
||||
>
|
||||
<span>👤</span>
|
||||
<span>个人资料</span>
|
||||
</button>
|
||||
{onMemoryClick && (
|
||||
<button
|
||||
onClick={onMemoryClick}
|
||||
@@ -343,3 +362,24 @@ export function Sidebar({ onClose, onMemoryClick }: SidebarProps) {
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
/** Admin 导航按钮 */
|
||||
function AdminNavButton({ page, icon, label }: { page: string; icon: string; label: string }) {
|
||||
const currentPage = usePageStore((s) => s.currentPage);
|
||||
const setPage = usePageStore((s) => s.setPage);
|
||||
const isActive = currentPage === page;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => setPage(page as 'admin-models' | 'admin-dashboard')}
|
||||
className={`w-full flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium transition-colors ${
|
||||
isActive
|
||||
? 'bg-pink-50 dark:bg-pink-900/30 text-pink-600'
|
||||
: 'text-gray-500 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
<span>{icon}</span>
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user