mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-05-18 22:47:31 +08:00
webui: add sessions/memory management and show full hot-reload field details
This commit is contained in:
@@ -74,6 +74,8 @@ func (s *RegistryServer) Start(ctx context.Context) error {
|
|||||||
mux.HandleFunc("/webui/api/nodes", s.handleWebUINodes)
|
mux.HandleFunc("/webui/api/nodes", s.handleWebUINodes)
|
||||||
mux.HandleFunc("/webui/api/cron", s.handleWebUICron)
|
mux.HandleFunc("/webui/api/cron", s.handleWebUICron)
|
||||||
mux.HandleFunc("/webui/api/skills", s.handleWebUISkills)
|
mux.HandleFunc("/webui/api/skills", s.handleWebUISkills)
|
||||||
|
mux.HandleFunc("/webui/api/sessions", s.handleWebUISessions)
|
||||||
|
mux.HandleFunc("/webui/api/memory", s.handleWebUIMemory)
|
||||||
mux.HandleFunc("/webui/api/exec_approvals", s.handleWebUIExecApprovals)
|
mux.HandleFunc("/webui/api/exec_approvals", s.handleWebUIExecApprovals)
|
||||||
mux.HandleFunc("/webui/api/logs/stream", s.handleWebUILogsStream)
|
mux.HandleFunc("/webui/api/logs/stream", s.handleWebUILogsStream)
|
||||||
s.server = &http.Server{Addr: s.addr, Handler: mux}
|
s.server = &http.Server{Addr: s.addr, Handler: mux}
|
||||||
@@ -226,10 +228,18 @@ func (s *RegistryServer) handleWebUIConfig(w http.ResponseWriter, r *http.Reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
info := hotReloadFieldInfo()
|
||||||
|
paths := make([]string, 0, len(info))
|
||||||
|
for _, it := range info {
|
||||||
|
if p, ok := it["path"].(string); ok && strings.TrimSpace(p) != "" {
|
||||||
|
paths = append(paths, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"config": cfg,
|
"config": cfg,
|
||||||
"hot_reload_fields": hotReloadFieldPaths(),
|
"hot_reload_fields": paths,
|
||||||
|
"hot_reload_field_details": info,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -803,6 +813,116 @@ func anyToString(v interface{}) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *RegistryServer) handleWebUISessions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !s.checkAuth(r) {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sessionsDir := filepath.Join(filepath.Dir(strings.TrimSpace(s.workspacePath)), "sessions")
|
||||||
|
entries, err := os.ReadDir(sessionsDir)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "sessions": []interface{}{}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
type item struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
}
|
||||||
|
out := make([]item, 0, len(entries))
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := e.Name()
|
||||||
|
if strings.HasSuffix(name, ".json") {
|
||||||
|
out = append(out, item{Key: strings.TrimSuffix(name, ".json")})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "sessions": out})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RegistryServer) handleWebUIMemory(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !s.checkAuth(r) {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
memoryDir := filepath.Join(strings.TrimSpace(s.workspacePath), "memory")
|
||||||
|
_ = os.MkdirAll(memoryDir, 0755)
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
path := strings.TrimSpace(r.URL.Query().Get("path"))
|
||||||
|
if path == "" {
|
||||||
|
entries, err := os.ReadDir(memoryDir)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
files := make([]string, 0, len(entries))
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
files = append(files, e.Name())
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "files": files})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clean := filepath.Clean(path)
|
||||||
|
if strings.HasPrefix(clean, "..") {
|
||||||
|
http.Error(w, "invalid path", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
full := filepath.Join(memoryDir, clean)
|
||||||
|
b, err := os.ReadFile(full)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "path": clean, "content": string(b)})
|
||||||
|
case http.MethodPost:
|
||||||
|
var body struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
http.Error(w, "invalid json", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clean := filepath.Clean(strings.TrimSpace(body.Path))
|
||||||
|
if clean == "" || strings.HasPrefix(clean, "..") {
|
||||||
|
http.Error(w, "invalid path", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
full := filepath.Join(memoryDir, clean)
|
||||||
|
if err := os.WriteFile(full, []byte(body.Content), 0644); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "path": clean})
|
||||||
|
case http.MethodDelete:
|
||||||
|
path := filepath.Clean(strings.TrimSpace(r.URL.Query().Get("path")))
|
||||||
|
if path == "" || strings.HasPrefix(path, "..") {
|
||||||
|
http.Error(w, "invalid path", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
full := filepath.Join(memoryDir, path)
|
||||||
|
if err := os.Remove(full); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "deleted": true, "path": path})
|
||||||
|
default:
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *RegistryServer) handleWebUIExecApprovals(w http.ResponseWriter, r *http.Request) {
|
func (s *RegistryServer) handleWebUIExecApprovals(w http.ResponseWriter, r *http.Request) {
|
||||||
if !s.checkAuth(r) {
|
if !s.checkAuth(r) {
|
||||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
@@ -950,18 +1070,18 @@ func (s *RegistryServer) checkAuth(r *http.Request) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func hotReloadFieldPaths() []string {
|
func hotReloadFieldInfo() []map[string]interface{} {
|
||||||
return []string{
|
return []map[string]interface{}{
|
||||||
"logging.*",
|
{"path": "logging.*", "name": "日志配置", "description": "日志等级、落盘等"},
|
||||||
"sentinel.*",
|
{"path": "sentinel.*", "name": "哨兵配置", "description": "健康检查与自动修复"},
|
||||||
"agents.*",
|
{"path": "agents.*", "name": "Agent 配置", "description": "模型、策略、默认行为"},
|
||||||
"providers.*",
|
{"path": "providers.*", "name": "Provider 配置", "description": "LLM 提供商与代理"},
|
||||||
"tools.*",
|
{"path": "tools.*", "name": "工具配置", "description": "工具开关、执行参数"},
|
||||||
"channels.*",
|
{"path": "channels.*", "name": "渠道配置", "description": "Telegram/其它渠道参数"},
|
||||||
"cron.*",
|
{"path": "cron.*", "name": "定时任务配置", "description": "cron 全局运行参数"},
|
||||||
"agents.defaults.heartbeat.*",
|
{"path": "agents.defaults.heartbeat.*", "name": "心跳策略", "description": "心跳频率、提示词"},
|
||||||
"agents.defaults.autonomy.*",
|
{"path": "agents.defaults.autonomy.*", "name": "自治策略", "description": "自治任务开关与限流"},
|
||||||
"gateway.* (except listen address/port may require restart in some environments)",
|
{"path": "gateway.*", "name": "网关配置", "description": "多数可热更,监听地址/端口可能需重启"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
5206
webui/package-lock.json
generated
Normal file
5206
webui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ import Cron from './pages/Cron';
|
|||||||
import Nodes from './pages/Nodes';
|
import Nodes from './pages/Nodes';
|
||||||
import Logs from './pages/Logs';
|
import Logs from './pages/Logs';
|
||||||
import Skills from './pages/Skills';
|
import Skills from './pages/Skills';
|
||||||
|
import Memory from './pages/Memory';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
@@ -23,6 +24,7 @@ export default function App() {
|
|||||||
<Route path="config" element={<Config />} />
|
<Route path="config" element={<Config />} />
|
||||||
<Route path="cron" element={<Cron />} />
|
<Route path="cron" element={<Cron />} />
|
||||||
<Route path="nodes" element={<Nodes />} />
|
<Route path="nodes" element={<Nodes />} />
|
||||||
|
<Route path="memory" element={<Memory />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { LayoutDashboard, MessageSquare, Settings, Clock, Server, Terminal, Globe, Zap } from 'lucide-react';
|
import { LayoutDashboard, MessageSquare, Settings, Clock, Server, Terminal, Zap, FolderOpen } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAppContext } from '../context/AppContext';
|
import { useAppContext } from '../context/AppContext';
|
||||||
import NavItem from './NavItem';
|
import NavItem from './NavItem';
|
||||||
@@ -20,6 +20,7 @@ const Sidebar: React.FC = () => {
|
|||||||
<NavItem icon={<Settings className="w-5 h-5" />} label={t('config')} to="/config" />
|
<NavItem icon={<Settings className="w-5 h-5" />} label={t('config')} to="/config" />
|
||||||
<NavItem icon={<Clock className="w-5 h-5" />} label={t('cronJobs')} to="/cron" />
|
<NavItem icon={<Clock className="w-5 h-5" />} label={t('cronJobs')} to="/cron" />
|
||||||
<NavItem icon={<Server className="w-5 h-5" />} label={t('nodes')} to="/nodes" />
|
<NavItem icon={<Server className="w-5 h-5" />} label={t('nodes')} to="/nodes" />
|
||||||
|
<NavItem icon={<FolderOpen className="w-5 h-5" />} label={t('memory')} to="/memory" />
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="p-4 border-t border-zinc-800 bg-zinc-900/50">
|
<div className="p-4 border-t border-zinc-800 bg-zinc-900/50">
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ interface AppContextType {
|
|||||||
refreshCron: () => Promise<void>;
|
refreshCron: () => Promise<void>;
|
||||||
refreshNodes: () => Promise<void>;
|
refreshNodes: () => Promise<void>;
|
||||||
refreshSkills: () => Promise<void>;
|
refreshSkills: () => Promise<void>;
|
||||||
|
refreshSessions: () => Promise<void>;
|
||||||
loadConfig: () => Promise<void>;
|
loadConfig: () => Promise<void>;
|
||||||
|
hotReloadFields: string[];
|
||||||
|
hotReloadFieldDetails: Array<{ path: string; name?: string; description?: string }>;
|
||||||
q: string;
|
q: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,16 +51,32 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
const [cron, setCron] = useState<CronJob[]>([]);
|
const [cron, setCron] = useState<CronJob[]>([]);
|
||||||
const [skills, setSkills] = useState<Skill[]>([]);
|
const [skills, setSkills] = useState<Skill[]>([]);
|
||||||
const [sessions, setSessions] = useState<Session[]>([{ key: 'webui:default', title: 'Default' }]);
|
const [sessions, setSessions] = useState<Session[]>([{ key: 'webui:default', title: 'Default' }]);
|
||||||
|
const [hotReloadFields, setHotReloadFields] = useState<string[]>([]);
|
||||||
|
const [hotReloadFieldDetails, setHotReloadFieldDetails] = useState<Array<{ path: string; name?: string; description?: string }>>([]);
|
||||||
|
|
||||||
const q = token ? `?token=${encodeURIComponent(token)}` : '';
|
const q = token ? `?token=${encodeURIComponent(token)}` : '';
|
||||||
|
|
||||||
const loadConfig = useCallback(async () => {
|
const loadConfig = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`/webui/api/config${q}`);
|
const hotQ = q ? `${q}&include_hot_reload_fields=1` : '?include_hot_reload_fields=1';
|
||||||
|
const r = await fetch(`/webui/api/config${hotQ}`);
|
||||||
if (!r.ok) throw new Error('Failed to load config');
|
if (!r.ok) throw new Error('Failed to load config');
|
||||||
const txt = await r.text();
|
const txt = await r.text();
|
||||||
setCfgRaw(txt);
|
try {
|
||||||
try { setCfg(JSON.parse(txt)); } catch { setCfg({}); }
|
const parsed = JSON.parse(txt);
|
||||||
|
if (parsed && parsed.config) {
|
||||||
|
setCfg(parsed.config);
|
||||||
|
setCfgRaw(JSON.stringify(parsed.config, null, 2));
|
||||||
|
setHotReloadFields(Array.isArray(parsed.hot_reload_fields) ? parsed.hot_reload_fields : []);
|
||||||
|
setHotReloadFieldDetails(Array.isArray(parsed.hot_reload_field_details) ? parsed.hot_reload_field_details : []);
|
||||||
|
} else {
|
||||||
|
setCfg(parsed || {});
|
||||||
|
setCfgRaw(txt);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setCfgRaw(txt);
|
||||||
|
try { setCfg(JSON.parse(txt)); } catch { setCfg({}); }
|
||||||
|
}
|
||||||
setIsGatewayOnline(true);
|
setIsGatewayOnline(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setIsGatewayOnline(false);
|
setIsGatewayOnline(false);
|
||||||
@@ -104,9 +123,23 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
}
|
}
|
||||||
}, [q]);
|
}, [q]);
|
||||||
|
|
||||||
|
const refreshSessions = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/webui/api/sessions${q}`);
|
||||||
|
if (!r.ok) throw new Error('Failed to load sessions');
|
||||||
|
const j = await r.json();
|
||||||
|
const arr = Array.isArray(j.sessions) ? j.sessions : [];
|
||||||
|
setSessions(arr.map((s: any) => ({ key: s.key, title: s.key })));
|
||||||
|
setIsGatewayOnline(true);
|
||||||
|
} catch (e) {
|
||||||
|
setIsGatewayOnline(false);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}, [q]);
|
||||||
|
|
||||||
const refreshAll = useCallback(async () => {
|
const refreshAll = useCallback(async () => {
|
||||||
await Promise.all([loadConfig(), refreshCron(), refreshNodes(), refreshSkills()]);
|
await Promise.all([loadConfig(), refreshCron(), refreshNodes(), refreshSkills(), refreshSessions()]);
|
||||||
}, [loadConfig, refreshCron, refreshNodes, refreshSkills]);
|
}, [loadConfig, refreshCron, refreshNodes, refreshSkills, refreshSessions]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshAll();
|
refreshAll();
|
||||||
@@ -114,9 +147,10 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
refreshCron();
|
refreshCron();
|
||||||
refreshNodes();
|
refreshNodes();
|
||||||
refreshSkills();
|
refreshSkills();
|
||||||
|
refreshSessions();
|
||||||
}, 10000);
|
}, 10000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [token, refreshAll, refreshCron, refreshNodes, refreshSkills]);
|
}, [token, refreshAll, refreshCron, refreshNodes, refreshSkills, refreshSessions]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppContext.Provider value={{
|
<AppContext.Provider value={{
|
||||||
@@ -124,7 +158,8 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
cfg, setCfg, cfgRaw, setCfgRaw, nodes, setNodes,
|
cfg, setCfg, cfgRaw, setCfgRaw, nodes, setNodes,
|
||||||
cron, setCron, skills, setSkills,
|
cron, setCron, skills, setSkills,
|
||||||
sessions, setSessions,
|
sessions, setSessions,
|
||||||
refreshAll, refreshCron, refreshNodes, refreshSkills, loadConfig, q
|
refreshAll, refreshCron, refreshNodes, refreshSkills, refreshSessions, loadConfig,
|
||||||
|
hotReloadFields, hotReloadFieldDetails, q
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</AppContext.Provider>
|
</AppContext.Provider>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const resources = {
|
|||||||
nodes: 'Nodes',
|
nodes: 'Nodes',
|
||||||
logs: 'Real-time Logs',
|
logs: 'Real-time Logs',
|
||||||
skills: 'Skills',
|
skills: 'Skills',
|
||||||
|
memory: 'Memory',
|
||||||
gatewayStatus: 'Gateway Status',
|
gatewayStatus: 'Gateway Status',
|
||||||
online: 'Online',
|
online: 'Online',
|
||||||
offline: 'Offline',
|
offline: 'Offline',
|
||||||
@@ -154,6 +155,7 @@ const resources = {
|
|||||||
nodes: '节点',
|
nodes: '节点',
|
||||||
logs: '实时日志',
|
logs: '实时日志',
|
||||||
skills: '技能管理',
|
skills: '技能管理',
|
||||||
|
memory: '记忆文件',
|
||||||
gatewayStatus: '网关状态',
|
gatewayStatus: '网关状态',
|
||||||
online: '在线',
|
online: '在线',
|
||||||
offline: '离线',
|
offline: '离线',
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function setPath(obj: any, path: string, value: any) {
|
|||||||
|
|
||||||
const Config: React.FC = () => {
|
const Config: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { cfg, setCfg, cfgRaw, setCfgRaw, loadConfig, q } = useAppContext();
|
const { cfg, setCfg, cfgRaw, setCfgRaw, loadConfig, hotReloadFieldDetails, q } = useAppContext();
|
||||||
const [showRaw, setShowRaw] = useState(false);
|
const [showRaw, setShowRaw] = useState(false);
|
||||||
|
|
||||||
async function saveConfig() {
|
async function saveConfig() {
|
||||||
@@ -53,6 +53,18 @@ const Config: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-zinc-900/40 border border-zinc-800/80 rounded-2xl p-4">
|
||||||
|
<div className="text-sm font-semibold text-zinc-300 mb-2">热更新字段(完整)</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-xs">
|
||||||
|
{hotReloadFieldDetails.map((it) => (
|
||||||
|
<div key={it.path} className="p-2 rounded bg-zinc-950 border border-zinc-800">
|
||||||
|
<div className="font-mono text-zinc-200">{it.path}</div>
|
||||||
|
<div className="text-zinc-400">{it.name || ''}{it.description ? ` · ${it.description}` : ''}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 bg-zinc-900/40 border border-zinc-800/80 rounded-2xl overflow-hidden flex flex-col shadow-sm">
|
<div className="flex-1 bg-zinc-900/40 border border-zinc-800/80 rounded-2xl overflow-hidden flex flex-col shadow-sm">
|
||||||
{!showRaw ? (
|
{!showRaw ? (
|
||||||
<div className="p-8 overflow-y-auto">
|
<div className="p-8 overflow-y-auto">
|
||||||
|
|||||||
85
webui/src/pages/Memory.tsx
Normal file
85
webui/src/pages/Memory.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useAppContext } from '../context/AppContext';
|
||||||
|
|
||||||
|
const Memory: React.FC = () => {
|
||||||
|
const { q } = useAppContext();
|
||||||
|
const [files, setFiles] = useState<string[]>([]);
|
||||||
|
const [active, setActive] = useState('');
|
||||||
|
const [content, setContent] = useState('');
|
||||||
|
|
||||||
|
async function loadFiles() {
|
||||||
|
const r = await fetch(`/webui/api/memory${q}`);
|
||||||
|
const j = await r.json();
|
||||||
|
setFiles(Array.isArray(j.files) ? j.files : []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const qp = (k: string, v: string) => `${q}${q ? '&' : '?'}${k}=${encodeURIComponent(v)}`;
|
||||||
|
|
||||||
|
async function openFile(path: string) {
|
||||||
|
const r = await fetch(`/webui/api/memory${qp('path', path)}`);
|
||||||
|
const j = await r.json();
|
||||||
|
setActive(path);
|
||||||
|
setContent(j.content || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveFile() {
|
||||||
|
if (!active) return;
|
||||||
|
await fetch(`/webui/api/memory${q}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ path: active, content }),
|
||||||
|
});
|
||||||
|
await loadFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeFile(path: string) {
|
||||||
|
await fetch(`/webui/api/memory${qp('path', path)}`, { method: 'DELETE' });
|
||||||
|
if (active === path) {
|
||||||
|
setActive('');
|
||||||
|
setContent('');
|
||||||
|
}
|
||||||
|
await loadFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createFile() {
|
||||||
|
const name = prompt('memory file name', `note-${Date.now()}.md`);
|
||||||
|
if (!name) return;
|
||||||
|
await fetch(`/webui/api/memory${q}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ path: name, content: '' }),
|
||||||
|
});
|
||||||
|
await loadFiles();
|
||||||
|
await openFile(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadFiles().catch(() => {});
|
||||||
|
}, [q]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full">
|
||||||
|
<aside className="w-72 border-r border-zinc-800 p-4 space-y-2 overflow-y-auto">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="font-semibold">Memory Files</h2>
|
||||||
|
<button onClick={createFile} className="px-2 py-1 rounded bg-zinc-800">+</button>
|
||||||
|
</div>
|
||||||
|
{files.map((f) => (
|
||||||
|
<div key={f} className={`flex items-center justify-between p-2 rounded ${active === f ? 'bg-zinc-800' : 'hover:bg-zinc-900'}`}>
|
||||||
|
<button className="text-left flex-1" onClick={() => openFile(f)}>{f}</button>
|
||||||
|
<button className="text-red-400" onClick={() => removeFile(f)}>x</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</aside>
|
||||||
|
<main className="flex-1 p-4 space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="font-semibold">{active || 'No file selected'}</h2>
|
||||||
|
<button onClick={saveFile} className="px-3 py-1 rounded bg-indigo-600">Save</button>
|
||||||
|
</div>
|
||||||
|
<textarea value={content} onChange={(e) => setContent(e.target.value)} className="w-full h-[80vh] bg-zinc-900 border border-zinc-800 rounded p-3" />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Memory;
|
||||||
@@ -13,7 +13,7 @@ export type CronJob = {
|
|||||||
to?: string;
|
to?: string;
|
||||||
};
|
};
|
||||||
export type Cfg = Record<string, any>;
|
export type Cfg = Record<string, any>;
|
||||||
export type View = 'dashboard' | 'chat' | 'config' | 'cron' | 'nodes';
|
export type View = 'dashboard' | 'chat' | 'config' | 'cron' | 'nodes' | 'memory';
|
||||||
export type Lang = 'en' | 'zh';
|
export type Lang = 'en' | 'zh';
|
||||||
|
|
||||||
export type LogEntry = {
|
export type LogEntry = {
|
||||||
|
|||||||
Reference in New Issue
Block a user