079project 6.0.0 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,286 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
2
+ import './App.css';
3
+ import AuthGate from './components/AuthGate';
4
+ import { api } from './api/client';
5
+ import ConfigPanel from './components/ConfigPanel';
6
+
7
+ const loadJson = (key, fallback) => {
8
+ try {
9
+ const raw = localStorage.getItem(key);
10
+ if (!raw) return fallback;
11
+ return JSON.parse(raw);
12
+ } catch (_e) {
13
+ return fallback;
14
+ }
15
+ };
16
+
17
+ const saveJson = (key, value) => {
18
+ try {
19
+ localStorage.setItem(key, JSON.stringify(value));
20
+ } catch (_e) {}
21
+ };
22
+
23
+ function App() {
24
+ const [account, setAccount] = useState(() => loadJson('phoenix.account', { id: 'local', name: 'Local User' }));
25
+ const [sessions, setSessions] = useState(() => loadJson('phoenix.sessions', []));
26
+ const [activeSessionId, setActiveSessionId] = useState(() => loadJson('phoenix.activeSessionId', null));
27
+ const [activePage, setActivePage] = useState(() => loadJson('phoenix.activePage', 'chat'));
28
+ const [message, setMessage] = useState('');
29
+ const [busy, setBusy] = useState(false);
30
+ const [status, setStatus] = useState(null);
31
+ const [error, setError] = useState(null);
32
+ const inputRef = useRef(null);
33
+
34
+ const activeSession = useMemo(() => {
35
+ const found = sessions.find((s) => s.id === activeSessionId);
36
+ return found || null;
37
+ }, [sessions, activeSessionId]);
38
+
39
+ useEffect(() => {
40
+ saveJson('phoenix.account', account);
41
+ }, [account]);
42
+ useEffect(() => {
43
+ saveJson('phoenix.sessions', sessions);
44
+ }, [sessions]);
45
+ useEffect(() => {
46
+ saveJson('phoenix.activeSessionId', activeSessionId);
47
+ }, [activeSessionId]);
48
+ useEffect(() => {
49
+ saveJson('phoenix.activePage', activePage);
50
+ }, [activePage]);
51
+
52
+ useEffect(() => {
53
+ let cancelled = false;
54
+ api.systemStatus()
55
+ .then((r) => {
56
+ if (!cancelled) setStatus(r);
57
+ })
58
+ .catch((e) => {
59
+ if (!cancelled) setStatus({ ok: false, error: e.message });
60
+ });
61
+ return () => {
62
+ cancelled = true;
63
+ };
64
+ }, []);
65
+
66
+ const ensureSession = () => {
67
+ if (activeSession) return activeSession;
68
+ const id = `s_${Date.now().toString(36)}_${Math.random().toString(16).slice(2)}`;
69
+ const session = { id, title: 'New chat', createdAt: Date.now(), messages: [] };
70
+ setSessions((prev) => [session, ...prev]);
71
+ setActiveSessionId(id);
72
+ return session;
73
+ };
74
+
75
+ const appendMessage = (sessionId, msg) => {
76
+ setSessions((prev) =>
77
+ prev.map((s) =>
78
+ s.id === sessionId
79
+ ? {
80
+ ...s,
81
+ messages: [...s.messages, msg],
82
+ title: s.title === 'New chat' ? (msg.role === 'user' ? msg.text.slice(0, 24) || 'Chat' : s.title) : s.title
83
+ }
84
+ : s
85
+ )
86
+ );
87
+ };
88
+
89
+ const onSend = async () => {
90
+ const text = message.trim();
91
+ if (!text || busy) return;
92
+ setError(null);
93
+ setBusy(true);
94
+ const session = ensureSession();
95
+ const sid = session.id;
96
+ appendMessage(sid, { id: `m_${Date.now()}`, role: 'user', text, ts: Date.now() });
97
+ setMessage('');
98
+ try {
99
+ const r = await api.chat(text, sid);
100
+ const reply = r?.result?.reply ?? '';
101
+ appendMessage(sid, {
102
+ id: `m_${Date.now()}_ai`,
103
+ role: 'assistant',
104
+ text: reply,
105
+ ts: Date.now(),
106
+ meta: {
107
+ latency: r?.result?.latency,
108
+ seeds: r?.result?.seeds,
109
+ memes: r?.result?.memes
110
+ }
111
+ });
112
+ } catch (e) {
113
+ setError(e.message);
114
+ appendMessage(sid, { id: `m_${Date.now()}_err`, role: 'system', text: `Error: ${e.message}`, ts: Date.now() });
115
+ } finally {
116
+ setBusy(false);
117
+ setTimeout(() => inputRef.current?.focus(), 0);
118
+ }
119
+ };
120
+
121
+ const newSession = () => {
122
+ const id = `s_${Date.now().toString(36)}_${Math.random().toString(16).slice(2)}`;
123
+ const session = { id, title: 'New chat', createdAt: Date.now(), messages: [] };
124
+ setSessions((prev) => [session, ...prev]);
125
+ setActiveSessionId(id);
126
+ setTimeout(() => inputRef.current?.focus(), 0);
127
+ };
128
+
129
+ const deleteSession = (id) => {
130
+ setSessions((prev) => prev.filter((s) => s.id !== id));
131
+ if (activeSessionId === id) {
132
+ setActiveSessionId(null);
133
+ }
134
+ };
135
+
136
+ return (
137
+ <AuthGate>
138
+ <div className="phoenix-root">
139
+ <aside className="sidebar">
140
+ <div className="brand">
141
+ <div className="brand-title">079 Phoenix</div>
142
+ <div className="brand-sub">AI workspace</div>
143
+ </div>
144
+
145
+ <div className="account">
146
+ <div className="account-row">
147
+ <div className="avatar">{(account?.name || 'U').slice(0, 1).toUpperCase()}</div>
148
+ <div className="account-meta">
149
+ <div className="account-name">{account?.name || 'User'}</div>
150
+ <div className="account-id">{account?.id || 'local'}</div>
151
+ </div>
152
+ </div>
153
+ <div className="account-actions">
154
+ <button className="btn btn-ghost" onClick={() => setAccount((a) => ({ ...a, name: a.name === 'Local User' ? 'Operator' : 'Local User' }))}>
155
+ 切换昵称
156
+ </button>
157
+ <button className="btn" onClick={newSession}>
158
+ 新会话
159
+ </button>
160
+ </div>
161
+ </div>
162
+
163
+ <div className="sessions">
164
+ <div className="section-title">会话</div>
165
+ <div className="session-list">
166
+ {sessions.length === 0 ? (
167
+ <div className="muted">暂无会话</div>
168
+ ) : (
169
+ sessions.map((s) => (
170
+ <div key={s.id} className={`session-item ${s.id === activeSessionId ? 'active' : ''}`}>
171
+ <button className="session-main" onClick={() => setActiveSessionId(s.id)}>
172
+ <div className="session-title">{s.title || 'Chat'}</div>
173
+ <div className="session-sub">{new Date(s.createdAt).toLocaleString()}</div>
174
+ </button>
175
+ <button className="session-del" onClick={() => deleteSession(s.id)} title="删除">
176
+ ×
177
+ </button>
178
+ </div>
179
+ ))
180
+ )}
181
+ </div>
182
+ </div>
183
+
184
+ <div className="nav">
185
+ <div className="section-title">导航</div>
186
+ <div className="nav-list">
187
+ <button className={`nav-item ${activePage === 'chat' ? 'active' : ''}`} onClick={() => setActivePage('chat')}>
188
+ Chat
189
+ </button>
190
+ <button className={`nav-item ${activePage === 'config' ? 'active' : ''}`} onClick={() => setActivePage('config')}>
191
+ Config
192
+ </button>
193
+ </div>
194
+ </div>
195
+
196
+ <div className="status">
197
+ <div className="section-title">后端</div>
198
+ <div className="status-row">
199
+ <span className={`dot ${status?.ok ? 'ok' : 'bad'}`} />
200
+ <span className="muted">{status?.ok ? 'connected' : 'disconnected'}</span>
201
+ </div>
202
+ </div>
203
+ </aside>
204
+
205
+ <main className="main">
206
+ {activePage === 'chat' ? (
207
+ <>
208
+ <header className="topbar">
209
+ <div className="topbar-title">{activeSession ? activeSession.title : '请选择或新建会话'}</div>
210
+ <div className="topbar-actions">
211
+ <button
212
+ className="btn btn-ghost"
213
+ onClick={() => api.snapshotCreate('ui').then(() => api.systemStatus().then(setStatus)).catch((e) => setError(e.message))}
214
+ >
215
+ 保存快照
216
+ </button>
217
+ <button className="btn btn-ghost" onClick={() => api.barrierStats().then(() => {}).catch((e) => setError(e.message))}>
218
+ Barrier
219
+ </button>
220
+ </div>
221
+ </header>
222
+
223
+ <section className="chat">
224
+ <div className="chat-scroll">
225
+ {(activeSession?.messages || []).map((m) => (
226
+ <div key={m.id} className={`msg ${m.role}`}>
227
+ <div className="msg-role">{m.role}</div>
228
+ <div className="msg-bubble">
229
+ <div className="msg-text">{m.text}</div>
230
+ {m.meta ? (
231
+ <div className="msg-meta">
232
+ {m.meta.latency != null ? <span>latency: {m.meta.latency}ms</span> : null}
233
+ {Array.isArray(m.meta.seeds) ? <span> seeds: {m.meta.seeds.length}</span> : null}
234
+ {Array.isArray(m.meta.memes) ? <span> memes: {m.meta.memes.length}</span> : null}
235
+ </div>
236
+ ) : null}
237
+ </div>
238
+ </div>
239
+ ))}
240
+ </div>
241
+
242
+ <div className="composer">
243
+ <input
244
+ ref={inputRef}
245
+ className="composer-input"
246
+ value={message}
247
+ placeholder={busy ? '生成中…' : '输入消息,Enter 发送'}
248
+ onChange={(e) => setMessage(e.target.value)}
249
+ onKeyDown={(e) => {
250
+ if (e.key === 'Enter' && !e.shiftKey) {
251
+ e.preventDefault();
252
+ onSend();
253
+ }
254
+ }}
255
+ />
256
+ <button className="btn" disabled={busy || !message.trim()} onClick={onSend}>
257
+ 发送
258
+ </button>
259
+ </div>
260
+
261
+ {error ? <div className="error">{error}</div> : null}
262
+ </section>
263
+ </>
264
+ ) : (
265
+ <>
266
+ <header className="topbar">
267
+ <div className="topbar-title">Config</div>
268
+ <div className="topbar-actions">
269
+ <button className="btn btn-ghost" onClick={() => api.systemStatus().then(setStatus).catch((e) => setError(e.message))}>
270
+ 刷新后端状态
271
+ </button>
272
+ </div>
273
+ </header>
274
+ <section className="cfg-wrap">
275
+ <ConfigPanel onError={(msg) => setError(msg)} />
276
+ {error ? <div className="error">{error}</div> : null}
277
+ </section>
278
+ </>
279
+ )}
280
+ </main>
281
+ </div>
282
+ </AuthGate>
283
+ );
284
+ }
285
+
286
+ export default App;
@@ -0,0 +1,8 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import App from './App';
3
+
4
+ test('renders shell', () => {
5
+ render(<App />);
6
+ expect(screen.getByText(/079 phoenix/i)).toBeInTheDocument();
7
+ expect(screen.getByText('新会话')).toBeInTheDocument();
8
+ });
@@ -0,0 +1,103 @@
1
+ const API_BASE = process.env.REACT_APP_API_BASE || '';
2
+
3
+ const TOKEN_KEY = 'phoenix_auth_token';
4
+
5
+ export function getAuthToken() {
6
+ try {
7
+ return localStorage.getItem(TOKEN_KEY) || '';
8
+ } catch (_e) {
9
+ return '';
10
+ }
11
+ }
12
+
13
+ export function setAuthToken(token) {
14
+ try {
15
+ if (!token) localStorage.removeItem(TOKEN_KEY);
16
+ else localStorage.setItem(TOKEN_KEY, String(token));
17
+ } catch (_e) {
18
+ // ignore
19
+ }
20
+ }
21
+
22
+ async function request(path, { method = 'GET', body, headers } = {}) {
23
+ const token = getAuthToken();
24
+ const res = await fetch(`${API_BASE}${path}`, {
25
+ method,
26
+ headers: {
27
+ 'Content-Type': 'application/json',
28
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
29
+ ...(headers || {})
30
+ },
31
+ body: body !== undefined ? JSON.stringify(body) : undefined
32
+ });
33
+ const text = await res.text();
34
+ let json;
35
+ try {
36
+ json = text ? JSON.parse(text) : null;
37
+ } catch (_e) {
38
+ json = { ok: false, error: 'invalid-json', raw: text };
39
+ }
40
+ if (!res.ok) {
41
+ const msg = json && typeof json === 'object' ? (json.error || json.message || res.statusText) : res.statusText;
42
+ const err = new Error(msg);
43
+ err.status = res.status;
44
+ err.payload = json;
45
+ throw err;
46
+ }
47
+ return json;
48
+ }
49
+
50
+ export const api = {
51
+ authConfig: () => request('/auth/config'),
52
+ authBootstrap: (username, password) => request('/auth/bootstrap', { method: 'POST', body: { username, password } }),
53
+ authLogin: async (username, password) => {
54
+ const out = await request('/auth/login', { method: 'POST', body: { username, password } });
55
+ if (out?.token) setAuthToken(out.token);
56
+ return out;
57
+ },
58
+ authMe: () => request('/auth/me'),
59
+ authLogout: async () => {
60
+ const out = await request('/auth/logout', { method: 'POST', body: {} });
61
+ setAuthToken('');
62
+ return out;
63
+ },
64
+ chat: (text, sessionId) => request('/api/chat', { method: 'POST', body: { text, sessionId } }),
65
+ arrayChat: (text, sessionId, options) => request('/api/array/chat', { method: 'POST', body: { text, sessionId, options } }),
66
+ runtimeFeatures: () => request('/api/runtime/features'),
67
+ runtimePatch: (patch) => request('/api/runtime/features', { method: 'PATCH', body: patch || {} }),
68
+ studyStatus: () => request('/api/study/status'),
69
+ dialogReset: () => request('/api/learn/dialog/reset', { method: 'POST', body: {} }),
70
+ searchConfig: () => request('/api/search/config'),
71
+ setSearchConfig: (config) => request('/api/search/config', { method: 'PUT', body: config || {} }),
72
+ searchEndpointAdd: (url) => request('/api/search/endpoints/add', { method: 'POST', body: { url } }),
73
+ searchEndpointRemove: (url) => request('/api/search/endpoints/remove', { method: 'POST', body: { url } }),
74
+ getParams: () => request('/api/model/params'),
75
+ setParams: (params) => request('/api/model/params', { method: 'POST', body: params }),
76
+ resetParams: () => request('/api/model/params/reset', { method: 'POST', body: {} }),
77
+ snapshots: () => request('/api/snapshots'),
78
+ snapshotCreate: (name) => request('/api/snapshots/create', { method: 'POST', body: { name } }),
79
+ snapshotRestore: (id) => request(`/api/snapshots/restore/${encodeURIComponent(id)}`, { method: 'POST', body: {} }),
80
+ snapshotDelete: (id) => request(`/api/snapshots/${encodeURIComponent(id)}`, { method: 'DELETE' }),
81
+ systemStatus: () => request('/api/system/status'),
82
+ systemConfig: () => request('/api/system/config'),
83
+ groups: () => request('/api/groups'),
84
+ groupMetrics: (gid) => request(`/api/groups/${encodeURIComponent(gid)}/metrics`),
85
+ requestOnline: (query, options) => request('/api/corpus/online', { method: 'POST', body: { query, options: options || {} } }),
86
+ shards: () => request('/api/shards'),
87
+ robotsList: () => request('/robots/list'),
88
+ robotsIngest: (options) => request('/robots/ingest', { method: 'POST', body: options || {} }),
89
+ robotsRetrain: (options) => request('/api/robots/retrain', { method: 'POST', body: options || {} }),
90
+ testsList: () => request('/api/tests/list'),
91
+ testsCase: (name, content) => request('/api/tests/case', { method: 'POST', body: { name, content } }),
92
+ testsRefresh: () => request('/api/tests/refresh', { method: 'POST', body: {} }),
93
+ exportGraph: (seeds, radius) => request('/api/export/graph', { method: 'POST', body: { seeds, radius } }),
94
+ exportGraphGroup: (groupId, seeds, radius) => request('/api/export/graph/group', { method: 'POST', body: { groupId, seeds, radius } }),
95
+ rlLearn: (cycles) => request('/api/learn/reinforce', { method: 'POST', body: { cycles } }),
96
+ rlLatest: () => request('/api/learn/reinforce/latest'),
97
+ advLearn: (samples) => request('/api/learn/adversarial', { method: 'POST', body: { samples } }),
98
+ advLatest: () => request('/api/learn/adversarial/latest'),
99
+ learnThresholds: (rlEvery, advEvery) => request('/api/learn/thresholds', { method: 'POST', body: { rlEvery, advEvery } }),
100
+ barrierStart: (maliciousThreshold) => request('/api/memebarrier/start', { method: 'POST', body: { maliciousThreshold } }),
101
+ barrierStop: () => request('/api/memebarrier/stop', { method: 'POST', body: {} }),
102
+ barrierStats: () => request('/api/memebarrier/stats')
103
+ };
@@ -0,0 +1,153 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { api } from '../api/client';
3
+
4
+ export default function AuthGate({ children }) {
5
+ const [state, setState] = useState({ loading: true, user: null, error: null, needsBootstrap: false });
6
+
7
+ useEffect(() => {
8
+ let alive = true;
9
+ (async () => {
10
+ try {
11
+ const out = await api.authMe();
12
+ if (!alive) return;
13
+ setState({ loading: false, user: out.user, error: null, needsBootstrap: false });
14
+ } catch (e) {
15
+ if (!alive) return;
16
+ // If backend hasn't been bootstrapped yet, login will 401 but bootstrap should be allowed.
17
+ setState({ loading: false, user: null, error: e, needsBootstrap: false });
18
+ }
19
+ })();
20
+ return () => {
21
+ alive = false;
22
+ };
23
+ }, []);
24
+
25
+ if (state.loading) {
26
+ return (
27
+ <div style={{ padding: 24, color: '#e5e7eb' }}>
28
+ <div style={{ fontSize: 18, fontWeight: 700 }}>正在验证身份…</div>
29
+ </div>
30
+ );
31
+ }
32
+
33
+ if (state.user) {
34
+ return children;
35
+ }
36
+
37
+ return <LoginPanel onAuthed={(user) => setState({ loading: false, user, error: null, needsBootstrap: false })} />;
38
+ }
39
+
40
+ function LoginPanel({ onAuthed }) {
41
+ const [mode, setMode] = useState('login'); // login | bootstrap
42
+ const [username, setUsername] = useState('admin');
43
+ const [password, setPassword] = useState('');
44
+ const [status, setStatus] = useState({ busy: false, error: null, info: null });
45
+
46
+ async function handleLogin() {
47
+ setStatus({ busy: true, error: null, info: null });
48
+ try {
49
+ const out = await api.authLogin(username, password);
50
+ const me = await api.authMe();
51
+ onAuthed(me.user);
52
+ setStatus({ busy: false, error: null, info: `欢迎 ${out.user?.username || username}` });
53
+ } catch (e) {
54
+ setStatus({ busy: false, error: e?.message || '登录失败', info: null });
55
+ }
56
+ }
57
+
58
+ async function handleBootstrap() {
59
+ setStatus({ busy: true, error: null, info: null });
60
+ try {
61
+ await api.authBootstrap(username, password);
62
+ await handleLogin();
63
+ } catch (e) {
64
+ setStatus({ busy: false, error: e?.message || '初始化失败', info: null });
65
+ }
66
+ }
67
+
68
+ return (
69
+ <div style={{ minHeight: '100vh', background: '#0b0f19', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
70
+ <div style={{ width: 420, background: '#121a2a', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 12, padding: 18 }}>
71
+ <div style={{ color: '#e5e7eb', fontSize: 18, fontWeight: 800, marginBottom: 8 }}>身份验证</div>
72
+ <div style={{ color: '#9aa4b2', fontSize: 12, marginBottom: 12 }}>
73
+ {mode === 'login' ? '请输入用户名与密码登录。' : '首次启动:创建管理员账号(只允许一次)。'}
74
+ </div>
75
+
76
+ <div style={{ display: 'grid', gap: 10 }}>
77
+ <label style={{ color: '#cbd5e1', fontSize: 12 }}>
78
+ 用户名
79
+ <input
80
+ value={username}
81
+ onChange={(e) => setUsername(e.target.value)}
82
+ style={inputStyle}
83
+ placeholder="admin"
84
+ autoComplete="username"
85
+ />
86
+ </label>
87
+ <label style={{ color: '#cbd5e1', fontSize: 12 }}>
88
+ 密码
89
+ <input
90
+ type="password"
91
+ value={password}
92
+ onChange={(e) => setPassword(e.target.value)}
93
+ style={inputStyle}
94
+ placeholder="至少 6 位"
95
+ autoComplete={mode === 'login' ? 'current-password' : 'new-password'}
96
+ />
97
+ </label>
98
+
99
+ {status.error ? <div style={{ color: '#fca5a5', fontSize: 12 }}>{String(status.error)}</div> : null}
100
+ {status.info ? <div style={{ color: '#86efac', fontSize: 12 }}>{String(status.info)}</div> : null}
101
+
102
+ <div style={{ display: 'flex', gap: 10, alignItems: 'center', justifyContent: 'space-between' }}>
103
+ <button
104
+ onClick={mode === 'login' ? handleLogin : handleBootstrap}
105
+ disabled={status.busy}
106
+ style={primaryBtn}
107
+ >
108
+ {status.busy ? '处理中…' : mode === 'login' ? '登录' : '创建管理员并登录'}
109
+ </button>
110
+
111
+ <button
112
+ onClick={() => setMode(mode === 'login' ? 'bootstrap' : 'login')}
113
+ disabled={status.busy}
114
+ style={linkBtn}
115
+ >
116
+ {mode === 'login' ? '首次启动?去初始化' : '返回登录'}
117
+ </button>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ );
123
+ }
124
+
125
+ const inputStyle = {
126
+ width: '100%',
127
+ marginTop: 6,
128
+ padding: '10px 12px',
129
+ borderRadius: 10,
130
+ border: '1px solid rgba(255,255,255,0.12)',
131
+ background: '#0b1220',
132
+ color: '#e5e7eb',
133
+ outline: 'none'
134
+ };
135
+
136
+ const primaryBtn = {
137
+ padding: '10px 12px',
138
+ borderRadius: 10,
139
+ border: '1px solid rgba(255,255,255,0.10)',
140
+ background: '#2563eb',
141
+ color: 'white',
142
+ fontWeight: 700,
143
+ cursor: 'pointer'
144
+ };
145
+
146
+ const linkBtn = {
147
+ padding: '10px 8px',
148
+ borderRadius: 10,
149
+ border: 'none',
150
+ background: 'transparent',
151
+ color: '#93c5fd',
152
+ cursor: 'pointer'
153
+ };