Skip to content

Commit 46e7c88

Browse files
feat(mod-manager): revamp layout and add GitHub-style markdown panels
- Auto-fit mod manager split layout on first load and surface fetch failures - Add README/CHANGELOG GitHub-style viewer with markdown preprocessing for upstream formatting quirks - Refactor pipeline trace UX with selectable rows, impact detail panel, and jump-to-pipeline navigation - Cache pipeline snapshot/prompt rendering on backend with TTL + inflight dedupe
1 parent 8d6431e commit 46e7c88

27 files changed

Lines changed: 2503 additions & 744 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ yarn.lock
4545
astrbot.lock
4646

4747
# Other
48+
playwright-screenshots/
4849
chroma
4950
venv/*
5051
pytest.ini

astrbot/dashboard/routes/pipeline.py

Lines changed: 106 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
from __future__ import annotations
22

3+
import asyncio
4+
import hashlib
5+
import time
6+
from copy import deepcopy
37
from typing import Any, cast
48

59
from quart import request
@@ -28,6 +32,13 @@
2832

2933

3034
class PipelineRoute(Route):
35+
_static_cache: dict[tuple[str, str, bool], tuple[dict[str, Any], float]] = {}
36+
_render_cache: dict[tuple[str, str, str, bool], tuple[dict[str, Any], float]] = {}
37+
_inflight: dict[tuple[str, tuple[Any, ...]], asyncio.Future] = {}
38+
_cache_lock: asyncio.Lock = asyncio.Lock()
39+
STATIC_CACHE_TTL: float = 15.0
40+
RENDER_CACHE_TTL: float = 10.0
41+
3142
def __init__(self, context: RouteContext, core_lifecycle: AstrBotCoreLifecycle | None = None) -> None:
3243
super().__init__(context)
3344
self.core_lifecycle = core_lifecycle
@@ -307,7 +318,32 @@ async def get_snapshot(self):
307318
preview_prompt_provided = bool(preview_prompt_raw_str)
308319
preview_prompt = preview_prompt_raw_str if preview_prompt_provided else "(预览)用户输入:<未提供>"
309320

310-
snapshot = build_pipeline_snapshot(umo=umo, force_refresh=force_refresh, debug=debug)
321+
cls = self.__class__
322+
scope_mode = "session" if umo else "global"
323+
static_key = (scope_mode, str(umo or ""), bool(debug))
324+
325+
base_snapshot: dict[str, Any] | None = None
326+
if not force_refresh:
327+
now = time.monotonic()
328+
async with cls._cache_lock:
329+
entry = cls._static_cache.get(static_key)
330+
if entry is not None:
331+
cached_snapshot, ts = entry
332+
if now - ts <= cls.STATIC_CACHE_TTL:
333+
base_snapshot = cached_snapshot
334+
else:
335+
cls._static_cache.pop(static_key, None)
336+
337+
if base_snapshot is None:
338+
base_snapshot = build_pipeline_snapshot(umo=umo, force_refresh=force_refresh, debug=debug)
339+
async with cls._cache_lock:
340+
now = time.monotonic()
341+
for k, (_, ts) in list(cls._static_cache.items()):
342+
if now - ts > cls.STATIC_CACHE_TTL:
343+
cls._static_cache.pop(k, None)
344+
cls._static_cache[static_key] = (deepcopy(base_snapshot), now)
345+
346+
snapshot = deepcopy(base_snapshot)
311347

312348
if render:
313349
snapshot.setdefault("_debug", {})
@@ -382,12 +418,75 @@ async def get_snapshot(self):
382418
snapshot.setdefault("_debug", {})
383419
snapshot["_debug"]["persona_prompt_influencers_len"] = len(persona_influencers)
384420

385-
rendered = await self._render_final_prompt_preview(
386-
umo=render_umo,
387-
prompt=preview_prompt,
388-
debug=debug,
389-
persona_prompt_influencers=persona_influencers,
390-
)
421+
preview_hash = hashlib.sha256(preview_prompt.encode("utf-8")).hexdigest()
422+
render_cache_key = (str(snapshot.get("snapshot_id") or ""), str(render_umo or ""), preview_hash, bool(debug))
423+
424+
rendered: dict[str, Any] | None = None
425+
if not force_refresh:
426+
now = time.monotonic()
427+
async with cls._cache_lock:
428+
entry = cls._render_cache.get(render_cache_key)
429+
if entry is not None:
430+
cached_rendered, ts = entry
431+
if now - ts <= cls.RENDER_CACHE_TTL:
432+
rendered = deepcopy(cached_rendered)
433+
else:
434+
cls._render_cache.pop(render_cache_key, None)
435+
436+
if rendered is None:
437+
inflight_key = ("render", render_cache_key)
438+
fut: asyncio.Future | None = None
439+
owner = False
440+
441+
async with cls._cache_lock:
442+
if not force_refresh and rendered is None:
443+
entry = cls._render_cache.get(render_cache_key)
444+
if entry is not None:
445+
cached_rendered, ts = entry
446+
if time.monotonic() - ts <= cls.RENDER_CACHE_TTL:
447+
rendered = deepcopy(cached_rendered)
448+
449+
if rendered is None:
450+
fut = cls._inflight.get(inflight_key)
451+
if fut is None:
452+
fut = asyncio.get_running_loop().create_future()
453+
cls._inflight[inflight_key] = fut
454+
owner = True
455+
456+
if rendered is None and fut is not None and not owner:
457+
rendered = cast(dict[str, Any], await fut)
458+
459+
if rendered is None and owner:
460+
try:
461+
computed = await self._render_final_prompt_preview(
462+
umo=render_umo,
463+
prompt=preview_prompt,
464+
debug=debug,
465+
persona_prompt_influencers=persona_influencers,
466+
)
467+
rendered = computed
468+
async with cls._cache_lock:
469+
now = time.monotonic()
470+
for k, (_, ts) in list(cls._render_cache.items()):
471+
if now - ts > cls.RENDER_CACHE_TTL:
472+
cls._render_cache.pop(k, None)
473+
cls._render_cache[render_cache_key] = (deepcopy(computed), now)
474+
inflight_fut = cls._inflight.pop(inflight_key, None)
475+
if inflight_fut is not None and not inflight_fut.done():
476+
inflight_fut.set_result(deepcopy(computed))
477+
except Exception as e:
478+
async with cls._cache_lock:
479+
inflight_fut = cls._inflight.pop(inflight_key, None)
480+
if inflight_fut is not None and not inflight_fut.done():
481+
inflight_fut.set_exception(e)
482+
raise
483+
484+
if rendered is None:
485+
rendered = {
486+
"prompt": "",
487+
"system_prompt": "",
488+
}
489+
391490
rendered_prompt = rendered.get("prompt") or ""
392491
rendered_system_prompt = rendered.get("system_prompt") or ""
393492

dashboard/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"chance": "1.1.11",
2424
"date-fns": "2.30.0",
2525
"event-source-polyfill": "^1.0.31",
26+
"github-markdown-css": "^5.8.1",
2627
"highlight.js": "^11.11.1",
2728
"js-md5": "^0.8.3",
2829
"katex": "^0.16.27",

dashboard/src/components/extension/mod-manager/GlobalPanel.vue

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import type { CommandConflictGroup, PluginSummary } from './types'
66
import type { EffectTarget, PipelineStageId } from './pipeline/pipelineSnapshotTypes'
77
import PipelineSnapshotPanel from './pipeline/PipelineSnapshotPanel.vue'
88
9-
const { tm } = useModuleI18n('features/extension')
10-
119
type GlobalPanelTab = 'pipeline' | 'trace'
1210
1311
const props = withDefaults(
@@ -26,31 +24,61 @@ const emit = defineEmits<{
2624
(e: 'select-plugin', name: string): void
2725
}>()
2826
27+
const { tm } = useModuleI18n('features/extension')
28+
const $t = tm
29+
2930
const activeTab = ref<GlobalPanelTab>('pipeline')
3031
3132
const traceNavigationToken = ref(0)
3233
const traceFocusTarget = ref<EffectTarget | null>(null)
3334
const traceStageId = ref<PipelineStageId | null>(null)
35+
const traceParticipantId = ref<string | null>(null)
3436
3537
const handleSelectPlugin = (name: string) => {
3638
if (!name) return
3739
emit('select-plugin', name)
3840
}
3941
4042
const handleNavigateTrace = (payload: { participantId: string; stageId: PipelineStageId | null; target: EffectTarget }) => {
43+
traceParticipantId.value = payload.participantId || null
4144
traceFocusTarget.value = payload.target
4245
traceStageId.value = payload.stageId
4346
traceNavigationToken.value += 1
4447
activeTab.value = 'trace'
4548
}
49+
50+
const handleNavigatePipeline = () => {
51+
activeTab.value = 'pipeline'
52+
}
4653
</script>
4754

4855
<template>
4956
<v-card class="h-100 d-flex flex-column global-panel" rounded="lg" variant="flat">
5057
<div class="global-panel__header">
5158
<v-tabs v-model="activeTab" color="primary" density="comfortable">
52-
<v-tab value="pipeline">{{ tm('pipeline.tabTitle') }}</v-tab>
53-
<v-tab value="trace">{{ tm('pipeline.traceTabTitle') }}</v-tab>
59+
<v-tab value="pipeline">
60+
<span class="d-inline-flex align-center">
61+
{{ $t('pipeline.tabs.pipeline') }}
62+
<v-tooltip location="bottom">
63+
<template #activator="{ props: tooltipProps }">
64+
<v-icon v-bind="tooltipProps" size="small" class="ml-1">mdi-information-outline</v-icon>
65+
</template>
66+
{{ $t('pipeline.tabs.pipelineTooltip') }}
67+
</v-tooltip>
68+
</span>
69+
</v-tab>
70+
71+
<v-tab value="trace">
72+
<span class="d-inline-flex align-center">
73+
{{ $t('pipeline.tabs.trace') }}
74+
<v-tooltip location="bottom">
75+
<template #activator="{ props: tooltipProps }">
76+
<v-icon v-bind="tooltipProps" size="small" class="ml-1">mdi-information-outline</v-icon>
77+
</template>
78+
{{ $t('pipeline.tabs.traceTooltip') }}
79+
</v-tooltip>
80+
</span>
81+
</v-tab>
5482
</v-tabs>
5583
<v-divider />
5684
</div>
@@ -63,8 +91,10 @@ const handleNavigateTrace = (payload: { participantId: string; stageId: Pipeline
6391
:trace-navigation-token="traceNavigationToken"
6492
:trace-focus-target="traceFocusTarget"
6593
:trace-stage-id="traceStageId"
94+
:trace-participant-id="traceParticipantId"
6695
@select-plugin="handleSelectPlugin"
6796
@navigate-trace="handleNavigateTrace"
97+
@navigate-pipeline="handleNavigatePipeline"
6898
/>
6999
</div>
70100
</v-card>

dashboard/src/components/extension/mod-manager/LegacyInstalledView.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const emit = defineEmits<{
2323
(e: 'action-configure', plugin: PluginSummary): void
2424
(e: 'action-view-handlers', plugin: PluginSummary): void
2525
(e: 'action-view-readme', plugin: PluginSummary): void
26+
(e: 'view-changelog', plugin: PluginSummary): void
2627
(e: 'action-open-repo', url: string): void
2728
}>()
2829
@@ -306,6 +307,7 @@ function toPlugin(item: unknown): PluginSummary {
306307
@toggle-activation="handleToggleActivation(extension)"
307308
@view-handlers="emit('action-view-handlers', extension)"
308309
@view-readme="emit('action-view-readme', extension)"
310+
@view-changelog="emit('view-changelog', extension)"
309311
/>
310312
</div>
311313
</v-col>

0 commit comments

Comments
 (0)