dsh-contextos-devops 0.0.0-stage → 1.48.31
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.
- package/LICENSE +21 -0
- package/README.md +207 -2
- package/assets/avatar.jpg +0 -0
- package/assets/client.css +3316 -0
- package/assets/icon-180.png +0 -0
- package/build/client.mjs +64 -0
- package/client.js +7622 -0
- package/cordis.patch.yml +3 -0
- package/icon.svg +15 -0
- package/index.js +345 -0
- package/lib/base/env.js +22 -0
- package/lib/base/field.js +25 -0
- package/lib/base/http.js +140 -0
- package/lib/base/platform.js +33 -0
- package/lib/base/registry.js +186 -0
- package/lib/base/state.js +66 -0
- package/lib/base/table.js +47 -0
- package/lib/base/texttable.js +26 -0
- package/lib/base/url.js +178 -0
- package/lib/core/README.md +17 -0
- package/lib/core/apps.js +675 -0
- package/lib/core/auth.js +110 -0
- package/lib/core/avatar.js +289 -0
- package/lib/core/backup.js +105 -0
- package/lib/core/codegen.js +401 -0
- package/lib/core/copilot.js +419 -0
- package/lib/core/data.js +157 -0
- package/lib/core/device.js +148 -0
- package/lib/core/files.js +152 -0
- package/lib/core/gobuild.js +63 -0
- package/lib/core/iam.js +122 -0
- package/lib/core/identity.js +22 -0
- package/lib/core/nodes.js +627 -0
- package/lib/core/ops.js +306 -0
- package/lib/core/project.js +393 -0
- package/lib/core/spaces.js +406 -0
- package/lib/core/terminal.js +137 -0
- package/lib/core/terminal2.js +141 -0
- package/lib/core/users.js +51 -0
- package/lib/core/vocab.js +158 -0
- package/lib/misc/async.js +15 -0
- package/lib/misc/shell.js +27 -0
- package/lib/misc/static.js +33 -0
- package/locale/en.json +10 -0
- package/locale/zh.json +10 -0
- package/package.json +91 -4
- package/src/client/README.md +37 -0
- package/src/client/parts/00-prelude.js +315 -0
- package/src/client/parts/05-bootstrap.js +61 -0
- package/src/client/parts/08-store.js +85 -0
- package/src/client/parts/10-ui.js +188 -0
- package/src/client/parts/12-debug.js +361 -0
- package/src/client/parts/15-layout.js +86 -0
- package/src/client/parts/20-devworkspace.js +789 -0
- package/src/client/parts/25-editor-ui.js +28 -0
- package/src/client/parts/30-modals.js +85 -0
- package/src/client/parts/32-cards.js +176 -0
- package/src/client/parts/34-forms.js +176 -0
- package/src/client/parts/36-data.js +267 -0
- package/src/client/parts/40-project.js +430 -0
- package/src/client/parts/45-map.js +109 -0
- package/src/client/parts/46-pm.js +158 -0
- package/src/client/parts/47-three.js +324 -0
- package/src/client/parts/49-msg.js +167 -0
- package/src/client/parts/50-chat.js +1422 -0
- package/src/client/parts/51-pmselection.js +31 -0
- package/src/client/parts/52-pmdata.js +29 -0
- package/src/client/parts/55-pm.js +146 -0
- package/src/client/parts/58-config.js +381 -0
- package/src/client/parts/59-monitor.js +262 -0
- package/src/client/parts/60-page.js +1518 -0
- package/src/client/styles/kit.css +20 -0
- package/src/client/styles/main.css +3294 -0
- package/src/client/styles/tokens.css +2 -0
- package/vendor/leaflet/leaflet.css +661 -0
- package/vendor/leaflet/leaflet.js +6 -0
- package/vendor/three/three.module.js +53044 -0
- package/vendor/xterm/addon-fit.js +2 -0
- package/vendor/xterm/xterm.css +218 -0
- package/vendor/xterm/xterm.js +2 -0
|
@@ -0,0 +1,1518 @@
|
|
|
1
|
+
function PipeSelect({ icon, zh, value, options, onChange, title }) {
|
|
2
|
+
const [open, setOpen] = useState(false);
|
|
3
|
+
useEffect(() => {
|
|
4
|
+
if (!open) return;
|
|
5
|
+
const close = () => setOpen(false);
|
|
6
|
+
const t = setTimeout(() => window.addEventListener('click', close, { once: true }), 0);
|
|
7
|
+
return () => { clearTimeout(t); window.removeEventListener('click', close); };
|
|
8
|
+
}, [open]);
|
|
9
|
+
const opts = options || [];
|
|
10
|
+
return h('div', { className: 'cx-pipe-sel' },
|
|
11
|
+
h('button', {
|
|
12
|
+
type: 'button', className: 'cx-stage cx-stage-title cx-stage-btn', title: title || zh,
|
|
13
|
+
onClick: (e) => { e.stopPropagation(); setOpen((v) => !v); },
|
|
14
|
+
},
|
|
15
|
+
h(Ico, { name: icon, size: 18 }),
|
|
16
|
+
h('span', { className: 'cx-aff-zh' }, zh),
|
|
17
|
+
h('span', { className: 'cx-aff-value' }, value),
|
|
18
|
+
h('span', { className: 'cx-pipe-caret' }, '⌄')),
|
|
19
|
+
open ? h('div', { className: 'cx-pipe-menu' }, opts.length ? opts.map((o) => h('button', {
|
|
20
|
+
key: String(o.value), type: 'button', className: `cx-menu-item${o.on ? ' on' : ''}`,
|
|
21
|
+
onClick: (e) => { e.stopPropagation(); onChange(o.value); setOpen(false); },
|
|
22
|
+
}, o.label)) : h('div', { className: 'cx-menu-item empty' }, '—')) : null);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function Page() {
|
|
26
|
+
const [toast, setToast] = useState(null);
|
|
27
|
+
const [dataPulse, setDataPulse] = useState(0);
|
|
28
|
+
const bootSelRef = useRef(null);
|
|
29
|
+
if (bootSelRef.current === null) bootSelRef.current = cxRead('sel', {});
|
|
30
|
+
// 上次退出时的整套状态:节点/空间/实例/工作模式/视图/网页标签
|
|
31
|
+
const bootRef = useRef(null);
|
|
32
|
+
if (bootRef.current === null) bootRef.current = cxSessionGet() || {};
|
|
33
|
+
const boot = bootRef.current;
|
|
34
|
+
const restored = !!boot.view;
|
|
35
|
+
const nodeFresh = (x) => !!x && !!(x.builtAt || x.sizeText || (x.runtime && x.runtime.builtAt));
|
|
36
|
+
const [nodes, setNodes] = useState(() => cxRead('nodes', []).filter(nodeFresh));
|
|
37
|
+
const nodesRef = useRef(nodes); nodesRef.current = nodes;
|
|
38
|
+
const bootedRef = useRef(false);
|
|
39
|
+
const [selMap, setSelMap] = useState(() => cxRead('sel-map', {}));
|
|
40
|
+
const selMapRef = useRef(selMap); selMapRef.current = selMap;
|
|
41
|
+
useEffect(() => cxWrite('sel-map', selMap), [selMap]);
|
|
42
|
+
const rememberSel = (port, patch) => { if (!port) return; setSelMap((m) => ({ ...m, [port]: { ...(m[port] || {}), ...patch } })); };
|
|
43
|
+
const appKeyOf = (a) => (a ? `${a.space || ''}:${a.command || ''}:${a.place_uid || a.name || ''}` : '');
|
|
44
|
+
const [node, setNode] = useState(() => boot.node ?? bootSelRef.current?.node ?? null);
|
|
45
|
+
const [spaces, setSpaces] = useState([]);
|
|
46
|
+
const [space, setSpace] = useState(() => boot.space ?? bootSelRef.current?.space ?? null);
|
|
47
|
+
const [apps, setApps] = useState([]);
|
|
48
|
+
const [app, setApp] = useState(() => boot.app ?? bootSelRef.current?.app ?? null);
|
|
49
|
+
const [modal, setModal] = useState(null);
|
|
50
|
+
// 消息列表里的数据表单请求(替代 DATA 弹窗)
|
|
51
|
+
const [formReq, setFormReq] = useState(null);
|
|
52
|
+
const [appMode, setAppMode] = useState('recent');
|
|
53
|
+
const [store, setStore] = useState([]);
|
|
54
|
+
const [storeService, setStoreService] = useState(null);
|
|
55
|
+
const [webTabs, setWebTabs] = useState(() => {
|
|
56
|
+
try {
|
|
57
|
+
const saved = Array.isArray(boot.tabs) && boot.tabs.length ? { tabs: boot.tabs, active: boot.active } : cxRead('webtabs', null);
|
|
58
|
+
if (!saved || !Array.isArray(saved.tabs)) return [];
|
|
59
|
+
return saved.tabs.filter((t) => t && (t.base || t.url)).slice(0, 8).map((t, i) => ({
|
|
60
|
+
id: t.id || `w${Date.now().toString(36)}${i}`,
|
|
61
|
+
base: t.base || t.url, url: t.base || t.url,
|
|
62
|
+
title: t.title || t.base || t.url,
|
|
63
|
+
reload: 0, app: t.app || null,
|
|
64
|
+
}));
|
|
65
|
+
} catch { return []; }
|
|
66
|
+
});
|
|
67
|
+
const [webLoaded, setWebLoaded] = useState({});
|
|
68
|
+
const [webMenu, setWebMenu] = useState(null);
|
|
69
|
+
useEffect(() => { if (!webMenu) return; const close = () => setWebMenu(null); window.addEventListener('mousedown', close); return () => window.removeEventListener('mousedown', close); }, [webMenu]);
|
|
70
|
+
const [activeWeb, setActiveWeb] = useState(() => {
|
|
71
|
+
if (boot.active) return boot.active;
|
|
72
|
+
return cxRead('webtabs', null)?.active || null;
|
|
73
|
+
});
|
|
74
|
+
const [viewMode, setViewMode] = useState(() => boot.view || 'chat');
|
|
75
|
+
const [intro, setIntro] = useState(null);
|
|
76
|
+
const [devMounted, setDevMounted] = useState(viewMode === 'dev');
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (viewMode !== 'dev' || devMounted) return;
|
|
79
|
+
const t = setTimeout(() => setDevMounted(true), 160); /* 先出过场,再挂载重型编辑器 */
|
|
80
|
+
return () => clearTimeout(t);
|
|
81
|
+
}, [viewMode, devMounted]);
|
|
82
|
+
const [workMode, setWorkMode] = useState(() => boot.mode || cxReadText('work-mode', 'avatar'));
|
|
83
|
+
useEffect(() => cxWriteText('work-mode', workMode), [workMode]);
|
|
84
|
+
const [chatFull, setChatFull] = useState(() => cxReadText('chatfull', '1') === '1');
|
|
85
|
+
const [themeMode, setThemeMode] = useState(() => cxReadText('theme', 'system'));
|
|
86
|
+
|
|
87
|
+
const [opFx, setOpFx] = useState(null);
|
|
88
|
+
const [pipeMenu, setPipeMenu] = useState(null);
|
|
89
|
+
const [sidebarHidden, setSidebarHidden] = useState(false);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
const el = shellRef.current;
|
|
92
|
+
if (!el) return;
|
|
93
|
+
const update = () => setSidebarHidden(el.getBoundingClientRect().left < 120);
|
|
94
|
+
update();
|
|
95
|
+
const ro = new ResizeObserver(update); ro.observe(el);
|
|
96
|
+
const iv = setInterval(update, 2000);
|
|
97
|
+
window.addEventListener('resize', update);
|
|
98
|
+
return () => { ro.disconnect(); clearInterval(iv); window.removeEventListener('resize', update); };
|
|
99
|
+
}, []);
|
|
100
|
+
const pipeModeLabel = (id) => {
|
|
101
|
+
const m = CHAT_MODES.find((x) => x.id === id);
|
|
102
|
+
if (!m) return id;
|
|
103
|
+
return locale === 'zh' ? m.name : ({ avatar: 'Digital Avatar', factory: 'Software Factory', iot: 'Cyber World' }[m.id] || m.id);
|
|
104
|
+
};
|
|
105
|
+
const pipeViewLabel = (id) => (locale === 'zh' ? ({ chat: '对话', manage: '管理', dev: '开发' }[id] || id) : String(id || '').toUpperCase());
|
|
106
|
+
const openPipe = (e, title, items, icon) => {
|
|
107
|
+
try { e.stopPropagation(); e.preventDefault(); } catch {}
|
|
108
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
109
|
+
const top = Math.min(r.bottom + 4, Math.max(80, (window.innerHeight || 800) - 340));
|
|
110
|
+
setPipeMenu({ x: Math.round(r.left), y: Math.round(top), title, items, icon });
|
|
111
|
+
};
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const onFloat = (e) => { const d = e && e.detail; if (!d) return; setPipeMenu({ x: d.x, right: d.right, y: d.y, title: d.title, items: d.items, icon: d.icon }); };
|
|
114
|
+
window.addEventListener('cx-float-menu', onFloat);
|
|
115
|
+
return () => window.removeEventListener('cx-float-menu', onFloat);
|
|
116
|
+
}, []);
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (!pipeMenu) return;
|
|
119
|
+
const close = () => setPipeMenu(null);
|
|
120
|
+
const t = setTimeout(() => window.addEventListener('click', close, { once: true }), 0);
|
|
121
|
+
return () => { clearTimeout(t); window.removeEventListener('click', close); };
|
|
122
|
+
}, [pipeMenu]);
|
|
123
|
+
// 鼠标跟随光效:--mx/--my 驱动光晕与光标环
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
const el = shellRef.current;
|
|
126
|
+
if (!el) return;
|
|
127
|
+
let raf = 0;
|
|
128
|
+
const onMove = (e) => {
|
|
129
|
+
if (raf) return;
|
|
130
|
+
raf = requestAnimationFrame(() => {
|
|
131
|
+
raf = 0;
|
|
132
|
+
const r = el.getBoundingClientRect();
|
|
133
|
+
el.style.setProperty('--mx', `${Math.round(e.clientX - r.left)}px`);
|
|
134
|
+
el.style.setProperty('--my', `${Math.round(e.clientY - r.top)}px`);
|
|
135
|
+
el.style.setProperty('--cx-cursor', '1');
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
const onLeave = () => { try { el.style.setProperty('--cx-cursor', '0'); } catch {} };
|
|
139
|
+
el.addEventListener('pointermove', onMove, { passive: true });
|
|
140
|
+
el.addEventListener('pointerleave', onLeave);
|
|
141
|
+
return () => { el.removeEventListener('pointermove', onMove); el.removeEventListener('pointerleave', onLeave); cancelAnimationFrame(raf); };
|
|
142
|
+
}, []);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const onOp = (e) => {
|
|
145
|
+
const kind = e?.detail?.kind;
|
|
146
|
+
if (!kind) return;
|
|
147
|
+
setOpFx({ kind, key: Date.now() });
|
|
148
|
+
try { window.__cxOpSfx && window.__cxOpSfx(kind); } catch {}
|
|
149
|
+
};
|
|
150
|
+
window.addEventListener('cx-data-op', onOp);
|
|
151
|
+
return () => window.removeEventListener('cx-data-op', onOp);
|
|
152
|
+
}, []);
|
|
153
|
+
const [fx, setFx] = useState(() => ({ ...cxStoreDef('fx'), ...cxRead('fx', {}) }));
|
|
154
|
+
useEffect(() => cxWrite('fx', fx), [fx]);
|
|
155
|
+
const fxSet = (k, v) => setFx((x) => ({ ...x, [k]: v }));
|
|
156
|
+
const [theme, setTheme] = useState(() => { const sys = (typeof window !== 'undefined' && window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) ? 'light' : 'dark'; const m = cxReadText('theme', 'system'); return m === 'system' ? sys : m; });
|
|
157
|
+
const lightTheme = themeMode === 'light' || theme === 'light';
|
|
158
|
+
const [langMode, setLangMode] = useState(() => cxReadText('lang', 'system'));
|
|
159
|
+
const prefsReadyRef = useRef(false);
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
(async () => {
|
|
162
|
+
try {
|
|
163
|
+
const st = await api('stateGet');
|
|
164
|
+
const p = st && st.prefs;
|
|
165
|
+
if (p && typeof p === 'object') {
|
|
166
|
+
if (p.theme) { setThemeMode(p.theme); cxWriteText('theme', p.theme); }
|
|
167
|
+
if (p.lang) { setLangMode(p.lang); cxWriteText('lang', p.lang); }
|
|
168
|
+
if (p.view) setViewMode(p.view);
|
|
169
|
+
if (p.work) setWorkMode(p.work);
|
|
170
|
+
}
|
|
171
|
+
} catch {}
|
|
172
|
+
prefsReadyRef.current = true;
|
|
173
|
+
})();
|
|
174
|
+
}, []);
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (!prefsReadyRef.current) return;
|
|
177
|
+
const tmr = setTimeout(() => { try { api('stateSet', { name: 'prefs', value: { theme: themeMode, lang: langMode, view: viewMode, work: workMode } }).catch(() => {}); } catch {} }, 800);
|
|
178
|
+
return () => clearTimeout(tmr);
|
|
179
|
+
}, [themeMode, langMode, viewMode, workMode]);
|
|
180
|
+
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
/* 实测回报:把真实 DOM 的计算字号与 bundle 版本写入服务端,便于核对 */
|
|
183
|
+
let tries = 0;
|
|
184
|
+
const timer = setInterval(() => {
|
|
185
|
+
tries += 1;
|
|
186
|
+
const el = document.querySelector('.cx-compose-input');
|
|
187
|
+
if (!el && tries < 10) return;
|
|
188
|
+
clearInterval(timer);
|
|
189
|
+
try {
|
|
190
|
+
const cs = el ? window.getComputedStyle(el) : null;
|
|
191
|
+
api('stateSet', { name: 'clientCheck', value: {
|
|
192
|
+
at: new Date().toISOString(),
|
|
193
|
+
version: String(CLIENT_CSS).split('?v=')[1] || '',
|
|
194
|
+
found: !!el,
|
|
195
|
+
count: document.querySelectorAll('.cx-compose-input').length,
|
|
196
|
+
fontSize: cs ? cs.fontSize : '',
|
|
197
|
+
lineHeight: cs ? cs.lineHeight : '',
|
|
198
|
+
inline: el ? (el.style.getPropertyValue('font-size') || '') : '',
|
|
199
|
+
dpr: window.devicePixelRatio || 1,
|
|
200
|
+
build: CLIENT_BUILD,
|
|
201
|
+
css: Array.from(document.querySelectorAll('link[data-cx-client-css]')).map((l) => String(l.getAttribute('href') || '')),
|
|
202
|
+
/* 布局自检:新 JS 配旧 CSS 时,下面几项会立刻暴露(如 main=flex/row、tools 无边距) */
|
|
203
|
+
layout: (() => {
|
|
204
|
+
try {
|
|
205
|
+
const m = document.querySelector('.cx-msg');
|
|
206
|
+
if (!m) return 'no-message';
|
|
207
|
+
const pick = (sel) => { const n = m.querySelector(sel); if (!n) return '-'; const c = window.getComputedStyle(n); return `${c.display}/${c.flexDirection}/${c.marginLeft}/${c.opacity}`; };
|
|
208
|
+
return `main=${pick('.cx-msg-main')} meta=${pick('.cx-msg-meta')} tools=${pick('.cx-msg-tools')}`;
|
|
209
|
+
} catch { return 'err'; }
|
|
210
|
+
})(),
|
|
211
|
+
/* 几何自检:正文占比与工具栏右边距,比字符串更能证明「看起来对不对」 */
|
|
212
|
+
geo: (() => {
|
|
213
|
+
try {
|
|
214
|
+
const m = document.querySelector('.cx-msg.ai') || document.querySelector('.cx-msg');
|
|
215
|
+
if (!m) return null;
|
|
216
|
+
const r = m.getBoundingClientRect();
|
|
217
|
+
const t = m.querySelector('.cx-msg-tools');
|
|
218
|
+
const b = m.querySelector('.cx-msg-body');
|
|
219
|
+
const tr = t ? t.getBoundingClientRect() : null;
|
|
220
|
+
const br = b ? b.getBoundingClientRect() : null;
|
|
221
|
+
return {
|
|
222
|
+
card: [Math.round(r.width), Math.round(r.height)],
|
|
223
|
+
toolsRightGap: tr ? Math.round(r.right - tr.right) : null,
|
|
224
|
+
bodyRatio: br && r.width ? Number((br.width / r.width).toFixed(2)) : null,
|
|
225
|
+
};
|
|
226
|
+
} catch { return null; }
|
|
227
|
+
})(),
|
|
228
|
+
/* DOM 取证:把第一条消息的真实结构与每层盒模型抄回来,无需再截图 */
|
|
229
|
+
dom: (() => {
|
|
230
|
+
try {
|
|
231
|
+
const m = document.querySelector('.cx-msg.ai') || document.querySelector('.cx-msg');
|
|
232
|
+
if (!m) return null;
|
|
233
|
+
const box = (sel) => {
|
|
234
|
+
const n = sel ? m.querySelector(sel) : m;
|
|
235
|
+
if (!n) return null;
|
|
236
|
+
const r = n.getBoundingClientRect(); const c = window.getComputedStyle(n);
|
|
237
|
+
return { w: Math.round(r.width), h: Math.round(r.height), x: Math.round(r.x),
|
|
238
|
+
d: c.display, fd: c.flexDirection, fg: c.flexGrow, ml: c.marginLeft, wd: c.width };
|
|
239
|
+
};
|
|
240
|
+
return { html: String(m.outerHTML || '').slice(0, 500),
|
|
241
|
+
card: box(null), main: box('.cx-msg-main'), meta: box('.cx-msg-meta'), tools: box('.cx-msg-tools'), body: box('.cx-msg-body') };
|
|
242
|
+
} catch { return null; }
|
|
243
|
+
})(),
|
|
244
|
+
} }).catch(() => {});
|
|
245
|
+
} catch {}
|
|
246
|
+
}, 400);
|
|
247
|
+
return () => clearInterval(timer);
|
|
248
|
+
}, []);
|
|
249
|
+
const [locale, setLocale] = useState(() => { const m = cxReadText('lang', 'system'); return m === 'system' ? browserLang() : m; });
|
|
250
|
+
CURRENT_LANG = locale === 'zh' ? 'zh' : 'en';
|
|
251
|
+
const bootConsole = cxRead('console', {});
|
|
252
|
+
const [consoleOpen, setConsoleOpen] = useState(bootConsole.open === true);
|
|
253
|
+
const [consoleMax, setConsoleMax] = useState(false);
|
|
254
|
+
const consoleBoxRef = useRef(null);
|
|
255
|
+
useEffect(() => { if (!consoleMax) return; return cxMaxApply(consoleBoxRef.current, { offsetTop: 44 }); }, [consoleMax]);
|
|
256
|
+
const [consoleTab, setConsoleTab] = useState(bootConsole.tab || 'activity');
|
|
257
|
+
const [termSplit, setTermSplit] = useState(false);
|
|
258
|
+
const consoleOpenRef = useRef(consoleOpen); consoleOpenRef.current = consoleOpen;
|
|
259
|
+
const viewModeRef = useRef(viewMode); viewModeRef.current = viewMode;
|
|
260
|
+
const initialReqLog = cxRead('reqlog', []);
|
|
261
|
+
const reqLogRef = useRef(initialReqLog);
|
|
262
|
+
const [reqLog, setReqLog] = useState(initialReqLog);
|
|
263
|
+
useEffect(() => cxWrite('reqlog', reqLog.slice(-50)), [reqLog]);
|
|
264
|
+
const pushReq = (e) => {
|
|
265
|
+
try {
|
|
266
|
+
reqLogRef.current = [...reqLogRef.current, e].slice(-50);
|
|
267
|
+
if (consoleOpenRef.current) setReqLog(reqLogRef.current);
|
|
268
|
+
} catch {}
|
|
269
|
+
};
|
|
270
|
+
const saveRemote = (name, value) => { try { api('stateSet', { name, value }).catch(() => {}); } catch {} };
|
|
271
|
+
useEffect(() => { const t = setTimeout(() => { if (reqLog.length) saveRemote('reqlog', reqLog); }, 900); return () => clearTimeout(t); }, [reqLog]);
|
|
272
|
+
useEffect(() => { const t = setTimeout(() => { if (Object.keys(selMap).length) saveRemote('selMap', selMap); }, 900); return () => clearTimeout(t); }, [selMap]);
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
(async () => {
|
|
275
|
+
try {
|
|
276
|
+
const st = await api('stateGet');
|
|
277
|
+
if (!st) return;
|
|
278
|
+
if (Array.isArray(st.reqlog) && st.reqlog.length) { reqLogRef.current = st.reqlog.slice(-50); setReqLog((cur) => (cur.length >= st.reqlog.length ? cur : reqLogRef.current)); }
|
|
279
|
+
if (st.selMap && typeof st.selMap === 'object') setSelMap((cur) => (Object.keys(cur).length ? cur : st.selMap));
|
|
280
|
+
} catch {}
|
|
281
|
+
})();
|
|
282
|
+
}, []);
|
|
283
|
+
const [consoleHeight, setConsoleHeight] = useState(Number(bootConsole.height) || 190);
|
|
284
|
+
const [events, setEvents] = useState([]);
|
|
285
|
+
const [sort, setSort] = useState(() => { const v = cxRead('console', {}); return v.sort && v.sort.key ? v.sort : { key: '', dir: 1 }; });
|
|
286
|
+
const [detail, setDetail] = useState(null);
|
|
287
|
+
const parseDebug = (m) => {
|
|
288
|
+
const text = String(m || '').trim().replace(/^↳\s*/, '');
|
|
289
|
+
const mm = /^(GET|POST|PUT|DELETE|PATCH)\s+(\S+)([\s\S]*)$/i.exec(text);
|
|
290
|
+
return mm ? { method: mm[1].toUpperCase(), url: mm[2], rest: mm[3], raw: text } : { method: '', url: text, rest: '', raw: text };
|
|
291
|
+
};
|
|
292
|
+
const [files, setFiles] = useState(null);
|
|
293
|
+
useEffect(() => cxWrite('console', { open: consoleOpen, tab: consoleTab, height: consoleHeight, sort }), [consoleOpen, consoleTab, consoleHeight, sort]);
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
const onToast = (e) => setToast({ id: Date.now(), text: String(e?.detail?.text || ''), kind: e?.detail?.kind || 'ok' });
|
|
296
|
+
window.addEventListener('cx-toast', onToast);
|
|
297
|
+
return () => window.removeEventListener('cx-toast', onToast);
|
|
298
|
+
}, []);
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
if (!toast) return;
|
|
301
|
+
const t = setTimeout(() => setToast(null), toast.kind === 'err' ? 4200 : 2600);
|
|
302
|
+
return () => clearTimeout(t);
|
|
303
|
+
}, [toast]);
|
|
304
|
+
const loadPref = (k, d) => cxRead(k, d);
|
|
305
|
+
const [nodeStatusFilter, setNodeStatusFilter] = useState(() => loadPref('filter-node', []));
|
|
306
|
+
const [spaceStatusFilter, setSpaceStatusFilter] = useState(() => loadPref('filter-space', []));
|
|
307
|
+
const [appSpaceFilter, setAppSpaceFilter] = useState(() => loadPref('filter-app', []));
|
|
308
|
+
const [storeSpaceFilter, setStoreSpaceFilter] = useState(() => loadPref('filter-store', []));
|
|
309
|
+
useEffect(() => cxWrite('filter-node', nodeStatusFilter), [nodeStatusFilter]);
|
|
310
|
+
useEffect(() => cxWrite('filter-space', spaceStatusFilter), [spaceStatusFilter]);
|
|
311
|
+
useEffect(() => cxWrite('filter-app', appSpaceFilter), [appSpaceFilter]);
|
|
312
|
+
useEffect(() => cxWrite('filter-store', storeSpaceFilter), [storeSpaceFilter]);
|
|
313
|
+
const t = useCallback((key) => term(locale, key), [locale]);
|
|
314
|
+
const tp = useCallback((key) => locale === 'zh' ? `${term('zh', key)} / ${term('en', key)}` : term('en', key), [locale]);
|
|
315
|
+
useEffect(() => {
|
|
316
|
+
const mq = window.matchMedia ? window.matchMedia('(prefers-color-scheme: light)') : null;
|
|
317
|
+
const apply = () => setTheme(themeMode === 'system' ? (mq && mq.matches ? 'light' : 'dark') : themeMode);
|
|
318
|
+
apply();
|
|
319
|
+
if (!mq) return;
|
|
320
|
+
mq.addEventListener?.('change', apply);
|
|
321
|
+
return () => mq.removeEventListener?.('change', apply);
|
|
322
|
+
}, [themeMode]);
|
|
323
|
+
useEffect(() => {
|
|
324
|
+
const apply = () => setLocale(langMode === 'system' ? browserLang() : langMode);
|
|
325
|
+
apply();
|
|
326
|
+
window.addEventListener?.('languagechange', apply);
|
|
327
|
+
return () => window.removeEventListener?.('languagechange', apply);
|
|
328
|
+
}, [langMode]);
|
|
329
|
+
useEffect(() => cxWriteText('theme', themeMode), [themeMode]);
|
|
330
|
+
const restoredRef = useRef(restored);
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
if (lightTheme) return; /* 浅色主题不播过场 */
|
|
333
|
+
if (restoredRef.current) { restoredRef.current = false; return; } /* 从快照恢复:直接进界面,不播过场 */
|
|
334
|
+
if (MODE_INTRO_SEEN.has(viewMode)) return;
|
|
335
|
+
MODE_INTRO_SEEN.add(viewMode);
|
|
336
|
+
setIntro(viewMode);
|
|
337
|
+
}, [viewMode, lightTheme]);
|
|
338
|
+
|
|
339
|
+
useEffect(() => cxWriteText('lang', langMode), [langMode]);
|
|
340
|
+
useEffect(() => cxWriteText('view', viewMode), [viewMode]);
|
|
341
|
+
useEffect(() => cxWriteText('chatfull', chatFull ? '1' : '0'), [chatFull]);
|
|
342
|
+
const shellRef = useRef(null);
|
|
343
|
+
useEffect(() => {
|
|
344
|
+
if (fx.sound === 'off') return;
|
|
345
|
+
let ac = null;
|
|
346
|
+
const ctx = () => { ac = ac || new (window.AudioContext || window.webkitAudioContext)(); return ac; };
|
|
347
|
+
const tone = (freq, dur, gain, type = 'sine', slide) => {
|
|
348
|
+
try {
|
|
349
|
+
const a = ctx();
|
|
350
|
+
const o = a.createOscillator(); const g = a.createGain();
|
|
351
|
+
o.type = type; o.frequency.value = freq;
|
|
352
|
+
if (slide) o.frequency.exponentialRampToValueAtTime(slide, a.currentTime + dur);
|
|
353
|
+
g.gain.value = gain; o.connect(g); g.connect(a.destination);
|
|
354
|
+
const t0 = a.currentTime; g.gain.setValueAtTime(gain, t0);
|
|
355
|
+
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
|
|
356
|
+
o.start(t0); o.stop(t0 + dur);
|
|
357
|
+
} catch {}
|
|
358
|
+
};
|
|
359
|
+
const noise = (dur, gain) => {
|
|
360
|
+
try {
|
|
361
|
+
const a = ctx();
|
|
362
|
+
const b = a.createBuffer(1, Math.max(1, Math.floor(a.sampleRate * dur)), a.sampleRate);
|
|
363
|
+
const d = b.getChannelData(0);
|
|
364
|
+
for (let i = 0; i < d.length; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / d.length);
|
|
365
|
+
const src = a.createBufferSource(); src.buffer = b;
|
|
366
|
+
const f = a.createBiquadFilter(); f.type = 'lowpass'; f.frequency.value = 2600;
|
|
367
|
+
const g = a.createGain(); g.gain.value = gain;
|
|
368
|
+
src.connect(f); f.connect(g); g.connect(a.destination); src.start();
|
|
369
|
+
} catch {}
|
|
370
|
+
};
|
|
371
|
+
const play = (kind) => {
|
|
372
|
+
if (kind === 'key') {
|
|
373
|
+
if (fx.sound === 'cs') { noise(0.015, 0.03); return; }
|
|
374
|
+
if (fx.sound === 'ra' || fx.sound === 'sc2') { tone(1200, 0.015, 0.02, 'square'); return; }
|
|
375
|
+
if (fx.sound === 'wow') { tone(620, 0.02, 0.018, 'triangle'); return; }
|
|
376
|
+
if (fx.sound === 'chip') { tone(880, 0.015, 0.02, 'square'); return; }
|
|
377
|
+
tone(980, 0.018, 0.018);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (fx.sound === 'cs') {
|
|
381
|
+
if (kind === 'click') { noise(0.07, 0.16); tone(150, 0.06, 0.05, 'square', 80); }
|
|
382
|
+
else noise(0.02, 0.05);
|
|
383
|
+
} else if (fx.sound === 'ra') {
|
|
384
|
+
if (kind === 'click') { tone(540, 0.06, 0.05, 'square'); setTimeout(() => tone(780, 0.07, 0.05, 'square'), 60); }
|
|
385
|
+
else tone(1400, 0.02, 0.02, 'square');
|
|
386
|
+
} else if (fx.sound === 'wow') {
|
|
387
|
+
if (kind === 'click') { tone(196, 0.16, 0.06, 'sawtooth', 147); setTimeout(() => tone(294, 0.18, 0.05, 'triangle'), 90); }
|
|
388
|
+
else tone(392, 0.04, 0.02, 'triangle');
|
|
389
|
+
} else if (fx.sound === 'sc2') {
|
|
390
|
+
if (kind === 'click') { tone(1568, 0.05, 0.04, 'square'); setTimeout(() => tone(2093, 0.05, 0.035, 'square'), 55); }
|
|
391
|
+
else tone(2093, 0.02, 0.02, 'square');
|
|
392
|
+
} else if (fx.sound === 'chip') {
|
|
393
|
+
if (kind === 'click') { [660, 880, 1320].forEach((f, i) => setTimeout(() => tone(f, 0.06, 0.04, 'square'), i * 55)); }
|
|
394
|
+
else tone(1760, 0.02, 0.02, 'square');
|
|
395
|
+
} else {
|
|
396
|
+
if (kind === 'click') tone(880, 0.07, 0.05);
|
|
397
|
+
else tone(1320, 0.03, 0.02);
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
const opSound = (k) => {
|
|
401
|
+
if (fx.sound === 'off') return;
|
|
402
|
+
const seq = { create: [[523, 0.06], [659, 0.06], [784, 0.09]], update: [[880, 0.05], [1174, 0.06]], delete: [[330, 0.07], [196, 0.1]], refresh: [[1400, 0.04], [1800, 0.03]] }[k] || [];
|
|
403
|
+
if (fx.sound === 'cs') {
|
|
404
|
+
if (k === 'create') { noise(0.05, 0.12); setTimeout(() => noise(0.05, 0.12), 80); }
|
|
405
|
+
else if (k === 'update') noise(0.05, 0.1);
|
|
406
|
+
else if (k === 'delete') tone(120, 0.12, 0.08, 'square', 70);
|
|
407
|
+
else noise(0.03, 0.06);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const type = fx.sound === 'ra' || fx.sound === 'chip' ? 'square' : fx.sound === 'wow' ? 'sawtooth' : 'sine';
|
|
411
|
+
seq.forEach(([f, d], i) => setTimeout(() => tone(f, d, 0.05, type), i * 70));
|
|
412
|
+
};
|
|
413
|
+
window.__cxOpSfx = opSound;
|
|
414
|
+
const onClick = () => play('click');
|
|
415
|
+
const onOver = (e) => { if (e.target.closest('.cx-btn, .cx-sel-btn, .cx-drop, .cx-compose-plus, .cx-compose-send')) play('hover'); };
|
|
416
|
+
const el = shellRef.current;
|
|
417
|
+
if (!el) return;
|
|
418
|
+
const onKey = (e) => {
|
|
419
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
420
|
+
if (e.key.length === 1 || e.key === 'Backspace' || e.key === 'Enter') play('key');
|
|
421
|
+
};
|
|
422
|
+
el.addEventListener('click', onClick, true);
|
|
423
|
+
el.addEventListener('pointerover', onOver, true);
|
|
424
|
+
el.addEventListener('keydown', onKey, true);
|
|
425
|
+
return () => { el.removeEventListener('click', onClick, true); el.removeEventListener('pointerover', onOver, true); el.removeEventListener('keydown', onKey, true); try { delete window.__cxOpSfx; } catch {} };
|
|
426
|
+
}, [fx.sound]);
|
|
427
|
+
// 切换管理模式/对话模式时,让当前选中的卡片闪一下
|
|
428
|
+
useEffect(() => {
|
|
429
|
+
const el = shellRef.current;
|
|
430
|
+
if (!el) return;
|
|
431
|
+
el.classList.remove('cx-view-flash');
|
|
432
|
+
void el.offsetWidth;
|
|
433
|
+
el.classList.add('cx-view-flash');
|
|
434
|
+
const t = setTimeout(() => { try { el.classList.remove('cx-view-flash'); } catch {} }, 1500);
|
|
435
|
+
return () => clearTimeout(t);
|
|
436
|
+
}, [viewMode]);
|
|
437
|
+
React.useLayoutEffect(() => {
|
|
438
|
+
const el = shellRef.current; if (!el) return;
|
|
439
|
+
const check = () => {
|
|
440
|
+
try {
|
|
441
|
+
const rect = el.getBoundingClientRect();
|
|
442
|
+
el.style.setProperty('--cx-shell-left', rect.left + 'px');
|
|
443
|
+
el.style.setProperty('--cx-shell-top', rect.top + 'px');
|
|
444
|
+
el.style.setProperty('--cx-shell-width', rect.width + 'px');
|
|
445
|
+
el.style.setProperty('--cx-shell-height', rect.height + 'px');
|
|
446
|
+
const left = rect.left;
|
|
447
|
+
const target = parseFloat(getComputedStyle(el).getPropertyValue('--cx-host-left')) || 168;
|
|
448
|
+
const pad = left < 120 ? Math.max(0, target - 16 - left) : 0;
|
|
449
|
+
el.style.setProperty('--cx-host-left-pad', pad + 'px');
|
|
450
|
+
el.classList.toggle('host-chrome', pad > 0);
|
|
451
|
+
} catch {}
|
|
452
|
+
};
|
|
453
|
+
check();
|
|
454
|
+
const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(check) : null;
|
|
455
|
+
if (ro) { ro.observe(el); ro.observe(document.documentElement); }
|
|
456
|
+
window.addEventListener('resize', check);
|
|
457
|
+
const t = setTimeout(check, 400);
|
|
458
|
+
return () => { if (ro) ro.disconnect(); window.removeEventListener('resize', check); clearTimeout(t); };
|
|
459
|
+
}, []);
|
|
460
|
+
const [initState, setInitState] = useState(null);
|
|
461
|
+
const [welcome, setWelcome] = useState(null);
|
|
462
|
+
const prevInit = useRef(null);
|
|
463
|
+
useEffect(() => {
|
|
464
|
+
const now = !!initState;
|
|
465
|
+
const was = !!prevInit.current;
|
|
466
|
+
prevInit.current = now;
|
|
467
|
+
if (now === was) return;
|
|
468
|
+
/* 浅色主题:进入初始化时不播,初始化完成后的 READY 保留 */
|
|
469
|
+
if (now && lightTheme) { setWelcome(null); return; }
|
|
470
|
+
setWelcome(now ? { title: 'INITIALIZE', sub: 'CONTEXTOS · SETUP' } : { title: 'READY', sub: 'CONTEXTOS · ONLINE' });
|
|
471
|
+
}, [initState]);
|
|
472
|
+
|
|
473
|
+
const [nodesLoaded, setNodesLoaded] = useState(() => cxRead('nodes', []).length > 0);
|
|
474
|
+
const autoInitRef = useRef(false);
|
|
475
|
+
const initBusyRef = useRef(false);
|
|
476
|
+
const [showDevices, setShowDevices] = useState(false);
|
|
477
|
+
const [avatarData, setAvatarData] = useState({ active: '', items: [] });
|
|
478
|
+
const [bgData, setBgData] = useState({ active: '', items: [] });
|
|
479
|
+
const [bg, setBg] = useState(null);
|
|
480
|
+
const [avatar, setAvatar] = useState(null);
|
|
481
|
+
// 任一状态变化都写回整份快照(节流 400ms,pagehide 兜底 flush)
|
|
482
|
+
useEffect(() => {
|
|
483
|
+
cxSessionSave({
|
|
484
|
+
node, space, app, view: viewMode, mode: workMode,
|
|
485
|
+
tabs: webTabs.map((t) => ({ base: t.base || t.url, title: t.title, app: t.app || null })),
|
|
486
|
+
active: activeWeb,
|
|
487
|
+
avatar: avatar?.uid || '', world: bg?.id || '',
|
|
488
|
+
chatFull, console: { open: consoleOpen, tab: consoleTab, height: consoleHeight },
|
|
489
|
+
});
|
|
490
|
+
}, [node, space, app, viewMode, workMode, webTabs, activeWeb, avatar, bg, chatFull, consoleOpen, consoleTab, consoleHeight]);
|
|
491
|
+
const [showAvatars, setShowAvatars] = useState(false);
|
|
492
|
+
const [busy, setBusy] = useState('');
|
|
493
|
+
const [error, setError] = useState('');
|
|
494
|
+
const initialLogs = [{ t: new Date().toLocaleTimeString(), m: 'CONTROL PLANE READY · 选择节点开始', k: 'ok' }];
|
|
495
|
+
const logsRef = useRef(initialLogs);
|
|
496
|
+
const [logs, setLogs] = useState(initialLogs);
|
|
497
|
+
const [debug, setDebug] = useState([]);
|
|
498
|
+
const logRef = useRef(null);
|
|
499
|
+
const log = useCallback((m, k = '') => {
|
|
500
|
+
if (ACTIVITY.fn !== log) { ACTIVITY.fn = log; const buf = ACTIVITY.buf.splice(0); buf.forEach((x) => log(x.m, x.k)); }
|
|
501
|
+
const entry = { t: new Date().toLocaleTimeString(), m, k, el: LOCATE.last || '' };
|
|
502
|
+
logsRef.current = [...logsRef.current.slice(-80), entry];
|
|
503
|
+
if (consoleOpenRef.current) setLogs(logsRef.current);
|
|
504
|
+
setTimeout(() => { if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight; }, 30);
|
|
505
|
+
}, []);
|
|
506
|
+
useEffect(() => {
|
|
507
|
+
const onDown = (e) => { LOCATE.last = locatable(e.target); };
|
|
508
|
+
document.addEventListener('pointerdown', onDown, true);
|
|
509
|
+
return () => document.removeEventListener('pointerdown', onDown, true);
|
|
510
|
+
}, []);
|
|
511
|
+
useEffect(() => {
|
|
512
|
+
if (!consoleOpen) return;
|
|
513
|
+
setLogs(logsRef.current);
|
|
514
|
+
setReqLog(reqLogRef.current);
|
|
515
|
+
}, [consoleOpen]);
|
|
516
|
+
useEffect(() => {
|
|
517
|
+
const buf = [];
|
|
518
|
+
let timer = 0;
|
|
519
|
+
const flush = () => {
|
|
520
|
+
timer = 0;
|
|
521
|
+
if (!buf.length) return;
|
|
522
|
+
const batch = buf.splice(0);
|
|
523
|
+
setDebug((ds) => [...ds.slice(-200), ...batch].slice(-200));
|
|
524
|
+
};
|
|
525
|
+
const onDebug = (e) => {
|
|
526
|
+
if (!consoleOpenRef.current && viewModeRef.current !== 'dev') return;
|
|
527
|
+
const text = String(e?.detail?.text || '');
|
|
528
|
+
if (!text) return;
|
|
529
|
+
buf.push({ t: new Date().toLocaleTimeString(), m: text, method: String(e?.detail?.method || ''), url: String(e?.detail?.url || ''), request: e?.detail?.request || null, requestHeaders: e?.detail?.requestHeaders || {}, response: e?.detail?.response || null, responseHeaders: e?.detail?.responseHeaders || {}, status: e?.detail?.status || 0, size: e?.detail?.size || 0, ms: e?.detail?.ms || 0, logId: String(e?.detail?.logId || ''), el: LOCATE.last || '' });
|
|
530
|
+
if (!timer) timer = setTimeout(flush, 80);
|
|
531
|
+
};
|
|
532
|
+
window.addEventListener('cx-debug', onDebug);
|
|
533
|
+
return () => { clearTimeout(timer); window.removeEventListener('cx-debug', onDebug); };
|
|
534
|
+
}, []);
|
|
535
|
+
useEffect(() => {
|
|
536
|
+
const buf = [];
|
|
537
|
+
let timer = 0;
|
|
538
|
+
const flush = () => {
|
|
539
|
+
timer = 0;
|
|
540
|
+
if (!buf.length) return;
|
|
541
|
+
const batch = buf.splice(0);
|
|
542
|
+
setEvents((xs) => [...xs.slice(-399), ...batch].slice(-400));
|
|
543
|
+
};
|
|
544
|
+
const push = (entry) => {
|
|
545
|
+
buf.push(entry);
|
|
546
|
+
if (!timer) timer = setTimeout(flush, 80);
|
|
547
|
+
};
|
|
548
|
+
const onEvent = (e) => {
|
|
549
|
+
if (!consoleOpenRef.current && viewModeRef.current !== 'dev') return;
|
|
550
|
+
const d = e?.detail || {};
|
|
551
|
+
const at = new Date().toISOString().slice(11, 23);
|
|
552
|
+
push({ id: `${at}-${Math.random().toString(36).slice(2, 6)}`, at, kind: d.kind || 'event', text: String(d.text || ''), ok: d.ok !== false, ms: d.ms, error: String(d.error || ''), el: LOCATE.last || '' });
|
|
553
|
+
};
|
|
554
|
+
const onClick = (e) => {
|
|
555
|
+
if (!consoleOpenRef.current && viewModeRef.current !== 'dev') return;
|
|
556
|
+
const btn = e.target && e.target.closest ? e.target.closest('button, .cx-menu-item, .cx-pm-card, .cx-chip, [role="button"]') : null;
|
|
557
|
+
if (!btn) return;
|
|
558
|
+
const label = String(btn.getAttribute('title') || btn.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 40);
|
|
559
|
+
const cls = String(btn.className || '').split(' ').slice(0, 2).join('.');
|
|
560
|
+
const at = new Date().toISOString().slice(11, 23);
|
|
561
|
+
push({ id: `${at}-${Math.random().toString(36).slice(2, 6)}`, at, kind: 'click', text: `${cls} · ${label}`, ok: true, el: locatable(btn) });
|
|
562
|
+
};
|
|
563
|
+
window.addEventListener('cx-event', onEvent);
|
|
564
|
+
document.addEventListener('click', onClick, true);
|
|
565
|
+
return () => { clearTimeout(timer); window.removeEventListener('cx-event', onEvent); document.removeEventListener('click', onClick, true); };
|
|
566
|
+
}, []);
|
|
567
|
+
useEffect(() => { setSort({ key: '', dir: 1 }); setDetail(null); }, [consoleTab]);
|
|
568
|
+
useEffect(() => { if (consoleTab !== 'files') return; api('files').then(setFiles).catch(() => setFiles({ error: 'files 接口不可用(重启 DSH 后生效)', files: [] })); }, [consoleTab]);
|
|
569
|
+
useEffect(() => {
|
|
570
|
+
const el = logRef.current;
|
|
571
|
+
if (!el) return;
|
|
572
|
+
setTimeout(() => { if (el) el.scrollTop = el.scrollHeight; }, 30);
|
|
573
|
+
}, [logs, debug, events, files, consoleTab]);
|
|
574
|
+
const stateRows = (() => {
|
|
575
|
+
if (consoleTab !== 'state') return [];
|
|
576
|
+
const liveRows = [
|
|
577
|
+
{ k: '', t: 'LIVE node', m: String(node ?? '') },
|
|
578
|
+
{ k: '', t: 'LIVE space', m: space || '' },
|
|
579
|
+
{ k: '', t: 'LIVE app', m: app?.name || '' },
|
|
580
|
+
{ k: '', t: 'LIVE avatar', m: avatar?.name || '' },
|
|
581
|
+
{ k: '', t: 'LIVE scene', m: bg?.name || '' },
|
|
582
|
+
{ k: '', t: 'LIVE view', m: viewMode },
|
|
583
|
+
{ k: '', t: 'LIVE locale', m: locale },
|
|
584
|
+
];
|
|
585
|
+
try {
|
|
586
|
+
return [...liveRows, ...cxStoreDump().map((x) => ({ k: 'ok', t: x.key, m: String(x.value || '').slice(0, 260) }))];
|
|
587
|
+
} catch { return liveRows; }
|
|
588
|
+
})();
|
|
589
|
+
const prettyJson = (v) => { try { return JSON.stringify(JSON.parse(v), null, 2); } catch { return String(v ?? ''); } };
|
|
590
|
+
const rowKey = (r) => `${r.t}|${r.req || r.text || r.m || ''}`;
|
|
591
|
+
const locate = useCallback((id) => {
|
|
592
|
+
if (!id) return;
|
|
593
|
+
const el = document.querySelector('[data-cx-id="' + String(id).replace(/"/g, '') + '"]');
|
|
594
|
+
if (!el) { log(`定位失败:控件 ${id} 不在当前视图`, 'err'); return; }
|
|
595
|
+
try { el.scrollIntoView({ block: 'center', behavior: 'smooth' }); } catch { el.scrollIntoView(); }
|
|
596
|
+
el.classList.remove('cx-locate-flash');
|
|
597
|
+
void el.offsetWidth;
|
|
598
|
+
el.classList.add('cx-locate-flash');
|
|
599
|
+
setTimeout(() => { try { el.classList.remove('cx-locate-flash'); } catch {} }, 1800);
|
|
600
|
+
log(`定位到控件 ${id}`, 'info');
|
|
601
|
+
}, [log]);
|
|
602
|
+
const openDetail = (r) => {
|
|
603
|
+
if (r && r.el) locate(r.el);
|
|
604
|
+
const key = rowKey(r);
|
|
605
|
+
if (consoleTab === 'request') {
|
|
606
|
+
const it = r.raw || {};
|
|
607
|
+
const u = it.usage || {};
|
|
608
|
+
const cost = (() => { const c = u.cost ?? u.total_cost ?? u.total_price; if (c !== undefined && c !== null) return String(c); const i = Number(u.input_tokens || u.prompt_tokens || 0); const o = Number(u.output_tokens || u.completion_tokens || 0); return (i || o) ? `≈¥${((i * 0.000002) + (o * 0.000008)).toFixed(6)}` : '—'; })();
|
|
609
|
+
setDetail({
|
|
610
|
+
key, row: r, title: '',
|
|
611
|
+
fields: [['TIME', new Date(it.at).toLocaleString()], ['MS', it.ms], ['BYTES', `↑${it.reqBytes || 0} ↓${it.resBytes || 0}`], ['TOKENS', it.tokens], ['COST', cost], ['MODE/VIEW', `${it.mode || '-'} / ${(it.context && it.context.view) || '-'}`]],
|
|
612
|
+
body: `REQUEST:\n${it.message || ''}\n\nPARAMS:\n${JSON.stringify(it.context || {}, null, 2)}\n\nRESPONSE:\n${it.reply || ''}${it.action ? `\n\nACTION:\n${JSON.stringify(it.action, null, 2)}` : ''}`,
|
|
613
|
+
});
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (consoleTab === 'activity') { setDetail({ key, row: r, title: 'ACTIVITY', fields: [['TIME', r.t], ['TYPE', r.k || '—'], ['MESSAGE', r.m]] }); return; }
|
|
617
|
+
if (consoleTab === 'events') { setDetail({ key, row: r, title: 'EVENT', fields: [['TIME', r.t], ['KIND', r.kind], ['RESULT', r.ok], ['MS', r.ms ?? '—']], body: r.text }); return; }
|
|
618
|
+
if (consoleTab === 'debug') { traceDebugRow(r); setDetail({ key, row: r, node, space, title: '', fields: [['TIME', r.t], ['LOG.ID', r.logId || '—'], ['STATUS', r.status || '—'], ['MS', `${r.ms || 0} ms`], ['BYTES', `${r.size || 0} B`], ['METHOD', r.method], ['CMD', debugCmdOf(r.req)], ['ACTION', debugActionOf(r.req)], ['URL', r.req], ...debugMetaFields(r)], requestHeaders: r.requestHeaders || {}, responseHeaders: r.responseHeaders || {}, body: r.response ? JSON.stringify(r.response, null, 2) : (typeof r.raw === 'string' ? r.raw : ''), table: debugResponseTable(r), params: debugParamsOf(r) }); return; }
|
|
619
|
+
if (consoleTab === 'state') { setDetail({ key, row: r, title: 'STATE', fields: [['KEY', r.t]], body: prettyJson(r.m) }); return; }
|
|
620
|
+
if (consoleTab === 'files') {
|
|
621
|
+
setDetail({ key, row: r, title: 'FILE', fields: [['NAME', r.t], ['SIZE', r.m], ['MODIFIED', r.x]], body: '加载中…' });
|
|
622
|
+
if (r.t && r.t !== 'DIR') {
|
|
623
|
+
api('file', { name: r.t }).then((d) => setDetail((cur) => (cur && cur.key === key ? { ...cur, body: d.content } : cur)))
|
|
624
|
+
.catch((e) => setDetail((cur) => (cur && cur.key === key ? { ...cur, body: String(e?.message || e) } : cur)));
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
const consoleTable = (() => {
|
|
629
|
+
if (consoleTab === 'activity') return { cols: [{ k: 't', label: 'TIME' }, { k: 'k', label: 'TYPE' }, { k: 'm', label: 'MESSAGE' }, { k: 'loc', label: '' }], rows: logs.map((l) => ({ ...l, loc: l.el ? '⌖' : '' })) };
|
|
630
|
+
if (consoleTab === 'request') return { cols: [{ k: 't', label: 'TIME' }, { k: 'ms', label: 'MS' }, { k: 'tokens', label: 'TOKENS' }, { k: 'msg', label: 'MESSAGE' }], rows: (reqLog || []).map((r) => ({ t: new Date(r.at).toLocaleTimeString(), ms: r.ms, tokens: r.tokens, msg: String(r.message || '').replace(/\s+/g, ' ').slice(0, 120), raw: r })) };
|
|
631
|
+
if (consoleTab === 'events') return { cols: [{ k: 't', label: 'TIME' }, { k: 'kind', label: 'KIND' }, { k: 'text', label: 'DETAIL' }, { k: 'ms', label: 'MS' }, { k: 'ok', label: 'RESULT' }], rows: events.map((e) => ({ t: e.at, kind: String(e.kind || '').toUpperCase(), text: `${e.text}${e.error ? ` · ${e.error}` : ''}`, ms: e.ms ?? '', ok: e.ok === false ? 'ERR' : 'OK', el: e.el, loc: e.el ? '⌖' : '' })) };
|
|
632
|
+
if (consoleTab === 'debug') return { cols: [{ k: 't', label: 'TIME' }, { k: 'status', label: 'STATUS' }, { k: 'ms', label: 'MS' }, { k: 'method', label: 'METHOD' }, { k: 'cmd', label: 'CMD' }, { k: 'action', label: 'ACTION' }, { k: 'req', label: 'URL' }, { k: 'loc', label: '' }], rows: debug.map((l) => { const p = parseDebug(l.m); const url = l.url || p.url; return { t: l.t, status: l.status || '', ms: l.ms || '', method: l.method || p.method || '—', cmd: debugCmdOf(url), action: debugActionOf(url), req: url, rest: p.rest, raw: l.response ? l : p.raw, response: l.response, requestHeaders: l.requestHeaders, responseHeaders: l.responseHeaders, logId: l.logId, size: l.size, request: l.request, el: l.el, loc: l.el ? '⌖' : '' }; }) };
|
|
633
|
+
if (consoleTab === 'state') return { cols: [{ k: 't', label: 'KEY' }, { k: 'm', label: 'VALUE' }], rows: stateRows };
|
|
634
|
+
if (consoleTab === 'files') {
|
|
635
|
+
if (files?.error) return { cols: [{ k: 't', label: 'ERROR' }], rows: [{ t: files.error }] };
|
|
636
|
+
const rows = (files?.files || []).map((f) => ({ t: f.name, m: f.size, x: f.mtime }));
|
|
637
|
+
if (files?.dir) rows.unshift({ t: 'DIR', m: '', x: files.dir });
|
|
638
|
+
return { cols: [{ k: 't', label: 'FILE' }, { k: 'm', label: 'SIZE' }, { k: 'x', label: 'MODIFIED' }], rows };
|
|
639
|
+
}
|
|
640
|
+
return { cols: [], rows: [] };
|
|
641
|
+
})();
|
|
642
|
+
const sortKey = sort.key && consoleTable.cols.some((c) => c.k === sort.key) ? sort.key : (consoleTable.cols[0]?.k || '');
|
|
643
|
+
const sortedRows = (() => {
|
|
644
|
+
const dir = sort.dir || 1;
|
|
645
|
+
return [...consoleTable.rows].sort((a, b) => {
|
|
646
|
+
const va = a[sortKey], vb = b[sortKey];
|
|
647
|
+
const na = Number(va), nb = Number(vb);
|
|
648
|
+
if (String(va ?? '').trim() !== '' && String(vb ?? '').trim() !== '' && !Number.isNaN(na) && !Number.isNaN(nb)) return (na - nb) * dir;
|
|
649
|
+
return String(va ?? '').localeCompare(String(vb ?? '')) * dir;
|
|
650
|
+
});
|
|
651
|
+
})();
|
|
652
|
+
const showToast = (text, kind = 'ok') => { try { window.dispatchEvent(new CustomEvent('cx-toast', { detail: { text, kind } })); } catch {} };
|
|
653
|
+
const act = useCallback(async (label, fn) => {
|
|
654
|
+
setBusy(label); setError('');
|
|
655
|
+
log(`→ ${label}`, 'run');
|
|
656
|
+
try { const r = await fn(); log(`✓ ${label}`, 'ok'); if (!/^LOAD /i.test(label)) { showToast(label, 'ok'); setDataPulse((x) => x + 1); } return r; }
|
|
657
|
+
catch (e) { const m = String(e?.message || e); setError(m); log(`✗ ${label}: ${m}`, 'err'); showToast(`${label}: ${m}`, 'err'); return undefined; }
|
|
658
|
+
finally { setBusy(''); }
|
|
659
|
+
}, [log]);
|
|
660
|
+
|
|
661
|
+
const loadNodes = useCallback(async () => {
|
|
662
|
+
const d = await api('nodes');
|
|
663
|
+
const list = d.nodes || [];
|
|
664
|
+
setNodes(list);
|
|
665
|
+
setNodesLoaded(true);
|
|
666
|
+
activity(`加载节点 ${list.length} 个(${list.filter((n) => n.state === 'running').length} 运行中)`, 'ok');
|
|
667
|
+
cxWrite('nodes', list);
|
|
668
|
+
try { api('stateSet', { name: 'nodesCache', value: list }).catch(() => {}); } catch {}
|
|
669
|
+
setNode((cur) => cur != null ? (list.some((n) => n.port === cur) ? cur : list[0]?.port ?? null) : (list.find((n) => n.state === 'running')?.port ?? list[0]?.port ?? null));
|
|
670
|
+
return list;
|
|
671
|
+
}, []);
|
|
672
|
+
const spacesSeqRef = useRef(0);
|
|
673
|
+
const loadSpaces = useCallback(async (port) => {
|
|
674
|
+
const seq = ++spacesSeqRef.current;
|
|
675
|
+
if (!port) { setSpaces([]); return []; }
|
|
676
|
+
try { const d = await api('spaces', { node: port }); if (seq === spacesSeqRef.current) setSpaces(d || []); activity(`加载空间 ${(d || []).length} 个(节点 ${port})`, 'ok'); return d || []; } catch { if (seq === spacesSeqRef.current) setSpaces([]); return []; }
|
|
677
|
+
}, []);
|
|
678
|
+
const appsSeqRef = useRef(0);
|
|
679
|
+
const appsCacheRef = useRef({ key: '', at: 0, data: null, promise: null });
|
|
680
|
+
const actorUid = avatar?.uid || '';
|
|
681
|
+
const actorRef = useRef('');
|
|
682
|
+
actorRef.current = actorUid;
|
|
683
|
+
const loadApps = useCallback(async (port, sp, uid = '') => {
|
|
684
|
+
const who = uid || actorRef.current;
|
|
685
|
+
if (!port || !who) { setApps([]); return []; }
|
|
686
|
+
uid = who;
|
|
687
|
+
const key = `${port}:${sp || ''}:${uid || ''}`;
|
|
688
|
+
const cache = appsCacheRef.current;
|
|
689
|
+
if (cache.key === key && cache.promise) return cache.promise;
|
|
690
|
+
if (cache.key === key && Date.now() - cache.at < 600 && cache.data) return cache.data;
|
|
691
|
+
const seq = ++appsSeqRef.current;
|
|
692
|
+
const promise = (async () => {
|
|
693
|
+
try { const d = await api('apps', { node: port, space: sp || '', uid }); if (seq === appsSeqRef.current) setApps(d || []); activity(`加载应用 ${(d || []).length} 个(${sp || '全部空间'})`, 'ok'); appsCacheRef.current = { key, at: Date.now(), data: d || [], promise: null }; return d || []; }
|
|
694
|
+
catch (e) {
|
|
695
|
+
if (/unknown op|unknown op apps/i.test(String(e?.message || e))) {
|
|
696
|
+
const d = await fallbackApps(port, sp); if (seq === appsSeqRef.current) setApps(d); appsCacheRef.current = { key, at: Date.now(), data: d, promise: null }; return d;
|
|
697
|
+
}
|
|
698
|
+
if (seq === appsSeqRef.current) setApps([]); appsCacheRef.current = { key, at: Date.now(), data: [], promise: null }; return [];
|
|
699
|
+
}
|
|
700
|
+
})();
|
|
701
|
+
appsCacheRef.current = { ...cache, key, at: cache.at, data: cache.data, promise };
|
|
702
|
+
return promise;
|
|
703
|
+
}, []);
|
|
704
|
+
const loadAvatars = useCallback(async (port) => {
|
|
705
|
+
if (!port) { setAvatarData({ active: '', items: [] }); setAvatar(null); return null; }
|
|
706
|
+
try {
|
|
707
|
+
const d = await api('avatars', { node: port });
|
|
708
|
+
setAvatarData(d || { active: '', items: [] });
|
|
709
|
+
activity(`加载数字分身 ${(d?.items || []).length} 个`, 'ok');
|
|
710
|
+
const want = (cxSessionGet() || {}).avatar || '';
|
|
711
|
+
const a = (d?.items || []).find((x) => x.uid === want) || (d?.items || []).find((x) => x.uid === d?.active) || d?.items?.[0] || null;
|
|
712
|
+
setAvatar(a);
|
|
713
|
+
return a;
|
|
714
|
+
} catch { setAvatarData({ active: '', items: [] }); setAvatar(null); return null; }
|
|
715
|
+
}, []);
|
|
716
|
+
const applyScene = useCallback((b) => {
|
|
717
|
+
const p = b?.params || {};
|
|
718
|
+
setThemeMode(p.theme || 'system');
|
|
719
|
+
setLangMode(p.lang || 'system');
|
|
720
|
+
}, []);
|
|
721
|
+
const loadBackgrounds = useCallback(async (port) => {
|
|
722
|
+
if (!port) { setBgData({ active: '', items: [] }); setBg(null); return null; }
|
|
723
|
+
try {
|
|
724
|
+
const d = await api('backgrounds', { node: port });
|
|
725
|
+
setBgData(d || { active: '', items: [] });
|
|
726
|
+
activity(`加载赛博场景 ${(d?.items || []).length} 个`, 'ok');
|
|
727
|
+
const wantBg = (cxSessionGet() || {}).world || '';
|
|
728
|
+
const b = (d?.items || []).find((x) => x.id === wantBg) || (d?.items || []).find((x) => x.id === d?.active) || d?.items?.[0] || null;
|
|
729
|
+
setBg(b);
|
|
730
|
+
applyScene(b);
|
|
731
|
+
return b;
|
|
732
|
+
} catch { setBgData({ active: '', items: [] }); setBg(null); return null; }
|
|
733
|
+
}, [applyScene]);
|
|
734
|
+
const openInit = useCallback(() => {
|
|
735
|
+
setInitState({ phase: 'setup', running: false, done: false, error: '', params: { ...INIT_DEFAULTS }, steps: INIT_STEPS.map((x) => ({ ...x, status: 'wait', detail: '' })), logs: [] });
|
|
736
|
+
}, []);
|
|
737
|
+
const runInit = useCallback(async (params = {}) => {
|
|
738
|
+
if (initBusyRef.current) return;
|
|
739
|
+
initBusyRef.current = true;
|
|
740
|
+
const cfg = { ...INIT_DEFAULTS, ...params, avatar: { ...(params.avatar || {}) }, scene: { ...(params.scene || {}) }, project: { ...(params.project || {}) } };
|
|
741
|
+
const steps = INIT_STEPS.map((x) => ({ ...x, status: 'wait', detail: '' }));
|
|
742
|
+
setInitState({ phase: 'running', running: true, done: false, error: '', params: cfg, steps, logs: [] });
|
|
743
|
+
const trace = (entry) => setInitState((prev) => {
|
|
744
|
+
if (!prev) return prev;
|
|
745
|
+
const logs = [...(prev.logs || [])];
|
|
746
|
+
const i = entry.id ? logs.findIndex((x) => x.id === entry.id) : -1;
|
|
747
|
+
if (i >= 0) logs[i] = { ...logs[i], ...entry };
|
|
748
|
+
else logs.push({ id: entry.id || `${Date.now()}-${logs.length}`, t: entry.t || new Date().toLocaleTimeString(), ms: entry.ms || 0, op: entry.op || '', impact: entry.impact || '', status: entry.status || 'run' });
|
|
749
|
+
return { ...prev, logs };
|
|
750
|
+
});
|
|
751
|
+
const brief = (v) => { try { const raw = JSON.stringify(v || {}); return raw.length > 360 ? raw.slice(0, 360) + '…' : raw; } catch { return ''; } };
|
|
752
|
+
const call = async (op, args) => {
|
|
753
|
+
const id = `api-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
754
|
+
const t = new Date().toLocaleTimeString();
|
|
755
|
+
trace({ id, t, op: `API ${op}`, impact: brief(args), status: 'run' });
|
|
756
|
+
const started = Date.now();
|
|
757
|
+
try { const r = await api(op, args); trace({ id, t, op: `API ${op}`, impact: brief(args), ms: Date.now() - started, status: 'ok' }); return r; }
|
|
758
|
+
catch (e) { trace({ id, t, op: `API ${op}`, impact: brief(args), ms: Date.now() - started, status: 'error' }); throw e; }
|
|
759
|
+
};
|
|
760
|
+
const mark = (id, status, detail = '') => { trace({ op: `${status.toUpperCase()} ${id}`, impact: detail, status }); setInitState((prev) => prev ? ({ ...prev, steps: prev.steps.map((x) => x.id === id ? { ...x, status, detail } : x) }) : prev); };
|
|
761
|
+
const finish = () => setInitState((prev) => prev ? ({ ...prev, phase: 'done', running: false, done: true, steps: prev.steps.map((x) => ({ ...x, status: x.status === 'wait' ? 'done' : x.status })) }) : prev);
|
|
762
|
+
let current = 'node';
|
|
763
|
+
try {
|
|
764
|
+
let nodePort = 0;
|
|
765
|
+
if (cfg.mode === 'remote') {
|
|
766
|
+
mark('node', 'run', cfg.remoteUrl || '');
|
|
767
|
+
const r = await call('remoteLogin', { url: cfg.remoteUrl, user: cfg.remoteUser, pass: cfg.remotePass });
|
|
768
|
+
mark('node', 'done', `已连接 ${cfg.remoteUrl} · sessid ${String(r.sessid).slice(0, 10)}…`);
|
|
769
|
+
['spaces', 'avatar', 'scene', 'project'].forEach((id) => mark(id, 'done', T('远程连接:跳过本地步骤')));
|
|
770
|
+
current = 'chat'; mark('chat', 'run');
|
|
771
|
+
cxWriteText('init', '1');
|
|
772
|
+
setViewMode('chat'); setChatFull(true);
|
|
773
|
+
mark('chat', 'done', T('远程会话已建立'));
|
|
774
|
+
finish();
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (cfg.mode === 'docker') {
|
|
778
|
+
mark('node', 'run', `docker ${cfg.image || ''} · :${cfg.port}`);
|
|
779
|
+
const r = await call('dockerDeploy', { port: Number(cfg.port) || 9188, dir: cfg.dir || '', image: cfg.image || '' });
|
|
780
|
+
nodePort = r.port;
|
|
781
|
+
} else if (cfg.mode === 'remoteDeploy') {
|
|
782
|
+
mark('node', 'run', `ssh ${cfg.sshUser || ''}@${cfg.host || ''} · :${cfg.port}`);
|
|
783
|
+
const r = await call('remoteDeploy', { host: cfg.host, user: cfg.sshUser, ssh_port: cfg.sshPort, port: Number(cfg.port) || 9188, dir: cfg.dir || '', image: cfg.image || '' });
|
|
784
|
+
nodePort = r.port;
|
|
785
|
+
} else {
|
|
786
|
+
const list = await loadNodes();
|
|
787
|
+
const found = list.find((n) => Number(n.port) === Number(cfg.port) && n.state === 'running');
|
|
788
|
+
nodePort = found?.port;
|
|
789
|
+
if (!nodePort) {
|
|
790
|
+
const existing = list.find((n) => Number(n.port) === Number(cfg.port));
|
|
791
|
+
mark('node', 'run', `port ${cfg.port} · ${cfg.dir}`);
|
|
792
|
+
if (existing) {
|
|
793
|
+
await call('start', { port: Number(cfg.port) });
|
|
794
|
+
nodePort = existing.port;
|
|
795
|
+
} else {
|
|
796
|
+
const mode = 'auto';
|
|
797
|
+
await call('deploy', { port: Number(cfg.port) || 9188, dir: cfg.dir, repo: cfg.repo, mode });
|
|
798
|
+
nodePort = Number(cfg.port) || 9188;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
const alive = (await loadNodes()).find((n) => Number(n.port) === Number(nodePort));
|
|
802
|
+
if (!alive || alive.state !== 'running') throw new Error(`节点 :${nodePort} 未就绪(${alive?.state || 'missing'}),请查看 ${cfg.dir || '节点目录'}/var/log/deploy.log`);
|
|
803
|
+
}
|
|
804
|
+
mark('node', 'done', `node :${nodePort}`);
|
|
805
|
+
setInitState((prev) => prev ? { ...prev, ctx: { ...(prev.ctx || {}), node: nodePort } } : prev);
|
|
806
|
+
|
|
807
|
+
current = 'spaces';
|
|
808
|
+
mark('spaces', 'run', T('云社区 · 云办公'));
|
|
809
|
+
const wanted = cfg.spaces?.length ? cfg.spaces : ['community', 'enterprise'];
|
|
810
|
+
let have = [];
|
|
811
|
+
try { have = await api('spaces', { node: nodePort }) || []; } catch {}
|
|
812
|
+
const hasKey = (k) => have.some((sp) => String(sp.name || '').includes(k));
|
|
813
|
+
const missing = wanted.filter((k) => !hasKey(k));
|
|
814
|
+
if (!missing.length) {
|
|
815
|
+
mark('spaces', 'done', T('已存在,跳过'));
|
|
816
|
+
} else {
|
|
817
|
+
await call('officialSpaces', { node: nodePort, keys: missing });
|
|
818
|
+
mark('spaces', 'done', T('云社区 + 云办公已就绪'));
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
current = 'avatar';
|
|
822
|
+
mark('avatar', 'run', cfg.avatar.name || '');
|
|
823
|
+
const a = await call('createAvatar', { node: nodePort, name: cfg.avatar.name || '我的数字分身', company: cfg.avatar.company || '云轩领航', city: cfg.avatar.city || '深圳市', persona: '专业、亲和、直接', preferences: '技术、产品、效率', style: '简洁、结构化' });
|
|
824
|
+
if (cfg.avatar.image) await call('updateAvatar', { node: nodePort, uid: a.uid, patch: { image: cfg.avatar.image } }).catch(() => {});
|
|
825
|
+
await call('setAvatar', { node: nodePort, uid: a.uid });
|
|
826
|
+
mark('avatar', 'done', `uid ${String(a.uid).slice(0, 10)}…${cfg.avatar.image ? ' · ' + T('已上传头像') : ''}`);
|
|
827
|
+
setInitState((prev) => prev ? { ...prev, ctx: { ...(prev.ctx || {}), avatar: a } } : prev);
|
|
828
|
+
|
|
829
|
+
current = 'scene';
|
|
830
|
+
mark('scene', 'run', cfg.scene.name || '');
|
|
831
|
+
try {
|
|
832
|
+
const b = await call('createBackground', { node: nodePort, name: cfg.scene.name || '我的赛博世界' });
|
|
833
|
+
const sceneId = b?.created?.id || b?.id || '';
|
|
834
|
+
if (sceneId && cfg.scene.image) await call('updateBackground', { node: nodePort, id: sceneId, patch: { image: cfg.scene.image } }).catch(() => {});
|
|
835
|
+
if (sceneId) await call('setBackground', { node: nodePort, id: sceneId });
|
|
836
|
+
mark('scene', 'done', `${cfg.scene.name || T('我的赛博世界')}${cfg.scene.image ? ' · ' + T('已上传背景') : ''}`);
|
|
837
|
+
} catch (e) {
|
|
838
|
+
mark('scene', 'done', T('跳过:背景服务未就绪'));
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
current = 'project';
|
|
842
|
+
mark('project', 'run', cfg.project.name || '');
|
|
843
|
+
let projectReady = false;
|
|
844
|
+
let projectError = null;
|
|
845
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
846
|
+
try { await call('ensureProject', { node: nodePort, uid: a.uid }); projectReady = true; break; }
|
|
847
|
+
catch (e) { projectError = e; await new Promise((resolve) => setTimeout(resolve, 800)); }
|
|
848
|
+
}
|
|
849
|
+
if (!projectReady) throw projectError || new Error('project instance is not ready');
|
|
850
|
+
const t = await call('pmCreate', { node: nodePort, uid: a.uid, kind: 'target', title: cfg.project.name || '我的第一个项目', content: cfg.project.goal || '' });
|
|
851
|
+
const p = await call('pmCreate', { node: nodePort, uid: a.uid, kind: 'plan', target_uid: t.uid, title: `${cfg.project.name || '我的第一个项目'} · 计划`, content: cfg.project.plan || '' });
|
|
852
|
+
mark('project', 'done', `1 ${T('目标')} · 1 ${T('计划')}`);
|
|
853
|
+
|
|
854
|
+
current = 'chat';
|
|
855
|
+
mark('chat', 'run');
|
|
856
|
+
setNode(nodePort);
|
|
857
|
+
await loadSpaces(nodePort);
|
|
858
|
+
const active = await loadAvatars(nodePort);
|
|
859
|
+
await loadBackgrounds(nodePort).catch(() => null);
|
|
860
|
+
await loadApps(nodePort, '', actorUid);
|
|
861
|
+
cxWriteText('init', '1');
|
|
862
|
+
setViewMode('chat');
|
|
863
|
+
setChatFull(true);
|
|
864
|
+
mark('chat', 'done', T('对话已就绪'));
|
|
865
|
+
finish();
|
|
866
|
+
} catch (e) {
|
|
867
|
+
const label = (INIT_STEPS.find((x) => x.id === current) || {}).label || current;
|
|
868
|
+
setInitState((prev) => prev ? { ...prev, phase: 'error', running: false, done: false, failedStep: current, error: `${label}失败:${String(e?.message || e)}`, steps: prev.steps.map((x) => x.id === current || x.status === 'run' ? { ...x, status: 'err' } : x) } : prev);
|
|
869
|
+
} finally {
|
|
870
|
+
initBusyRef.current = false;
|
|
871
|
+
}
|
|
872
|
+
}, [loadNodes, loadAvatars, loadBackgrounds, loadSpaces, loadApps]);
|
|
873
|
+
|
|
874
|
+
const retryInit = useCallback(async () => {
|
|
875
|
+
const st = initState;
|
|
876
|
+
if (!st?.params) return;
|
|
877
|
+
if (st.failedStep !== 'project' || !st.ctx?.node || !st.ctx?.avatar) return runInit(st.params);
|
|
878
|
+
const cfg = st.params;
|
|
879
|
+
const nodePort = st.ctx.node;
|
|
880
|
+
const a = st.ctx.avatar;
|
|
881
|
+
setInitState((prev) => prev ? { ...prev, phase: 'running', running: true, error: '', failedStep: '', logs: [...(prev.logs || []), { t: new Date().toLocaleTimeString(), op: 'RETRY project', impact: 'ensureProject + pmCreate', status: 'run' }] } : prev);
|
|
882
|
+
try {
|
|
883
|
+
await api('ensureProject', { node: nodePort, uid: a.uid });
|
|
884
|
+
const t = await api('pmCreate', { node: nodePort, uid: a.uid, kind: 'target', title: cfg.project?.name || '我的第一个项目', content: cfg.project?.goal || '' });
|
|
885
|
+
await api('pmCreate', { node: nodePort, uid: a.uid, kind: 'plan', target_uid: t.uid, title: `${cfg.project?.name || '我的第一个项目'} · 计划`, content: cfg.project?.plan || '' });
|
|
886
|
+
setInitState((prev) => prev ? { ...prev, phase: 'done', running: false, done: true, steps: prev.steps.map((x) => x.id === 'project' ? { ...x, status: 'done' } : x) } : prev);
|
|
887
|
+
} catch (e) {
|
|
888
|
+
setInitState((prev) => prev ? { ...prev, phase: 'error', running: false, error: `重试失败:${String(e?.message || e)}`, failedStep: 'project' } : prev);
|
|
889
|
+
}
|
|
890
|
+
}, [initState, runInit]);
|
|
891
|
+
const loadStore = useCallback(async (port) => {
|
|
892
|
+
if (!port) { setStore([]); return []; }
|
|
893
|
+
try { const d = await api('storeApps', { node: port, lang: locale }); setStore(d || []); return d || []; } catch { setStore([]); return []; }
|
|
894
|
+
}, [locale]);
|
|
895
|
+
|
|
896
|
+
useEffect(() => {
|
|
897
|
+
if (bootedRef.current) return;
|
|
898
|
+
bootedRef.current = true;
|
|
899
|
+
let alive = true;
|
|
900
|
+
(async () => {
|
|
901
|
+
// 节点信息一律现查 /c/runtime(缓存只用于首屏占位),保证 version/builtAt/sizeText 是最新的
|
|
902
|
+
if (alive) act('LOAD NODES', loadNodes);
|
|
903
|
+
})();
|
|
904
|
+
return () => { alive = false; };
|
|
905
|
+
}, [act, loadNodes]);
|
|
906
|
+
const autoSpaceRef = useRef({});
|
|
907
|
+
useEffect(() => {
|
|
908
|
+
if (!node || !spaces.length) return;
|
|
909
|
+
if (autoSpaceRef.current[node]) return;
|
|
910
|
+
autoSpaceRef.current[node] = true;
|
|
911
|
+
const saved = selMapRef.current[node] || {};
|
|
912
|
+
const pick = (saved.space && spaces.some((x) => x.name === saved.space)) ? saved.space : (spaces.find((x) => x.status === 'start')?.name || spaces[0].name);
|
|
913
|
+
setSpace(pick);
|
|
914
|
+
rememberSel(node, { space: pick });
|
|
915
|
+
}, [node, spaces]);
|
|
916
|
+
const autoAppRef = useRef({});
|
|
917
|
+
useEffect(() => {
|
|
918
|
+
if (!node || !space || !apps.length) return;
|
|
919
|
+
const k = `${node}:${space}`;
|
|
920
|
+
if (autoAppRef.current[k]) return;
|
|
921
|
+
autoAppRef.current[k] = true;
|
|
922
|
+
const saved = (selMapRef.current[node] || {}).appKey || '';
|
|
923
|
+
const pick = apps.find((a) => appKeyOf(a) === saved) || apps[0];
|
|
924
|
+
setApp(pick);
|
|
925
|
+
rememberSel(node, { appKey: appKeyOf(pick) });
|
|
926
|
+
}, [node, space, apps]);
|
|
927
|
+
const firstNodeEffectRef = useRef(true);
|
|
928
|
+
useEffect(() => {
|
|
929
|
+
const keep = firstNodeEffectRef.current && bootSelRef.current?.node;
|
|
930
|
+
firstNodeEffectRef.current = false;
|
|
931
|
+
if (!keep) { setSpace(null); setApps([]); setApp(null); }
|
|
932
|
+
setAppMode('recent');
|
|
933
|
+
if (!node) return;
|
|
934
|
+
act('LOAD CONTEXT', async () => {
|
|
935
|
+
// boot order: node → active avatar → spaces → data; nothing asks the
|
|
936
|
+
// node for data before the identity is known.
|
|
937
|
+
await loadAvatars(node);
|
|
938
|
+
await Promise.all([loadSpaces(node), loadBackgrounds(node)]);
|
|
939
|
+
});
|
|
940
|
+
}, [node, act, loadSpaces, loadApps, loadAvatars, loadBackgrounds]);
|
|
941
|
+
const selectSpace = (name) => {
|
|
942
|
+
if (!node) return;
|
|
943
|
+
if (space === name) { setSpace(null); setApp(null); rememberSel(node, { space: null, appKey: null }); return; }
|
|
944
|
+
setSpace(name); setApp(null);
|
|
945
|
+
rememberSel(node, { space: name, appKey: null });
|
|
946
|
+
};
|
|
947
|
+
const selectApp = (a) => {
|
|
948
|
+
const key = appKeyOf(a);
|
|
949
|
+
if (app && appKeyOf(app) === key) { setApp(null); rememberSel(node, { appKey: null }); return; }
|
|
950
|
+
setApp(a); rememberSel(node, { appKey: key });
|
|
951
|
+
};
|
|
952
|
+
const prevSelSpaceRef = useRef(space);
|
|
953
|
+
useEffect(() => { const changed = prevSelSpaceRef.current !== space; prevSelSpaceRef.current = space; if (changed) setApp(null); if (node && actorUid) act('LOAD RECENT', () => loadApps(node, space, actorUid)); }, [node, space, actorUid, act, loadApps]);
|
|
954
|
+
useEffect(() => {
|
|
955
|
+
if (!avatar?.project?.place_uid) return;
|
|
956
|
+
const tick = async () => {
|
|
957
|
+
if (document.hidden) return;
|
|
958
|
+
try {
|
|
959
|
+
const d = await api('projectStatus', { node, uid: avatar.uid });
|
|
960
|
+
const n = (v) => Number(String(v || '0').split('x')[0]) || 0;
|
|
961
|
+
const tasks = n(d.counts?.task), reports = n(d.counts?.report);
|
|
962
|
+
if (tasks > reports) log(`⏰ 项目提醒:${tasks - reports} 个任务还没有报告,去完成并提交报告`, 'run');
|
|
963
|
+
} catch { /* project status is best effort */ }
|
|
964
|
+
};
|
|
965
|
+
const timer = setInterval(tick, 60000);
|
|
966
|
+
return () => clearInterval(timer);
|
|
967
|
+
}, [avatar?.project?.place_uid, node, log]);
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
useEffect(() => {
|
|
971
|
+
if (!nodesLoaded || autoInitRef.current) return;
|
|
972
|
+
autoInitRef.current = true;
|
|
973
|
+
let done = false;
|
|
974
|
+
done = cxReadText('init', '') === '1';
|
|
975
|
+
if (!done) openInit();
|
|
976
|
+
}, [nodesLoaded, openInit]);
|
|
977
|
+
|
|
978
|
+
const refreshAll = () => act('REFRESH', async () => {
|
|
979
|
+
await loadNodes();
|
|
980
|
+
if (!node) return;
|
|
981
|
+
const [a] = await Promise.all([loadAvatars(node), loadSpaces(node)]);
|
|
982
|
+
await loadApps(node, space, actorUid);
|
|
983
|
+
if (appMode === 'store') await loadStore(node);
|
|
984
|
+
});
|
|
985
|
+
const openUrl = (url) => { try { window.open(url, '_blank', 'noopener'); } catch {} };
|
|
986
|
+
const withActor = (raw) => {
|
|
987
|
+
try {
|
|
988
|
+
const u = new URL(raw, window.location.origin);
|
|
989
|
+
if (!/^(127\.0\.0\.1|localhost)$/.test(u.hostname)) return raw;
|
|
990
|
+
if (u.searchParams.has('sessid')) return raw;
|
|
991
|
+
// 统一去掉项目空间路径末尾的 /(/s/<space>/ → /s/<space>)
|
|
992
|
+
if (/^\/s\/[^/]+\/$/.test(u.pathname)) u.pathname = u.pathname.slice(0, -1);
|
|
993
|
+
// user.uid 只对「应用级」命令有意义:/s/<pod>/c/web.team.<mod>.portal 及其子命令。
|
|
994
|
+
// 系统级命令(/c/dream、/c/xterm、/c/vimer、/s/<pod>/c/runtime …)不认它,带上只会污染日志。
|
|
995
|
+
const appLevel = /^\/s\/[^/]+\/c\/web\.team\.[^/]+\.[^/]+/.test(u.pathname);
|
|
996
|
+
let actor = avatar?.uid || '';
|
|
997
|
+
if (!actor) actor = cxReadText('actor', '');
|
|
998
|
+
if (appLevel && actor) u.searchParams.set('user.uid', actor);
|
|
999
|
+
return u.toString();
|
|
1000
|
+
} catch { return raw; }
|
|
1001
|
+
};
|
|
1002
|
+
const withView = (raw) => {
|
|
1003
|
+
try {
|
|
1004
|
+
const u = new URL(raw, window.location.origin);
|
|
1005
|
+
if (/^\/s\/[^/]+\/$/.test(u.pathname)) u.pathname = u.pathname.slice(0, -1);
|
|
1006
|
+
u.searchParams.set('theme', theme);
|
|
1007
|
+
u.searchParams.set('language', locale === 'zh' ? 'zh' : 'en');
|
|
1008
|
+
return u.toString();
|
|
1009
|
+
} catch { return raw; }
|
|
1010
|
+
};
|
|
1011
|
+
useEffect(() => { if (avatar?.uid) cxWriteText('actor', avatar.uid); }, [avatar?.uid]);
|
|
1012
|
+
const webTabsRef = useRef([]);
|
|
1013
|
+
webTabsRef.current = webTabs;
|
|
1014
|
+
const openWeb = (url, title, appMeta) => {
|
|
1015
|
+
if (!url) return '';
|
|
1016
|
+
setDebug((ds) => [...ds.slice(-200), { t: new Date().toLocaleTimeString(), m: `OPEN ${url}` }]);
|
|
1017
|
+
const base0 = withActor(url);
|
|
1018
|
+
const found = webTabsRef.current.find((t) => (t.base || t.url) === base0);
|
|
1019
|
+
if (found) { setActiveWeb(found.id); return found.id; }
|
|
1020
|
+
const id = `w${Date.now().toString(36)}${Math.random().toString(36).slice(2, 5)}`;
|
|
1021
|
+
setActiveWeb(id);
|
|
1022
|
+
setWebTabs((tabs) => [...tabs, { id, base: base0, url: withView(base0), title: title || url, reload: 0, app: appMeta || null }]);
|
|
1023
|
+
return id;
|
|
1024
|
+
};
|
|
1025
|
+
useEffect(() => {
|
|
1026
|
+
setWebTabs((tabs) => tabs.length ? tabs.map((t) => { const b = withActor(t.base || t.url); return { ...t, base: b, url: withView(b) }; }) : tabs);
|
|
1027
|
+
}, [theme, locale, avatar?.uid]);
|
|
1028
|
+
useEffect(() => {
|
|
1029
|
+
try {
|
|
1030
|
+
cxWrite('sel', {
|
|
1031
|
+
node, space,
|
|
1032
|
+
app: app ? { space: app.space || '', command: app.command || '', name: app.name || '', title: app.title || '', place_uid: app.place_uid || '' } : null,
|
|
1033
|
+
});
|
|
1034
|
+
} catch {}
|
|
1035
|
+
}, [node, space, app]);
|
|
1036
|
+
useEffect(() => { if (space && spaces.length && !spaces.some((x) => x.name === space)) setSpace(null); }, [spaces]);
|
|
1037
|
+
useEffect(() => {
|
|
1038
|
+
if (!app || !apps.length) return;
|
|
1039
|
+
const found = apps.find((a) => (app.place_uid && a.place_uid === app.place_uid) || (a.space === app.space && a.command === app.command && (a.name === app.name || a.title === app.title)));
|
|
1040
|
+
if (found && found !== app) setApp(found);
|
|
1041
|
+
}, [apps]);
|
|
1042
|
+
useEffect(() => {
|
|
1043
|
+
try {
|
|
1044
|
+
cxWrite('webtabs', {
|
|
1045
|
+
active: activeWeb || '',
|
|
1046
|
+
tabs: webTabs.map((t) => ({ id: t.id, base: t.base || t.url, title: t.title, app: t.app || null })),
|
|
1047
|
+
});
|
|
1048
|
+
} catch {}
|
|
1049
|
+
}, [webTabs, activeWeb]);
|
|
1050
|
+
const activeTab = webTabs.find((t) => t.id === activeWeb) || webTabs[webTabs.length - 1] || null;
|
|
1051
|
+
const [webAddr, setWebAddr] = useState('');
|
|
1052
|
+
const [webHist, setWebHist] = useState({});
|
|
1053
|
+
const [webMax, setWebMax] = useState(false);
|
|
1054
|
+
const webBoxRef = useRef(null);
|
|
1055
|
+
useEffect(() => { if (!webMax) return; return cxMaxApply(webBoxRef.current); }, [webMax]);
|
|
1056
|
+
useEffect(() => { setWebAddr(activeTab?.base || activeTab?.url || ''); }, [activeTab?.id, activeTab?.base]);
|
|
1057
|
+
useEffect(() => { if (!activeTab) return; const u = activeTab.base || activeTab.url; setWebHist((h) => (h[activeTab.id] ? h : { ...h, [activeTab.id]: { list: [u], idx: 0 } })); }, [activeTab?.id]);
|
|
1058
|
+
const setWebUrl = (id, url) => setWebTabs((tabs) => tabs.map((t) => { if (t.id !== id) return t; const b = withActor(url); return { ...t, base: b, url: withView(b), title: b, reload: (t.reload || 0) + 1 }; }));
|
|
1059
|
+
const goBack = () => { const cur = activeTab && webHist[activeTab.id]; if (!cur || cur.idx <= 0) return; const idx = cur.idx - 1; setWebHist((h) => ({ ...h, [activeTab.id]: { ...cur, idx } })); setWebUrl(activeTab.id, cur.list[idx]); };
|
|
1060
|
+
const goFwd = () => { const cur = activeTab && webHist[activeTab.id]; if (!cur || cur.idx >= cur.list.length - 1) return; const idx = cur.idx + 1; setWebHist((h) => ({ ...h, [activeTab.id]: { ...cur, idx } })); setWebUrl(activeTab.id, cur.list[idx]); };
|
|
1061
|
+
const tabIcon = (t) => {
|
|
1062
|
+
const raw = String(t?.app?.icon || t?.app?.place_avatar || '').trim();
|
|
1063
|
+
if (!raw) return h(UiIcon, { name: 'web', size: 12 });
|
|
1064
|
+
const src = /^https?:/i.test(raw) ? raw : (node ? `http://127.0.0.1:${node}${raw.startsWith('/') ? '' : '/'}${raw}` : raw);
|
|
1065
|
+
return h('img', { className: 'cx-webtab-ico', src, alt: '' });
|
|
1066
|
+
};
|
|
1067
|
+
const currentApp = activeTab ? (activeTab.app || null) : app;
|
|
1068
|
+
const firstTabAppRef = useRef(true);
|
|
1069
|
+
useEffect(() => {
|
|
1070
|
+
if (!activeTab) return;
|
|
1071
|
+
if (firstTabAppRef.current) { firstTabAppRef.current = false; if (!bootSelRef.current?.app) setApp(activeTab.app || null); return; }
|
|
1072
|
+
setApp(activeTab.app || null);
|
|
1073
|
+
}, [activeTab?.id]);
|
|
1074
|
+
const closeWeb = (id) => setWebTabs((tabs) => {
|
|
1075
|
+
const next = tabs.filter((t) => t.id !== id);
|
|
1076
|
+
if (activeWeb === id) { const last = next[next.length - 1] || null; setActiveWeb(last?.id || null); setApp(last?.app || null); }
|
|
1077
|
+
return next;
|
|
1078
|
+
});
|
|
1079
|
+
const closeOthers = (id) => {
|
|
1080
|
+
setWebTabs((tabs) => {
|
|
1081
|
+
const keep = tabs.find((t) => t.id === id);
|
|
1082
|
+
if (keep) { setActiveWeb(keep.id); setApp(keep.app || null); }
|
|
1083
|
+
return keep ? [keep] : [];
|
|
1084
|
+
});
|
|
1085
|
+
};
|
|
1086
|
+
const closeAll = () => { setWebTabs([]); setActiveWeb(null); setApp(null); };
|
|
1087
|
+
const reloadTab = (id) => setWebTabs((tabs) => tabs.map((t) => t.id === id ? { ...t, url: withView(t.base || t.url), reload: (t.reload || 0) + 1 } : t));
|
|
1088
|
+
const navWeb = (id, url) => {
|
|
1089
|
+
const raw = String(url || '').trim();
|
|
1090
|
+
if (!raw) return;
|
|
1091
|
+
const full0 = /^https?:\/\//i.test(raw) ? raw : `http://${raw}`;
|
|
1092
|
+
const full = withActor(full0);
|
|
1093
|
+
setWebHist((h) => { const cur = h[id] || { list: [], idx: -1 }; const list = cur.list.slice(0, cur.idx + 1); list.push(full); return { ...h, [id]: { list, idx: list.length - 1 } }; });
|
|
1094
|
+
setWebTabs((tabs) => tabs.map((t) => t.id === id ? { ...t, base: full, url: withView(full), title: full, reload: (t.reload || 0) + 1 } : t));
|
|
1095
|
+
setActiveWeb(id);
|
|
1096
|
+
};
|
|
1097
|
+
const manageWeb = (id) => { setActiveWeb(id); setViewMode('manage'); };
|
|
1098
|
+
const reloadWeb = () => activeWeb && reloadTab(activeWeb);
|
|
1099
|
+
const openExternal = () => { if (activeTab?.url) openUrl(activeTab.url); };
|
|
1100
|
+
const selectCreated = async (r) => {
|
|
1101
|
+
setAppMode('recent');
|
|
1102
|
+
const list = await loadApps(node, space, actorUid);
|
|
1103
|
+
const found = (r?.place_uid && list.find((a) => a.place_uid === r.place_uid))
|
|
1104
|
+
|| (r?.command ? { name: (r.command.split('.')[2] || r.name), title: r.name, command: r.command, space: r.space, place_uid: r.place_uid, city_name: r.city, company_name: r.company, service_name: r.service?.name || '', place: r.place, functions: [] } : null);
|
|
1105
|
+
if (found) setApp(found);
|
|
1106
|
+
return found;
|
|
1107
|
+
};
|
|
1108
|
+
const chooseAvatar = async (a) => {
|
|
1109
|
+
try {
|
|
1110
|
+
await api('setAvatar', { node, uid: a.uid });
|
|
1111
|
+
setAvatar(a);
|
|
1112
|
+
await loadApps(node, space, actorUid);
|
|
1113
|
+
} catch (e) { log(`切换分身失败:${String(e?.message || e)}`, 'err'); }
|
|
1114
|
+
};
|
|
1115
|
+
useEffect(() => {
|
|
1116
|
+
if (!bg) return;
|
|
1117
|
+
const p = bg.params || {};
|
|
1118
|
+
if ((p.theme || 'system') === themeMode && (p.lang || 'system') === langMode) return;
|
|
1119
|
+
const t = setTimeout(() => {
|
|
1120
|
+
api('updateBackground', { node, id: bg.id, patch: { params: { theme: themeMode, lang: langMode } } })
|
|
1121
|
+
.then((r) => { setBgData(r); setBg((r.items || []).find((x) => x.id === bg.id) || bg); })
|
|
1122
|
+
.catch(() => {});
|
|
1123
|
+
}, 500);
|
|
1124
|
+
return () => clearTimeout(t);
|
|
1125
|
+
}, [themeMode, langMode, bg?.id]);
|
|
1126
|
+
const chooseBg = async (b) => {
|
|
1127
|
+
try { await api('setBackground', { node, id: b.id }); setBg(b); applyScene(b); } catch (e) { log(`切换背景失败:${String(e?.message || e)}`, 'err'); }
|
|
1128
|
+
};
|
|
1129
|
+
const updateScene = async (payload) => {
|
|
1130
|
+
if (!bg) return;
|
|
1131
|
+
try {
|
|
1132
|
+
const { name, ...params } = payload || {};
|
|
1133
|
+
const merged = { ...(bg.params || {}), ...params };
|
|
1134
|
+
const patch = { params: merged };
|
|
1135
|
+
if (name) patch.name = name;
|
|
1136
|
+
const r = await api('updateBackground', { node, id: bg.id, patch });
|
|
1137
|
+
setBgData(r);
|
|
1138
|
+
const nb = (r.items || []).find((x) => x.id === bg.id) || bg;
|
|
1139
|
+
setBg(nb); applyScene(nb);
|
|
1140
|
+
} catch (e) { log(`保存场景失败:${String(e?.message || e)}`, 'err'); }
|
|
1141
|
+
};
|
|
1142
|
+
const createBg = async () => {
|
|
1143
|
+
try {
|
|
1144
|
+
const r = await api('createBackground', { node, name: '' });
|
|
1145
|
+
setBgData({ active: r.active, items: r.items });
|
|
1146
|
+
setBg(r.created || (r.items || []).find((x) => x.id === r.active) || null);
|
|
1147
|
+
} catch (e) { log(`新建背景失败:${String(e?.message || e)}`, 'err'); }
|
|
1148
|
+
};
|
|
1149
|
+
const readImage = (file, cb, max = 720, quality = 0.82) => {
|
|
1150
|
+
try {
|
|
1151
|
+
const img = new Image(); const url = URL.createObjectURL(file);
|
|
1152
|
+
img.onload = () => {
|
|
1153
|
+
const scale = Math.min(1, max / Math.max(img.width || 1, img.height || 1));
|
|
1154
|
+
const c = document.createElement('canvas');
|
|
1155
|
+
c.width = Math.max(1, Math.round((img.width || 1) * scale)); c.height = Math.max(1, Math.round((img.height || 1) * scale));
|
|
1156
|
+
c.getContext('2d').drawImage(img, 0, 0, c.width, c.height);
|
|
1157
|
+
URL.revokeObjectURL(url);
|
|
1158
|
+
cb(c.toDataURL('image/jpeg', quality));
|
|
1159
|
+
};
|
|
1160
|
+
img.onerror = () => { URL.revokeObjectURL(url); log('图片读取失败', 'err'); };
|
|
1161
|
+
img.src = url;
|
|
1162
|
+
} catch (e) { log(`图片读取失败:${String(e?.message || e)}`, 'err'); }
|
|
1163
|
+
};
|
|
1164
|
+
const upBgImage = (file) => {
|
|
1165
|
+
if (!file || !bg) return;
|
|
1166
|
+
readImage(file, async (data) => {
|
|
1167
|
+
try {
|
|
1168
|
+
const r = await api('updateBackground', { node, id: bg.id, patch: { image: data } });
|
|
1169
|
+
if (avatar?.uid) api('saveUserMedia', { node, uid: avatar.uid, kind: 'background', dataUrl: data, name: avatar.name || '' }).catch(() => {});
|
|
1170
|
+
setBgData(r); setBg((r.items || []).find((x) => x.id === bg.id) || { ...bg, image: data });
|
|
1171
|
+
} catch (e) { log(`上传背景失败:${String(e?.message || e)}`, 'err'); }
|
|
1172
|
+
});
|
|
1173
|
+
};
|
|
1174
|
+
const upAvatarImage = (file) => {
|
|
1175
|
+
if (!file || !avatar) return;
|
|
1176
|
+
readImage(file, async (data) => {
|
|
1177
|
+
try {
|
|
1178
|
+
await api('updateAvatar', { node, uid: avatar.uid, patch: { image: data } });
|
|
1179
|
+
api('saveUserMedia', { node, uid: avatar.uid, kind: 'avatar', dataUrl: data, name: avatar.name || '' }).catch(() => {});
|
|
1180
|
+
const d = await api('avatars', { node });
|
|
1181
|
+
setAvatarData(d);
|
|
1182
|
+
setAvatar((d.items || []).find((x) => x.uid === avatar.uid) || { ...avatar, image: data });
|
|
1183
|
+
} catch (e) { log(`上传头像失败:${String(e?.message || e)}`, 'err'); }
|
|
1184
|
+
});
|
|
1185
|
+
};
|
|
1186
|
+
const updateAvatarProfile = async (patch) => {
|
|
1187
|
+
if (!avatar) return;
|
|
1188
|
+
try {
|
|
1189
|
+
await api('updateAvatar', { node, uid: avatar.uid, patch });
|
|
1190
|
+
const d = await api('avatars', { node });
|
|
1191
|
+
setAvatarData(d);
|
|
1192
|
+
setAvatar((d.items || []).find((x) => x.uid === avatar.uid) || { ...avatar, ...patch });
|
|
1193
|
+
} catch (e) { log(`修改分身失败:${String(e?.message || e)}`, 'err'); }
|
|
1194
|
+
};
|
|
1195
|
+
const openNode = (port) => { setApp(null); openWeb(`http://127.0.0.1:${port}/`, `node :${port}`); };
|
|
1196
|
+
const [webFocus, setWebFocus] = useState(false);
|
|
1197
|
+
const openSpace = (sp) => { if (!sp) return ''; setApp(null); return openWeb(`http://${CX_HOST}:${node}/s/${encodeURIComponent(sp)}`, sp); };
|
|
1198
|
+
// 应用实例的界面地址:system_uid 走查询参数(框架用它定位实例),
|
|
1199
|
+
// 路径段留空,避免被节点当成数据接口返回 JSON。
|
|
1200
|
+
const appUrl = (a) => cxWebUrl({
|
|
1201
|
+
node, pod: a.space || space || '', command: a.command, place: a.place || 'system',
|
|
1202
|
+
placeUid: a.place_uid || a['system_uid'] || '', uid: avatar?.uid || a.user_uid || '',
|
|
1203
|
+
});
|
|
1204
|
+
const openApp = (a) => { if (!a) return ''; setApp(a); return openWeb(appUrl(a), a.title || a.name, a); };
|
|
1205
|
+
const buildApp = (a) => act(`BUILD ${a.name}`, async () => { await api('buildSpace', { node, space: a.space || space }); await loadApps(node, space, actorUid); });
|
|
1206
|
+
const initSpace = (sp) => act(`INIT ${sp}`, async () => { await api('initSpace', { node, space: sp }); log(`skeleton ready: ${sp}`); });
|
|
1207
|
+
const buildSpace = (sp) => act(`BUILD ${sp}`, async () => { await api('buildSpace', { node, space: sp }); await loadApps(node, sp, actorUid); });
|
|
1208
|
+
const startNode = (port) => act(`START :${port}`, async () => { await api('start', { port }); await loadNodes(); });
|
|
1209
|
+
const stopNode = (port) => act(`STOP :${port}`, async () => { await api('stop', { port }); await loadNodes(); });
|
|
1210
|
+
const deleteNode = (port) => {
|
|
1211
|
+
if (!window.confirm(`删除节点 :${port}?目录和台账都会删除。`)) return;
|
|
1212
|
+
act(`DELETE :${port}`, async () => { await api('deleteNode', { port }); await loadNodes(); });
|
|
1213
|
+
};
|
|
1214
|
+
const startSpace = (sp) => act(`START ${sp}`, async () => { await api('spaceStart', { node, space: sp }); await loadSpaces(node); });
|
|
1215
|
+
const stopSpace = (sp) => act(`STOP ${sp}`, async () => { await api('spaceStop', { node, space: sp }); await loadSpaces(node); });
|
|
1216
|
+
const deleteSpace = (sp) => {
|
|
1217
|
+
if (!window.confirm(`删除项目空间 ${sp}?目录和记录都会删除。`)) return;
|
|
1218
|
+
act(`DELETE ${sp}`, async () => { await api('deleteSpace', { node, space: sp }); await loadSpaces(node); await loadApps(node, '', actorUid); });
|
|
1219
|
+
};
|
|
1220
|
+
const deployOfficial = (keys) => act(`DEPLOY OFFICIAL ${keys.join(',')}`, async () => {
|
|
1221
|
+
const out = await api('officialSpaces', { node, keys });
|
|
1222
|
+
await loadSpaces(node); await loadApps(node, '', actorUid);
|
|
1223
|
+
log(`official: ${out.map((x) => x.space || x.key).join(', ')}`);
|
|
1224
|
+
return out;
|
|
1225
|
+
});
|
|
1226
|
+
const showStore = () => { setAppMode('store'); act('LOAD STORE', () => loadStore(node)); };
|
|
1227
|
+
const backRecent = () => { setAppMode('recent'); act('LOAD RECENT', () => loadApps(node, space, actorUid)); };
|
|
1228
|
+
const openStoreCreate = (service) => { setStoreService(service); setModal('storeInstance'); };
|
|
1229
|
+
const storeAct = (service, action) => act(`STORE ${action} ${service.name}`, async () => {
|
|
1230
|
+
await api('storeAction', { node, space: service.space || '20240724-community', service_uid: service.uid, action });
|
|
1231
|
+
await loadStore(node);
|
|
1232
|
+
});
|
|
1233
|
+
// 商店模板的「编程」:vimer 直接定位到该模块的 portal.go
|
|
1234
|
+
// 远程 / 容器节点:取 token 或 sessid(本机节点免登录,取到的只是本机票据)
|
|
1235
|
+
const nodeAuth = (port) => act('NODE AUTH ' + port, async () => {
|
|
1236
|
+
const r = await api('nodeAuth', { node: port });
|
|
1237
|
+
if (!r || r.ok === false) {
|
|
1238
|
+
window.dispatchEvent(new CustomEvent('cx-toast', { detail: { text: '取凭证失败:' + (r && r.error || '未知错误'), kind: 'err' } }));
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
const d = r.data || r;
|
|
1242
|
+
const who = d.sessid ? `sessid ${String(d.sessid).slice(0, 12)}…` : `token ${String(d.token).slice(0, 12)}…`;
|
|
1243
|
+
try { await navigator.clipboard.writeText(d.sessid || d.token || ''); window.dispatchEvent(new CustomEvent('cx-toast', { detail: { text: '已复制 ' + who } })); }
|
|
1244
|
+
catch { window.dispatchEvent(new CustomEvent('cx-toast', { detail: { text: '已获取 ' + who } })); }
|
|
1245
|
+
});
|
|
1246
|
+
const storeCode = (service) => {
|
|
1247
|
+
const mod = String(service.command || '').split('.')[2] || service.name;
|
|
1248
|
+
const url = `http://${CX_HOST}:${node}/s/${encodeURIComponent(service.space)}/c/vimer`
|
|
1249
|
+
+ `?file=${encodeURIComponent(`${mod}/portal.go`)}&path=${encodeURIComponent('src/')}&debug=true`
|
|
1250
|
+
+ `#src/:${mod}/portal.go:1`;
|
|
1251
|
+
openCardTool(url, service.title || service.name);
|
|
1252
|
+
};
|
|
1253
|
+
// 商店模板的「配置」:门户表视图
|
|
1254
|
+
const storeConfig = (service) => {
|
|
1255
|
+
const url = `http://${CX_HOST}:${node}/s/${encodeURIComponent(service.space)}/c/${service.command}?view=table&debug=true`;
|
|
1256
|
+
openCardTool(url, service.title || service.name);
|
|
1257
|
+
};
|
|
1258
|
+
|
|
1259
|
+
const startConsoleResize = (e) => {
|
|
1260
|
+
e.preventDefault();
|
|
1261
|
+
const startY = e.clientY, startH = consoleHeight;
|
|
1262
|
+
const move = (ev) => setConsoleHeight(Math.max(80, Math.min(460, startH + startY - ev.clientY)));
|
|
1263
|
+
const up = () => { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up); };
|
|
1264
|
+
window.addEventListener('pointermove', move);
|
|
1265
|
+
window.addEventListener('pointerup', up);
|
|
1266
|
+
};
|
|
1267
|
+
const nodeRows = nodes.filter((n) => !nodeStatusFilter.length || nodeStatusFilter.includes(n.state));
|
|
1268
|
+
const spaceRows = spaces.filter((s) => !spaceStatusFilter.length || spaceStatusFilter.includes(s.status));
|
|
1269
|
+
const appRows = apps.filter((a) => !appSpaceFilter.length || appSpaceFilter.includes(a.space || space || ''));
|
|
1270
|
+
const appSpaceOptions = [...new Set(apps.map((a) => a.space || space).filter(Boolean))].map((v) => ({ value: v, label: v }));
|
|
1271
|
+
const stateLabel = (v) => ({ running: t('running'), stopped: t('stopped'), offline: t('offline'), missing: t('missing'), starting: t('starting') })[v] || v;
|
|
1272
|
+
const nodeStatusOptions = [...new Set(nodes.map((n) => n.state).filter(Boolean))].map((v) => ({ value: v, label: stateLabel(v) }));
|
|
1273
|
+
const spaceStatusOptions = [...new Set(spaces.map((s) => s.status).filter(Boolean))].map((v) => ({ value: v, label: v === 'start' ? t('running') : v === 'stop' ? t('offline') : v }));
|
|
1274
|
+
const storeRows = store.filter((s) => !storeSpaceFilter.length || storeSpaceFilter.includes(s.space));
|
|
1275
|
+
const storeSpaceOptions = [...new Set(store.map((s) => s.space).filter(Boolean))].map((v) => ({ value: v, label: v }));
|
|
1276
|
+
const activeNode = nodes.find((n) => n.port === node);
|
|
1277
|
+
const activeSpace = spaces.find((s) => s.name === space);
|
|
1278
|
+
const running = nodes.filter((n) => n.state === 'running').length;
|
|
1279
|
+
const stage = activeTab ? 4 : app ? 3 : space ? 2 : node ? 1 : 0;
|
|
1280
|
+
|
|
1281
|
+
const [ideProjects, setIdeProjects] = useState(() => cxRead('ide', []));
|
|
1282
|
+
const [ideActive, setIdeActive] = useState(() => cxReadText('ide-active', ''));
|
|
1283
|
+
const [ideFile, setIdeFile] = useState(null);
|
|
1284
|
+
useEffect(() => cxWrite('ide', ideProjects), [ideProjects]);
|
|
1285
|
+
useEffect(() => cxWriteText('ide-active', ideActive), [ideActive]);
|
|
1286
|
+
useEffect(() => { const key = `${node}:${space}`; if (ideProjects.some((x) => x.key === key)) setIdeActive(key); }, [node, space]);
|
|
1287
|
+
const selectIdeProject = (key) => { const p = ideProjects.find((x) => x.key === key); if (!p) return; setIdeActive(key); setNode(p.node); setSpace(p.space); };
|
|
1288
|
+
const closeIdeProject = (key) => { setIdeProjects((ps) => ps.filter((x) => x.key !== key)); if (ideActive === key) setIdeActive(''); };
|
|
1289
|
+
// 节点/空间右键常用工具:一律开在聊天框自带的网页标签里
|
|
1290
|
+
const openCardTool = (url, title) => { const id = openWeb(url, title || url); if (id) setChatTab(id); return id; };
|
|
1291
|
+
// 首页由节点/空间自身的代码决定,插件只打开根地址,不写入任何配置
|
|
1292
|
+
// 某个工具页是否已经在网页标签里打开
|
|
1293
|
+
const isOpenUrl = (url) => (webTabs || []).some((t) => String(t.url || '') === String(url || ''));
|
|
1294
|
+
// 危险按键来自命令自省 meta._style(dream 的 stop/remove 等)
|
|
1295
|
+
const [actionStyle, setActionStyle] = useState({});
|
|
1296
|
+
useEffect(() => {
|
|
1297
|
+
if (!node) return;
|
|
1298
|
+
api('commandStyle', { node, command: 'dream' })
|
|
1299
|
+
.then((r) => setActionStyle((r && r.style) || {}))
|
|
1300
|
+
.catch(() => setActionStyle({}));
|
|
1301
|
+
}, [node]);
|
|
1302
|
+
const tone = (a) => actionStyle[a] || '';
|
|
1303
|
+
const openHome = (sp) => openCardTool(
|
|
1304
|
+
`http://127.0.0.1:${node}${sp ? `/s/${encodeURIComponent(sp)}` : ''}`,
|
|
1305
|
+
sp ? `${sp} · home` : 'home',
|
|
1306
|
+
);
|
|
1307
|
+
const openIde = async (port, sp) => {
|
|
1308
|
+
const n = Number(port);
|
|
1309
|
+
setNode(n);
|
|
1310
|
+
setViewMode('dev');
|
|
1311
|
+
let target = sp || '';
|
|
1312
|
+
if (sp === undefined) {
|
|
1313
|
+
try { target = ((await api('spaces', { node: n })) || [])[0]?.name || ''; } catch { /* keep */ }
|
|
1314
|
+
}
|
|
1315
|
+
setSpace(target || '');
|
|
1316
|
+
if (target) {
|
|
1317
|
+
const key = `${n}:${target}`;
|
|
1318
|
+
setIdeProjects((ps) => (ps.some((x) => x.key === key) ? ps : [...ps, { key, node: n, space: target }]));
|
|
1319
|
+
setIdeActive(key);
|
|
1320
|
+
}
|
|
1321
|
+
};
|
|
1322
|
+
const chatProps = { open: true, booted: nodesLoaded, full: true, theme, locale, themeMode, setThemeMode, langMode, setLangMode, view: viewMode, mode: workMode, onModeChange: setWorkMode, onRequest: pushReq, fx, onToggleFull: () => setChatFull((v) => !v), onInit: openInit, onClose: () => setViewMode('manage'), nodes, node, spaces, space, apps, app, onSelectNode: setNode, onSelectSpace: setSpace, onSelectApp: (a) => { setApp(a); openApp(a); }, ideFile, onSelectAvatar: chooseAvatar, avatars: avatarData.items, backgrounds: bgData.items, bg, onSelectBg: chooseBg, onCreateBg: createBg, onUploadBg: upBgImage, onUploadAvatar: upAvatarImage, onUpdateScene: updateScene, onUpdateAvatar: updateAvatarProfile, onCreateAvatar: () => setShowAvatars(true), webTabs, onOpenWebTab: openWeb, onCloseWebTab: closeWeb, onNavWebTab: navWeb, onReloadWebTab: reloadTab, onManageWebTab: manageWeb, onRefresh: () => refreshAll(), onOpenApp: openApp, onData: () => app && (setViewMode('chat'), setFormReq({ app, at: Date.now() })), formReq, onStore: showStore, onDevices: () => setShowDevices(true), avatar, cards: {
|
|
1323
|
+
selectNode: setNode, startNode, stopNode, deleteNode, openNode, openIde,
|
|
1324
|
+
openTerminal: (a) => api('openTerminal', { node: a?.node || node, space: a?.space || space }).then(() => true),
|
|
1325
|
+
rebuild: (a) => api('buildSpace', { node: a?.node || node, space: a?.space }).then((r) => { refreshAll(); return r; }),
|
|
1326
|
+
selectSpace, openSpace, startSpace, stopSpace, deleteSpace, selectApp, openApp, openCardTool, openHome, isOpenUrl, tone,
|
|
1327
|
+
}, onOpenDev: () => setViewMode('dev') };
|
|
1328
|
+
return h('div', { className: 'cx-shell', 'data-theme': theme, 'data-color': fx.color, 'data-light': fx.light, 'data-style': fx.style, 'data-view': viewMode, 'data-webfocus': webFocus ? '1' : '0', ref: shellRef },
|
|
1329
|
+
h('style', null, CRITICAL_CSS),
|
|
1330
|
+
h('div', { className: 'cx-water', 'aria-hidden': 'true' }, Array.from({ length: 3 }, (_, i) => h('i', { key: i, style: { left: ['18%', '62%', '84%'][i], top: ['72%', '26%', '58%'][i], animationDelay: `${i * 2.2}s` } }))),
|
|
1331
|
+
h(CursorComet),
|
|
1332
|
+
pipeMenu ? h('div', { className: 'cx-pipe-menu-fixed', style: pipeMenu.right != null ? { right: pipeMenu.right, top: pipeMenu.y } : { left: pipeMenu.x, top: pipeMenu.y }, onClick: (e) => e.stopPropagation() },
|
|
1333
|
+
pipeMenu.title ? h('div', { className: 'cx-pipe-menu-title' }, pipeMenu.title) : null,
|
|
1334
|
+
// 菜单开着的时候网页标签也可能变化(如「关闭所有网页」),这里实时计算打开状态
|
|
1335
|
+
h(MenuList, { items: (pipeMenu.items || []).map((it) => (it && it.url ? { ...it, open: isOpenUrl(it.url) } : it)), onPick: () => setPipeMenu(null) })) : null,
|
|
1336
|
+
h('div', { className: 'cx-cursor-ring', 'aria-hidden': 'true' }),
|
|
1337
|
+
opFx ? h('div', { key: opFx.key, className: `cx-op-fx cx-op-${opFx.kind}`, 'aria-hidden': 'true' }) : null,
|
|
1338
|
+
intro && !lightTheme ? h(ModeIntro, { key: intro, mode: intro, onDone: () => setIntro(null) }) : null,
|
|
1339
|
+
welcome && (welcome.title === 'READY' || !lightTheme) ? h(WelcomeBurst, { key: `w${welcome.title}${Date.now()}`, title: welcome.title, sub: welcome.sub, onDone: () => setWelcome(null) }) : null,
|
|
1340
|
+
h('header', { className: 'cx-header' },
|
|
1341
|
+
sidebarHidden ? h('button', { className: 'cx-btn cx-sb-toggle', title: '显示 / 隐藏 DSH 侧边栏', onClick: () => { try { CX_LAYOUT && CX_LAYOUT.toggleSidebar && CX_LAYOUT.toggleSidebar(); } catch {} } }, '☰') : null,
|
|
1342
|
+
h('div', { className: 'cx-brand' }, h('div', { className: 'cx-mark' }, h(CtxMark, { variant: 'devops', size: 22 })), h('div', null, h('div', { className: 'cx-title' }, 'CONTEXTOS · DEVOPS'), h('div', { className: 'cx-sub' }, `${t('cluster')} · ${t('modules')} · ${t('automation')} · v__CLIENT_CSS_VERSION__`))),
|
|
1343
|
+
h('div', { className: 'cx-views' },
|
|
1344
|
+
[['chat', '对话', 'CHAT'], ['manage', '管理', 'MANAGE'], ['dev', '开发', 'DEV'], ['monitor', '监控', 'MONITOR']].map(([k, zh, en]) =>
|
|
1345
|
+
h(UiAffordance, { key: k, as: 'button', icon: k, zh: locale === 'zh' ? zh : en, en: locale === 'zh' ? en : '', mode: 'short', className: `cx-view${viewMode === k ? ' on' : ''}`, onClick: () => setViewMode(k), title: zh + ' / ' + en }))),
|
|
1346
|
+
h('div', { className: 'cx-actions' },
|
|
1347
|
+
h(DropSelect, { value: fx.sound, title: '声效', onChange: (v) => fxSet('sound', v), options: [{ value: 'soft', label: T('柔和') }, { value: 'cs', label: T('CS 风') }, { value: 'ra', label: T('红警风') }, { value: 'wow', label: T('魔兽风') }, { value: 'sc2', label: T('星际风') }, { value: 'chip', label: T('8-bit') }, { value: 'off', label: T('静音') }] }),
|
|
1348
|
+
h(DropSelect, { value: fx.light, title: '动效', onChange: (v) => fxSet('light', v), options: [{ value: 'high', label: T('动效·强') }, { value: 'mid', label: T('动效·中') }, { value: 'low', label: T('动效·弱') }, { value: 'micro', label: T('动效·微') }, { value: 'off', label: T('动效·关') }] }),
|
|
1349
|
+
h(DropSelect, { value: fx.color, title: '色系', onChange: (v) => fxSet('color', v), options: [{ value: 'cyan', label: T('青') }, { value: 'green', label: T('绿') }, { value: 'violet', label: T('紫') }, { value: 'amber', label: T('琥珀') }, { value: 'rose', label: T('玫红') }, { value: 'ice', label: T('冰蓝') }, { value: 'warm', label: T('暖橙') }] }),
|
|
1350
|
+
h(DropSelect, { value: fx.style, title: '风格', onChange: (v) => fxSet('style', v), options: [{ value: 'tech', label: T('科技风') }, { value: 'classic', label: T('古典风') }, { value: 'desert', label: T('沙漠风') }, { value: 'rainforest', label: T('雨林风') }, { value: 'cyber', label: T('赛博风') }, { value: 'steam', label: T('蒸汽风') }, { value: 'minimal', label: T('极简风') }] }),
|
|
1351
|
+
h(DropSelect, { value: themeMode, title: t('light') + ' / ' + t('dark'), onChange: setThemeMode, options: [{ value: 'system', label: t('system') }, { value: 'light', label: t('light') }, { value: 'dark', label: t('dark') }] }),
|
|
1352
|
+
h(DropSelect, { value: langMode, title: t('chinese') + ' / English', onChange: setLangMode, options: [{ value: 'system', label: t('system') }, { value: 'zh', label: '中文' }, { value: 'en', label: 'English' }] }),
|
|
1353
|
+
h('button', { className: 'cx-btn', onClick: openInit, disabled: !!initState?.running, title: t('initialize') }, t('initialize')))),
|
|
1354
|
+
h('div', { className: 'cx-pipe' },
|
|
1355
|
+
h(UiAffordance, { as: 'span', icon: 'server', zh: t('serve'), mode: 'short', iconSize: 18, onClick: (e) => openPipe(e, t('serve'), (nodes || []).map((n) => ({ label: `:${n.port} · ${n.name || n.state}`, on: n.port === node, go: () => setNode(n.port) })), 'server'), className: `cx-stage cx-stage-title${stage >= 1 ? ' on' : ''}` }, h('span', { className: 'cx-aff-value' }, activeNode ? `:${activeNode.port}` : t('deploy'))),
|
|
1356
|
+
h('span', { className: 'cx-arrow' }, '›'),
|
|
1357
|
+
h(UiAffordance, { as: 'span', icon: 'space', zh: t('space'), mode: 'short', iconSize: 18, onClick: (e) => openPipe(e, t('space'), (spaces || []).map((sp) => ({ label: `${sp.name} · ${sp.status || ''}`, on: sp.name === space, go: () => setSpace(sp.name) })), 'space'), className: `cx-stage cx-stage-title${stage >= 2 ? ' on' : ''}` }, h('span', { className: 'cx-aff-value' }, space || t('create'))),
|
|
1358
|
+
h('span', { className: 'cx-arrow' }, '›'),
|
|
1359
|
+
h(UiAffordance, { as: 'span', icon: 'app', zh: t('recent'), mode: 'short', iconSize: 18, onClick: (e) => openPipe(e, t('recent'), (apps || []).map((a) => ({ label: a.title || a.name, on: app && app.place_uid === a.place_uid, go: () => openApp(a) })), 'app'), className: `cx-stage cx-stage-title${stage >= 3 ? ' on' : ''}` }, h('span', { className: 'cx-aff-value' }, app ? app.title || app.name : t('recent'))),
|
|
1360
|
+
h('span', { className: 'cx-arrow' }, '›'),
|
|
1361
|
+
h(UiAffordance, { as: 'span', icon: 'web', zh: t('web'), mode: 'short', iconSize: 18, onClick: (e) => openPipe(e, t('web'), (webTabs || []).map((tb) => ({ label: tb.title || tb.url, on: tb.id === activeTab?.id, go: () => navWeb(tb.id) })), 'web'), className: `cx-stage cx-stage-title${stage >= 4 ? ' on' : ''}` }, h('span', { className: 'cx-aff-value' }, activeTab ? (activeTab.title || 'page') : 'tabs')),
|
|
1362
|
+
h('div', { className: 'cx-chips' },
|
|
1363
|
+
h(UiAffordance, { as: 'button', icon: 'avatar', zh: t('avatar'), mode: 'short', onClick: (e) => openPipe(e, t('avatar'), (avatarData.items || []).map((a) => ({ label: a.name || a.uid, on: a.uid === avatar?.uid, go: () => chooseAvatar(a) })), 'avatar'), className: 'cx-chip', title: t('avatar') }, h('b', null, avatar?.name || '—')),
|
|
1364
|
+
h(UiAffordance, { as: 'button', icon: 'image', zh: t('scene'), mode: 'short', onClick: (e) => openPipe(e, t('scene'), (bgData.items || []).map((b) => ({ label: b.name, on: b.id === bg?.id, go: () => chooseBg(b) })), 'image'), className: 'cx-chip', title: `${t('scene')}: ${bg?.name || T('默认')}` }, h('b', null, bg?.name || T('默认'))),
|
|
1365
|
+
h(UiAffordance, { as: 'button', icon: 'factory', zh: t('mode'), mode: 'short', onClick: (e) => openPipe(e, t('mode'), CHAT_MODES.map((m) => ({ label: pipeModeLabel(m.id), on: m.id === workMode, go: () => setWorkMode(m.id) })), 'factory'), className: 'cx-chip', title: t('mode') }, h('b', null, pipeModeLabel(workMode))),
|
|
1366
|
+
h(UiAffordance, { as: 'button', icon: 'layers', zh: t('view'), mode: 'short', onClick: (e) => openPipe(e, t('view'), [['chat', '对话'], ['manage', '管理'], ['dev', '开发']].map(([k, zh]) => ({ label: pipeViewLabel(k), on: k === viewMode, go: () => setViewMode(k) })), 'layers'), className: 'cx-chip', title: t('view') }, h('b', null, pipeViewLabel(viewMode))))),
|
|
1367
|
+
error ? h('div', { className: 'cx-err' }, error) : null,
|
|
1368
|
+
h('div', { className: `cx-chat-host${chatFull ? ' full' : ''}`, style: viewMode === 'chat' ? null : { display: 'none' } }, h(ChatPanel, { ...chatProps, docked: !chatFull, onWebFocus: setWebFocus })),
|
|
1369
|
+
devMounted ? h('div', { className: 'cx-dev-host', style: viewMode === 'dev' ? null : { display: 'none' } }, h(DevWorkspace, { locale, node, space, bg, theme, active: viewMode === 'dev', nodes, spaces, projects: ideProjects, activeKey: ideActive, onSelectProject: selectIdeProject, onCloseProject: closeIdeProject, onSelectNode: (p) => setNode(p), onSelectSpace: (sp) => setSpace(sp), onActiveFile: setIdeFile, requests: reqLog, onPick: openIde })) : null,
|
|
1370
|
+
viewMode === 'monitor' ? h('div', { className: 'cx-monitor-host' },
|
|
1371
|
+
h(MonitorView, { nodes, spaces, apps, events, logs, reqLog, locale, onRefresh: () => loadNodes() })) : null,
|
|
1372
|
+
h('div', { className: `cx-body${busy ? ' busy' : ''}`, style: viewMode === 'manage' ? null : { display: 'none' } },
|
|
1373
|
+
h(Panel, {
|
|
1374
|
+
icon: 'server', title: tp('serve'), count: `${nodeRows.length}/${nodes.length} NODES`,
|
|
1375
|
+
tools: [h(MultiFilter, { key: 'f', label: t('status'), options: nodeStatusOptions, value: nodeStatusFilter, onChange: setNodeStatusFilter }), h('span', { key: 'sp', className: 'cx-tools-spacer' }), h(IconButton, { name: 'refresh', key: 'r', className: 'cx-btn xs cx-icon-btn cx-tool-refresh', onClick: () => act('REFRESH NODES', loadNodes), title: t('refresh') }), h(IconButton, { name: 'plus', key: 'd', className: 'cx-btn xs pri cx-icon-btn', onClick: () => setModal('deploy'), title: t('create') })],
|
|
1376
|
+
}, nodeRows.length ? nodeRows.map((n) => h(NodeCard, { key: n.dir || n.port, n, active: node === n.port, busy: !!busy, locale, onSelect: setNode, onStart: startNode, onStop: stopNode, onDelete: deleteNode, onOpen: openNode, onIde: openIde, onTool: openCardTool, onHome: () => openHome(''), onAuth: nodeAuth, isOpen: isOpenUrl, tone })) : h('div', { className: 'cx-empty' }, '还没有节点。先部署一个:copy / download / source。')),
|
|
1377
|
+
h(Panel, {
|
|
1378
|
+
icon: 'space', title: tp('space'), count: node ? `${spaceRows.length}/${spaces.length} SPACES` : t('serve'),
|
|
1379
|
+
tools: [h(MultiFilter, { key: 'f', label: t('status'), options: spaceStatusOptions, value: spaceStatusFilter, onChange: setSpaceStatusFilter }), h('span', { key: 'sp', className: 'cx-tools-spacer' }), h(IconButton, { name: 'refresh', key: 'r', className: 'cx-btn xs cx-icon-btn cx-tool-refresh', disabled: !node, onClick: () => act('REFRESH SPACES', () => loadSpaces(node)), title: t('refresh') }), h(IconButton, { name: 'plus', key: 'c', className: 'cx-btn xs pri cx-icon-btn', disabled: !node, onClick: () => setModal('space'), title: t('create') })],
|
|
1380
|
+
}, !node
|
|
1381
|
+
? h('div', { className: 'cx-empty' }, '先选择左侧服务节点。')
|
|
1382
|
+
: h(React.Fragment, null,
|
|
1383
|
+
spaceRows.length ? spaceRows.map((s) => h(SpaceCard, { key: s.name, s, node, active: space === s.name, busy: !!busy, onSelect: selectSpace, onOpen: openSpace, onStart: startSpace, onStop: stopSpace, onDelete: deleteSpace, onIde: openIde, onTool: openCardTool, onHome: () => openHome(s.name), isOpen: isOpenUrl, tone, locale })) : h('div', { className: 'cx-empty' }, '该节点还没有项目空间。创建空空间后自动 git init。'),
|
|
1384
|
+
h(OfficialSpaces, { node, onDeploy: deployOfficial, locale }))),
|
|
1385
|
+
h(Panel, {
|
|
1386
|
+
icon: 'app',
|
|
1387
|
+
title: appMode === 'store' ? t('store') : tp('recent'),
|
|
1388
|
+
count: appMode === 'store' ? `${store.length} SERVICES` : (node ? `${appRows.length}/${apps.length} APPS` : 'SELECT NODE'),
|
|
1389
|
+
tools: appMode === 'store'
|
|
1390
|
+
? [h(MultiFilter, { key: 'f', label: t('space'), options: storeSpaceOptions, value: storeSpaceFilter, onChange: setStoreSpaceFilter }), h('button', { key: 'b', className: 'cx-btn xs pri', onClick: backRecent }, '← ' + t('recent')), h('span', { key: 'sp', className: 'cx-tools-spacer' }), h(IconButton, { name: 'refresh', key: 'r', className: 'cx-btn xs cx-icon-btn cx-tool-refresh', disabled: !node, onClick: () => act('REFRESH STORE', () => loadStore(node)), title: t('refresh') }), h(IconButton, { name: 'plus', key: 't', className: 'cx-btn xs cx-icon-btn', disabled: !space, onClick: () => setModal('app'), title: t('create') + ' ' + t('template') })]
|
|
1391
|
+
: [h(MultiFilter, { key: 'f', label: t('space'), options: appSpaceOptions, value: appSpaceFilter, onChange: setAppSpaceFilter }), h('button', { key: 's', className: 'cx-btn xs pri', disabled: !node, onClick: showStore }, t('store')), h('span', { key: 'sp', className: 'cx-tools-spacer' }), h(IconButton, { name: 'refresh', key: 'r', className: 'cx-btn xs cx-icon-btn cx-tool-refresh', disabled: !node, onClick: () => act('REFRESH RECENT', () => loadApps(node, space, actorUid)), title: t('refresh') }), h(IconButton, { name: 'plus', key: 'c', className: 'cx-btn xs cx-icon-btn', disabled: !space, onClick: () => setModal('app'), title: t('create') + ' ' + t('template') })],
|
|
1392
|
+
},
|
|
1393
|
+
appMode === 'store'
|
|
1394
|
+
? (storeRows.length ? storeRows.map((s) => h(StoreRow, { key: `${s.space}:${s.command}:${s.uid}`, s, onCreate: openStoreCreate, busy: !!busy, onAction: storeAct, onCode: storeCode, onConfig: storeConfig, locale, node })) : h('div', { className: 'cx-empty' }, '应用商店加载中,或当前节点没有 service。'))
|
|
1395
|
+
: (!node ? h('div', { className: 'cx-empty' }, '先选择左侧服务节点。') : appRows.length ? appRows.map((a) => h(AppRow, { key: `${a.space}:${a.command}:${a.place_uid || a.name}`, a, active: app?.place_uid === a.place_uid, onSelect: selectApp, onOpen: openApp, locale, node })) : h('div', { className: 'cx-empty' }, '该用户最近还没有应用。用「应用商店」创建,或 AI 生成模板。'))),
|
|
1396
|
+
h(Panel, {
|
|
1397
|
+
icon: 'web', title: tp('web'), count: webTabs.length ? `${webTabs.length} TABS` : 'NO TAB',
|
|
1398
|
+
},
|
|
1399
|
+
h('div', { className: `cx-web${webMax ? ' max' : ''}`, ref: webBoxRef },
|
|
1400
|
+
h('div', { className: 'cx-webtabs' },
|
|
1401
|
+
webTabs.map((t) => h('div', { key: t.id, className: `cx-webtab${t.id === activeTab?.id ? ' on' : ''}`, onClick: () => { setActiveWeb(t.id); setApp(t.app || null); }, onContextMenu: (e) => { e.preventDefault(); e.stopPropagation(); setWebMenu({ x: e.clientX, y: e.clientY, id: t.id, tab: t }); } },
|
|
1402
|
+
tabIcon(t),
|
|
1403
|
+
h('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis' } }, t.title || t.url),
|
|
1404
|
+
h(IconButton, { name: 'close', className: 'x', onClick: (e) => { e.stopPropagation(); closeWeb(t.id); } }))),
|
|
1405
|
+
h(IconButton, { name: 'plus', className: 'cx-btn xs', disabled: !node, onClick: () => node && openNode(node) })),
|
|
1406
|
+
activeTab ? h('div', { className: 'cx-addrbar' },
|
|
1407
|
+
h(IconButton, { name: 'arrowLeft', className: 'nav', disabled: !(webHist[activeTab.id] && webHist[activeTab.id].idx > 0), onClick: goBack, title: T('后退') }),
|
|
1408
|
+
h(IconButton, { name: 'arrowRight', className: 'nav', disabled: !(webHist[activeTab.id] && webHist[activeTab.id].idx < webHist[activeTab.id].list.length - 1), onClick: goFwd, title: T('前进') }),
|
|
1409
|
+
h(IconButton, { name: 'refresh', className: 'nav', onClick: reloadWeb, title: T('刷新') }),
|
|
1410
|
+
h('input', { className: 'cx-addr-input', value: webAddr, placeholder: 'https://…', onChange: (e) => setWebAddr(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter') navWeb(activeTab.id, webAddr); } }),
|
|
1411
|
+
h(IconButton, { name: webMax ? 'restore' : 'fullscreen', className: 'nav', onClick: () => setWebMax((v) => !v), title: webMax ? T('恢复') : T('最大化') }),
|
|
1412
|
+
h(MenuButton, { items: [
|
|
1413
|
+
{ label: '复制地址', action: () => { try { navigator.clipboard.writeText(activeTab.base || activeTab.url); } catch {} } },
|
|
1414
|
+
{ label: '在浏览器打开', action: () => { try { window.open(activeTab.base || activeTab.url, '_blank'); } catch {} } },
|
|
1415
|
+
{ label: '刷新', action: reloadWeb },
|
|
1416
|
+
{ label: '关闭标签', danger: true, action: () => closeWeb(activeTab.id) },
|
|
1417
|
+
] })) : null,
|
|
1418
|
+
h('div', { className: 'cx-webframe' },
|
|
1419
|
+
!webTabs.length
|
|
1420
|
+
? h('div', { className: 'cx-empty' }, '点击最近应用 OPEN,或节点/空间 OPEN,在此多标签打开。')
|
|
1421
|
+
: viewMode !== 'manage'
|
|
1422
|
+
? h('div', { className: 'cx-empty' }, '切换到管理视图加载网页')
|
|
1423
|
+
: [webTabs.map((t) => h('iframe', {
|
|
1424
|
+
key: `${t.id}#${t.reload || 0}`, src: t.url, title: t.title || 'web', allow: 'clipboard-read; clipboard-write',
|
|
1425
|
+
className: t.id === activeTab?.id ? 'on' : '',
|
|
1426
|
+
onLoad: () => setWebLoaded((m) => m[t.id] ? m : { ...m, [t.id]: 1 }),
|
|
1427
|
+
})),
|
|
1428
|
+
activeTab && !webLoaded[activeTab.id] ? h('div', { key: 'loadmask', className: 'cx-webmask' }, '加载中…') : null])))),
|
|
1429
|
+
|
|
1430
|
+
consoleOpen ? h('div', { className: `cx-console${consoleMax ? ' max' : ''}`, ref: consoleBoxRef, style: consoleMax ? null : { height: consoleHeight } },
|
|
1431
|
+
h('div', { className: 'cx-console-h' },
|
|
1432
|
+
h('div', { className: 'cx-console-tabs' },
|
|
1433
|
+
h('button', { className: `cx-console-tab${consoleTab === 'events' ? ' on' : ''}`, onClick: () => setConsoleTab('events') }, 'EVENTS', h('span', null, events.length)),
|
|
1434
|
+
h('button', { className: `cx-console-tab${consoleTab === 'activity' ? ' on' : ''}`, onClick: () => setConsoleTab('activity') }, 'ACTIVITY', h('span', null, logs.length)),
|
|
1435
|
+
h('button', { className: `cx-console-tab${consoleTab === 'request' ? ' on' : ''}`, onClick: () => setConsoleTab('request') }, 'REQUEST', h('span', null, (reqLog || []).length)),
|
|
1436
|
+
h('button', { className: `cx-console-tab${consoleTab === 'debug' ? ' on' : ''}`, onClick: () => setConsoleTab('debug') }, 'DEBUG / URL', h('span', null, debug.length)),
|
|
1437
|
+
h('button', { className: `cx-console-tab${consoleTab === 'state' ? ' on' : ''}`, onClick: () => setConsoleTab('state') }, 'STATE', h('span', null, stateRows.length)),
|
|
1438
|
+
h('button', { className: `cx-console-tab${consoleTab === 'files' ? ' on' : ''}`, onClick: () => setConsoleTab('files') }, 'FILES', h('span', null, files?.files?.length ?? '')),
|
|
1439
|
+
h('button', { className: `cx-console-tab${consoleTab === 'terminal' ? ' on' : ''}`, onClick: () => { setDetail(null); setConsoleTab('terminal'); } }, 'TERMINAL')),
|
|
1440
|
+
h('span', { className: 'cx-console-status' }, busy ? `RUNNING: ${busy}` : 'IDLE'),
|
|
1441
|
+
h('div', { className: 'cx-console-actions' },
|
|
1442
|
+
consoleTab === 'terminal' ? h('div', { className: 'cx-term-acts' },
|
|
1443
|
+
h('button', { className: 'cx-term-act', onClick: () => consoleTermDo(node, space, 'refresh'), title: T('刷新') }, T('刷新')),
|
|
1444
|
+
h('button', { className: 'cx-term-act', onClick: () => consoleTermDo(node, space, 'clear'), title: T('清屏') }, T('清屏')),
|
|
1445
|
+
h('button', { className: 'cx-term-act', onClick: () => consoleTermDo(node, space, 'reconnect'), title: T('重连') }, T('重连')),
|
|
1446
|
+
h('button', { className: `cx-term-act${termSplit ? ' on' : ''}`, onClick: () => setTermSplit((v) => !v), title: T('分屏') }, T('分屏'))) : null,
|
|
1447
|
+
h('button', { className: 'cx-console-clear', onClick: () => { setLogs([]); logsRef.current = []; setReqLog([]); reqLogRef.current = []; setDebug([]); setEvents([]); setFiles(null); setDetail(null); }, title: '清空' }, '清空'),
|
|
1448
|
+
h(IconButton, { name: consoleMax ? 'restore' : 'fullscreen', onClick: () => setConsoleMax((v) => !v), title: consoleMax ? T('恢复') : T('最大化') }),
|
|
1449
|
+
h(IconButton, { name: 'close', onClick: () => setConsoleOpen(false), title: T('关闭') }))),
|
|
1450
|
+
h('div', { className: 'cx-console-body' },
|
|
1451
|
+
consoleTab === 'terminal' ? h('div', { className: `cx-console-terms${termSplit ? ' split' : ''}` },
|
|
1452
|
+
h(ConsoleTerminal, { node, space, slot: '' }),
|
|
1453
|
+
termSplit ? h(ConsoleTerminal, { node, space, slot: '-b' }) : null) : h('div', { className: 'cx-console-main', ref: logRef },
|
|
1454
|
+
h('table', { className: 'cx-console-table' },
|
|
1455
|
+
h('thead', null, h('tr', null, consoleTable.cols.map((c) => h('th', { key: c.k, onClick: (e) => { e.stopPropagation(); setSort((sp) => sp.key === c.k ? { key: c.k, dir: -sp.dir } : { key: c.k, dir: 1 }); } }, c.label, sortKey === c.k ? (sort.dir > 0 ? ' ▲' : ' ▼') : '')))),
|
|
1456
|
+
h('tbody', null, sortedRows.map((r, i) => h('tr', {
|
|
1457
|
+
key: i,
|
|
1458
|
+
className: `${String(r.ok) === 'ERR' ? 'err' : ''}${detail && detail.key === rowKey(r) ? ' on' : ''}`,
|
|
1459
|
+
onClick: () => openDetail(r),
|
|
1460
|
+
}, consoleTable.cols.map((c) => h('td', { key: c.k, className: c.k === 'loc' ? 'cx-loc' : '' }, String(r[c.k] ?? '')))))),
|
|
1461
|
+
),
|
|
1462
|
+
sortedRows.length === 0 ? h('div', { className: 'cx-line' }, h('span', { className: 'm' }, consoleTab === 'files' ? '没有数据文件(服务端重启后可用)' : '等待操作…')) : null),
|
|
1463
|
+
detail && consoleTab !== 'terminal' ? h(DebugDetail, { detail, onClose: () => setDetail(null) }) : null),
|
|
1464
|
+
h('div', { className: 'cx-console-resize', onPointerDown: startConsoleResize })) : h('button', { className: 'cx-console-open', onClick: () => setConsoleOpen(true) }, 'CONSOLE'),
|
|
1465
|
+
webMenu ? h('div', { className: 'cx-ctxmenu', style: { left: Math.min(webMenu.x, Math.max(0, (typeof window !== 'undefined' ? window.innerWidth : 1200) - 210)), top: Math.min(webMenu.y, Math.max(0, (typeof window !== 'undefined' ? window.innerHeight : 800) - 220)) }, onMouseDown: (e) => e.stopPropagation() },
|
|
1466
|
+
h('div', { className: 'cx-ctxmenu-t' }, webMenu.tab?.title || webMenu.tab?.url || 'WEB'),
|
|
1467
|
+
[['关闭', () => closeWeb(webMenu.id)], ['关闭其它标签', () => closeOthers(webMenu.id)], ['关闭全部标签', closeAll], ['刷新', () => reloadTab(webMenu.id)], ['外部打开', () => openUrl(webMenu.tab?.url || '')]].map(([label, action]) => h('button', { key: label, onClick: () => { setWebMenu(null); action(); } }, T(label))),
|
|
1468
|
+
webMenu.tab?.app ? h(React.Fragment, null,
|
|
1469
|
+
h('button', { onClick: () => { setWebMenu(null); setApp(webMenu.tab.app); setModal('code'); } }, T('编辑代码')),
|
|
1470
|
+
h('button', { onClick: () => { setWebMenu(null); setApp(webMenu.tab.app); setViewMode('chat'); setFormReq({ app: webMenu.tab.app, at: Date.now() }); } }, T('添加数据')),
|
|
1471
|
+
h('button', { onClick: () => { setWebMenu(null); setApp(webMenu.tab.app); setModal('instance'); } }, T('创建应用实例'))) : null) : null,
|
|
1472
|
+
modal === 'deploy' ? h(DeployModal, { onClose: () => setModal(null), onDone: () => { setModal(null); act('LOAD NODES', loadNodes); }, onScan: () => { setModal(null); setShowDevices(true); } }) : null,
|
|
1473
|
+
modal === 'space' ? h(SpaceModal, { node, onClose: () => setModal(null), onDone: (name) => { setModal(null); act('LOAD SPACES', async () => { await loadSpaces(node); setSpace(name); }); } }) : null,
|
|
1474
|
+
modal === 'app' ? h(AppModal, { node, space, onClose: () => setModal(null), onDone: (r) => { setModal(null); const synthetic = { name: r.name, title: r.help, command: r.command, space: r.space, path: r.path, place: r.place || '', functions: [], recent: false }; setApps((xs) => [synthetic, ...xs.filter((a) => !(a.name === synthetic.name && a.command === synthetic.command))]); setApp(synthetic); openApp(synthetic); act('LOAD RECENT', () => loadApps(node, space, actorUid)); } }) : null,
|
|
1475
|
+
modal === 'data' && currentApp ? h(DataModal, { node, space: currentApp.space || space, app: currentApp, onClose: () => setModal(null) }) : null,
|
|
1476
|
+
modal === 'code' && currentApp ? h(CodeModal, { node, space: currentApp.space || space, app: currentApp, onClose: () => setModal(null) }) : null,
|
|
1477
|
+
modal === 'instance' && currentApp ? h(InstanceModal, { node, space: currentApp.space || space, app: currentApp, avatar, onClose: () => setModal(null), onDone: (r) => { setModal(null); act('LOAD RECENT', () => selectCreated(r)); } }) : null,
|
|
1478
|
+
modal === 'storeInstance' && storeService ? h(StoreInstanceModal, { node, service: storeService, avatar, onClose: () => setModal(null), onDone: (r) => { setModal(null); setStoreService(null); act('LOAD RECENT', () => selectCreated(r)); } }) : null,
|
|
1479
|
+
showDevices ? h(DeviceModal, { node, onClose: () => setShowDevices(false) }) : null,
|
|
1480
|
+
showAvatars ? h(AvatarModal, { node, data: avatarData, active: avatar, onClose: () => setShowAvatars(false), onSelect: (a) => { setAvatar(a); setShowAvatars(false); }, onCreated: async (a) => { const d = await api('avatars', { node }); setAvatarData(d); const found = (d.items || []).find((x) => x.uid === a.uid) || a; setAvatar(found); setShowAvatars(false); }, onUpdated: async (a) => { const d = await api('avatars', { node }); setAvatarData(d); const found = (d.items || []).find((x) => x.uid === a.uid) || a; setAvatar(found); } }) : null,
|
|
1481
|
+
h(InitOverlay, { state: initState, onClose: () => setInitState(null), onStart: runInit, onRetry: retryInit, onLoginRemote: (cfg) => api('remoteLogin', cfg) }),
|
|
1482
|
+
dataPulse ? h('div', { key: dataPulse, className: 'cx-data-flash', 'aria-hidden': 'true' }) : null,
|
|
1483
|
+
toast ? h('div', { key: toast.id, className: `cx-toast ${toast.kind}`, role: 'status', 'aria-live': 'polite' }, h('span', { className: 'cx-toast-mark' }, toast.kind === 'err' ? '!' : '✓'), h('span', null, toast.text)) : null,
|
|
1484
|
+
busy ? h('div', { className: 'cx-busy-overlay', role: 'status', 'aria-live': 'polite' },
|
|
1485
|
+
h('div', { className: 'cx-busy-card' }, h('span', { className: 'cx-busy-spinner' }), h('div', null, h('b', null, busy), h('div', { className: 'cx-busy-sub' }, locale === 'zh' ? '正在等待接口返回…' : 'Waiting for API…')))) : null,
|
|
1486
|
+
viewMode !== 'chat' ? h('button', { className: 'cx-chat-fab', onClick: () => setViewMode('chat'), title: 'AI Copilot' }, 'AI') : null);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
class Boundary extends React.Component {
|
|
1490
|
+
constructor(props) { super(props); this.state = { err: null }; }
|
|
1491
|
+
static getDerivedStateFromError(e) { return { err: e }; }
|
|
1492
|
+
componentDidCatch(e) { reportError(String(e?.message || e)); }
|
|
1493
|
+
render() {
|
|
1494
|
+
if (!this.state.err) return this.props.children;
|
|
1495
|
+
return h('div', { className: 'cx-error-pane', role: 'alert' },
|
|
1496
|
+
h('div', { className: 'cx-error-title' }, T('界面出现错误')),
|
|
1497
|
+
h('div', { className: 'cx-error-msg' }, String(this.state.err?.message || this.state.err)),
|
|
1498
|
+
h('div', { className: 'cx-error-actions' },
|
|
1499
|
+
h('button', { className: 'cx-btn', onClick: () => this.setState({ err: null }) }, T('重试')),
|
|
1500
|
+
h('button', { className: 'cx-btn pri', onClick: () => location.reload() }, T('刷新页面')),
|
|
1501
|
+
h('button', { className: 'cx-btn', onClick: () => this.setState({ err: null }) }, T('返回'))));
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
return {
|
|
1506
|
+
inject: ['slots', 'layout'],
|
|
1507
|
+
apply(ctx) {
|
|
1508
|
+
try { CX_LAYOUT = ctx.layout || (ctx.get && ctx.get('layout')) || null; } catch {}
|
|
1509
|
+
ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({ name: 'sidebar.panellist', id: 'dsh-contextos-devops', order: 20, label: () => 'ContextOS DevOps' }, Icon));
|
|
1510
|
+
ctx.slots.inject('main', () => ctx.slots.register({ name: 'main', key: 'dsh-contextos-devops' }, () => h(Boundary, null, h(Page))));
|
|
1511
|
+
// 输入框内最左:ContextOS 芯片(切换服务节点 / 打开控制台)
|
|
1512
|
+
ctx.slots.inject('conversation.input.left', () => ctx.slots.register({ name: 'conversation.input.left', id: 'dsh-contextos-devops', order: 0 }, (props) => h(CtxChip, { sessionId: (props && props.sessionId) || '' })));
|
|
1513
|
+
// 会话标题栏右侧:ContextOS 按钮(打开控制台)
|
|
1514
|
+
ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({ name: 'conversation.session.header.utilities', id: 'dsh-contextos-devops-center', order: 10 }, (props) => h(CtxHeaderButton, { sessionId: (props && props.sessionId) || '' })));
|
|
1515
|
+
},
|
|
1516
|
+
};
|
|
1517
|
+
},
|
|
1518
|
+
});
|