/** * Workspace status detector (feature ②): reads a directory under the session * workspace via BSB's fs.tree route and infers the Abaqus run state from the * files present — no backend change. * * Inference: * *.lck → 求解中(Abaqus job lock) * *.odb (无 .lck) → 有结果可后处理 * *.cae → 已建模型 * *.jnl / *.rpy → 建模脚本 * *.sta / .msg / .dat → 求解过程产物 * * Constraint (host): only paths inside the session workspace are readable. * Default target = the session cwd; the user can point it at the Abaqus * workdir when that dir lives inside the workspace. Choice persists per session. */ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { ensureCaeStyles } from './theme.js' import { fsTree, caeTelemetry, type FsEntry, type SessionScope, type CaeTelemetry } from './sidebarApi.js' import { IconRefresh, IconFolder } from './icons.js' interface Detected { cae: string[] odb: string[] lck: string[] sta: string[] msg: string[] dat: string[] script: string[] } function detect(entries: FsEntry[]): Detected { const d: Detected = { cae: [], odb: [], lck: [], sta: [], msg: [], dat: [], script: [] } for (const e of entries) { if (e.isDir) continue const n = e.name.toLowerCase() if (n.endsWith('.cae')) d.cae.push(e.name) else if (n.endsWith('.odb')) d.odb.push(e.name) else if (n.endsWith('.lck')) d.lck.push(e.name) else if (n.endsWith('.sta')) d.sta.push(e.name) else if (n.endsWith('.msg')) d.msg.push(e.name) else if (n.endsWith('.dat')) d.dat.push(e.name) else if (n.endsWith('.jnl') || n.endsWith('.rpy')) d.script.push(e.name) } return d } type Phase = 'running' | 'results' | 'modeled' | 'idle' function phaseOf(d: Detected): Phase { if (d.lck.length > 0) return 'running' if (d.odb.length > 0) return 'results' if (d.cae.length > 0) return 'modeled' return 'idle' } const PHASE_META: Record = { running: { label: '求解中', color: 'var(--cae-run)', soft: 'var(--cae-run-soft)' }, results: { label: '有结果', color: 'var(--cae-ok)', soft: 'var(--cae-ok-soft)' }, modeled: { label: '已建模', color: 'var(--cae-accent)', soft: 'var(--cae-accent-soft)' }, idle: { label: '未检测到产物', color: 'var(--cae-faint)', soft: 'var(--cae-inset)' }, } const POLL_MS = 4000 export function WorkspaceStatus({ scope, visible }: { scope: SessionScope; visible: boolean }) { ensureCaeStyles() const storageKey = `cae:workdir:${scope.sessionId}` const [target, setTarget] = useState(() => { try { return localStorage.getItem(storageKey) ?? '' } catch { return '' } }) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [resolved, setResolved] = useState(null) const [det, setDet] = useState(null) const [lastAt, setLastAt] = useState(null) const seq = useRef(0) // ── bridge telemetry (authoritative Abaqus workdir + live session) ─────── const [tele, setTele] = useState(null) const [teleLoading, setTeleLoading] = useState(false) const teleSeq = useRef(0) const refreshTele = useCallback(async (signal?: AbortSignal) => { const my = ++teleSeq.current setTeleLoading(true) try { const t = await caeTelemetry(signal) if (my !== teleSeq.current) return setTele(t) } catch { if (my !== teleSeq.current) return setTele(null) } finally { if (my === teleSeq.current) setTeleLoading(false) } }, []) const refresh = useCallback( async (signal?: AbortSignal) => { const my = ++seq.current setLoading(true) setError(null) try { const res = await fsTree(scope, target, signal) if (my !== seq.current) return setDet(detect(res.entries)) setResolved(res.path) setLastAt(Date.now()) } catch (e) { if (my !== seq.current) return setDet(null) setError(e instanceof Error ? e.message : String(e)) } finally { if (my === seq.current) setLoading(false) } }, [scope, target], ) // Poll while the tab is visible; stop when hidden. useEffect(() => { if (!visible) return const ctrl = new AbortController() void refresh(ctrl.signal) const t = setInterval(() => void refresh(ctrl.signal), POLL_MS) return () => { ctrl.abort() clearInterval(t) } }, [visible, refresh]) // Bridge telemetry: slower cadence (bridge ping is a real kernel round-trip). useEffect(() => { if (!visible) return const ctrl = new AbortController() void refreshTele(ctrl.signal) const t = setInterval(() => void refreshTele(ctrl.signal), 5000) return () => { ctrl.abort() clearInterval(t) } }, [visible, refreshTele]) const persist = (v: string) => { setTarget(v) try { localStorage.setItem(storageKey, v) } catch { /* ignore */ } } const phase: Phase | null = det ? phaseOf(det) : null const meta = phase ? PHASE_META[phase] : null const showList = useMemo(() => { if (!det) return [] const rows: { name: string; tag: string }[] = [] for (const n of det.cae) rows.push({ name: n, tag: '模型' }) for (const n of det.odb) rows.push({ name: n, tag: '结果' }) for (const n of det.sta) rows.push({ name: n, tag: '状态' }) for (const n of det.script) rows.push({ name: n, tag: '脚本' }) return rows.slice(0, 12) }, [det]) return (
工作目录侦测
{meta && ( {meta.label} )}
{/* bridge telemetry — the real Abaqus workdir, straight from the bridge */}
{teleLoading && tele === null ? ( 桥接状态检测中… ) : tele?.connected ? ( <> Abaqus 桥接已连接 {tele.cwd ?? ''} {tele.models && tele.models.length > 0 && ( · {tele.models.length} 模型 )} {tele && tele.cwd && tele.cwd !== '' && tele.cwd !== target && ( )} ) : ( <> Abaqus 桥接离线 未连到 CAE 内核,正在按文件推断 )}
persist(e.target.value)} placeholder="留空 = 会话工作目录;或填 workspace 内的 Abaqus workdir 子路径" />
{error ? (
读取失败:{error}(路径需在 session workspace 内)
) : det === null ? (
{visible ? '读取中…' : '(标签页未激活,暂停侦测)'}
) : ( <>
{resolved ?? ''} {lastAt !== null && ` · ${new Date(lastAt).toLocaleTimeString()}`}
0 ? 6 : 0 }}> {det.cae.length} 模型 {det.odb.length} 结果 {det.lck.length} 进行中 {det.script.length} 脚本
{showList.length > 0 && (
{showList.map((r) => (
[{r.tag}] {r.name}
))}
)} )}
) }