@xingwangzhe/stalux 1.25.14 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/package.json +20 -18
- package/src/components/stalux/categories/categoriesList.astro +1 -1
- package/src/components/stalux/footer/FooterBadges.astro +1 -1
- package/src/components/stalux/layout/footer.astro +1 -1
- package/src/components/stalux/layout/navs.astro +3 -3
- package/src/components/stalux/tags/tagsCloud.astro +1 -1
- package/src/index.ts +54 -23
- package/src/internal/components-plugin.ts +7 -1
- package/src/internal/font-slices.ts +14 -2
- package/src/pages/api/posts.json.ts +2 -2
- package/src/pages/archives.md.ts +1 -1
- package/src/pages/atom.xml.ts +2 -2
- package/src/pages/categories/index.md.ts +1 -1
- package/src/pages/index.md.ts +1 -1
- package/src/pages/llms-full.txt.ts +1 -1
- package/src/pages/llms.txt.ts +1 -1
- package/src/pages/posts/[post].astro +1 -1
- package/src/pages/rss.xml.ts +2 -2
- package/src/pages/tags/index.md.ts +1 -1
- package/src/plugins/feature-flags.ts +11 -2
- package/src/plugins/satteri-temml.ts +10 -3
- package/src/schemas/collections.ts +48 -24
- package/src/scripts/background.ts +8 -2
- package/src/scripts/clarity.ts +11 -2
- package/src/scripts/google-analytics.ts +13 -2
- package/src/scripts/logger.ts +36 -0
- package/src/scripts/navigation.ts +12 -1
- package/src/scripts/page-runtime.ts +28 -4
- package/src/scripts/photoswipe.ts +12 -1
- package/src/scripts/random-posts.ts +7 -1
- package/src/scripts/tags-cloud.ts +13 -2
- package/src/scripts/vercount.ts +11 -2
- package/src/scripts/view-transitions.ts +4 -0
- package/src/scripts/waline.ts +8 -2
- package/src/scripts/webmcp.ts +25 -14
- package/src/utils/ai-discovery.ts +32 -13
- package/src/utils/badge-generator.ts +4 -2
- package/src/utils/collections-stats.ts +5 -4
- package/src/utils/content-index.ts +22 -6
- package/src/utils/diagnostics.ts +45 -0
- package/src/utils/feed.ts +5 -1
- package/src/utils/lucide-icons.ts +7 -3
- package/src/utils/word-count-utils.ts +22 -9
|
@@ -16,11 +16,27 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { defineCollection } from "astro:content";
|
|
19
|
-
import { glob } from "astro/loaders";
|
|
19
|
+
import { glob, type Loader } from "astro/loaders";
|
|
20
20
|
import { z } from "astro/zod";
|
|
21
21
|
|
|
22
22
|
import { configSchema } from "./config";
|
|
23
23
|
|
|
24
|
+
/** Preserve glob's name, watch behavior and incremental store; only trace the load boundary. */
|
|
25
|
+
function tracedLoader(loader: Loader): Loader {
|
|
26
|
+
return {
|
|
27
|
+
...loader,
|
|
28
|
+
async load(context) {
|
|
29
|
+
const logger = context.logger.fork(`stalux/content/${context.collection}`);
|
|
30
|
+
const started = performance.now();
|
|
31
|
+
logger.debug("load started");
|
|
32
|
+
await loader.load(context);
|
|
33
|
+
logger.debug(
|
|
34
|
+
`load completed; entries=${context.store.entries().length}; elapsed=${(performance.now() - started).toFixed(1)}ms`,
|
|
35
|
+
);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
// ---------------------------------------------------------------------------
|
|
25
41
|
// 集合配置选项
|
|
26
42
|
// ---------------------------------------------------------------------------
|
|
@@ -71,31 +87,37 @@ export function defineCollections(paths: CollectionPaths = {}) {
|
|
|
71
87
|
const root = paths.contentDir ?? "stalux";
|
|
72
88
|
|
|
73
89
|
const posts = defineCollection({
|
|
74
|
-
loader:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
loader: tracedLoader(
|
|
91
|
+
glob({
|
|
92
|
+
pattern: ["*.{md,mdx}"],
|
|
93
|
+
base: `${root}/posts/`,
|
|
94
|
+
generateId: ({ data }) => String((data as Record<string, unknown>).abbrlink ?? ""),
|
|
95
|
+
retainBody: true,
|
|
96
|
+
deferRender: true,
|
|
97
|
+
}),
|
|
98
|
+
),
|
|
81
99
|
schema: postSchema,
|
|
82
100
|
});
|
|
83
101
|
|
|
84
102
|
const config = defineCollection({
|
|
85
|
-
loader:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
103
|
+
loader: tracedLoader(
|
|
104
|
+
glob({
|
|
105
|
+
pattern: ["*.yml"],
|
|
106
|
+
base: `${root}/config/`,
|
|
107
|
+
generateId: ({ data }) => String((data as Record<string, unknown>).id ?? ""),
|
|
108
|
+
}),
|
|
109
|
+
),
|
|
90
110
|
schema: configSchema,
|
|
91
111
|
});
|
|
92
112
|
|
|
93
113
|
const about = defineCollection({
|
|
94
|
-
loader:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
114
|
+
loader: tracedLoader(
|
|
115
|
+
glob({
|
|
116
|
+
base: `${root}/about`,
|
|
117
|
+
pattern: "**/*.{md,mdx}",
|
|
118
|
+
retainBody: true,
|
|
119
|
+
}),
|
|
120
|
+
),
|
|
99
121
|
schema: z.object({
|
|
100
122
|
title: z.string().min(1, "about.title is required"),
|
|
101
123
|
description: z.string().min(1, "about.description is required"),
|
|
@@ -103,12 +125,14 @@ export function defineCollections(paths: CollectionPaths = {}) {
|
|
|
103
125
|
});
|
|
104
126
|
|
|
105
127
|
const words = defineCollection({
|
|
106
|
-
loader:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
128
|
+
loader: tracedLoader(
|
|
129
|
+
glob({
|
|
130
|
+
pattern: ["*.md"],
|
|
131
|
+
base: `${root}/words/`,
|
|
132
|
+
retainBody: true,
|
|
133
|
+
deferRender: true,
|
|
134
|
+
}),
|
|
135
|
+
),
|
|
112
136
|
schema: z.object({
|
|
113
137
|
source: z.string().optional(),
|
|
114
138
|
link: z.url("words.link must be a valid URL").optional(),
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* window load(首访)与 swap 完成后、过渡动画期间(软导航)触发,
|
|
13
13
|
* 此时设置背景,过渡动画中旧图淡出、新图淡入,自然 crossfade 无跳变。
|
|
14
14
|
*/
|
|
15
|
-
|
|
15
|
+
import { createClientLogger } from "./logger";
|
|
16
|
+
|
|
17
|
+
const logger = createClientLogger("background");
|
|
16
18
|
|
|
17
19
|
declare global {
|
|
18
20
|
interface Window {
|
|
@@ -63,7 +65,11 @@ function crossfadeBackground(): void {
|
|
|
63
65
|
const previousIndex = Number(document.body.dataset.staluxBgIndex ?? -1);
|
|
64
66
|
const index = pickBackgroundIndex(previousIndex);
|
|
65
67
|
const next = getLayerEl(nextLayer);
|
|
66
|
-
if (!next || !backgroundImages[index])
|
|
68
|
+
if (!next || !backgroundImages[index]) {
|
|
69
|
+
logger.debug("background layer unavailable; skipped");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
logger.debug(`crossfade layer=${nextLayer}; index=${index}`);
|
|
67
73
|
|
|
68
74
|
next.style.backgroundImage = `url('${backgroundImages[index]}')`;
|
|
69
75
|
setLayerOpacity(nextLayer, 1);
|
package/src/scripts/clarity.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { createClientLogger } from "./logger";
|
|
2
|
+
|
|
3
|
+
const logger = createClientLogger("clarity");
|
|
4
|
+
|
|
1
5
|
declare global {
|
|
2
6
|
interface Window {
|
|
3
7
|
clarity?: ((...args: unknown[]) => void) & { q?: unknown[][] };
|
|
@@ -18,6 +22,13 @@ function load() {
|
|
|
18
22
|
script.id = "stalux-clarity-script";
|
|
19
23
|
script.async = true;
|
|
20
24
|
script.src = `https://www.clarity.ms/tag/${encodeURIComponent(id)}`;
|
|
25
|
+
script.addEventListener("load", () => logger.debug("external script loaded"), { once: true });
|
|
26
|
+
script.addEventListener(
|
|
27
|
+
"error",
|
|
28
|
+
() => logger.warn("external script failed to load (network or content blocker)"),
|
|
29
|
+
{ once: true },
|
|
30
|
+
);
|
|
31
|
+
logger.debug("loading external script");
|
|
21
32
|
document.head.appendChild(script);
|
|
22
33
|
}
|
|
23
34
|
|
|
@@ -28,5 +39,3 @@ if (!window.__staluxClarityPageLoadListener) {
|
|
|
28
39
|
if (document.readyState === "loading")
|
|
29
40
|
document.addEventListener("DOMContentLoaded", load, { once: true });
|
|
30
41
|
else load();
|
|
31
|
-
|
|
32
|
-
export {};
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { createClientLogger } from "./logger";
|
|
2
|
+
|
|
3
|
+
const logger = createClientLogger("google-analytics");
|
|
4
|
+
|
|
1
5
|
declare global {
|
|
2
6
|
interface Window {
|
|
3
7
|
dataLayer?: unknown[];
|
|
@@ -19,6 +23,15 @@ function load() {
|
|
|
19
23
|
script.async = true;
|
|
20
24
|
script.dataset.staluxGoogle = "true";
|
|
21
25
|
script.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(gaID)}`;
|
|
26
|
+
script.addEventListener("load", () => logger.debug("external script loaded"), {
|
|
27
|
+
once: true,
|
|
28
|
+
});
|
|
29
|
+
script.addEventListener(
|
|
30
|
+
"error",
|
|
31
|
+
() => logger.warn("external script failed to load (network or content blocker)"),
|
|
32
|
+
{ once: true },
|
|
33
|
+
);
|
|
34
|
+
logger.debug("loading external script");
|
|
22
35
|
document.head.appendChild(script);
|
|
23
36
|
}
|
|
24
37
|
if (!window.__staluxAnalyticsLoaded) {
|
|
@@ -44,5 +57,3 @@ if (!window.__staluxAnalyticsPageLoadListener) {
|
|
|
44
57
|
if (document.readyState === "loading")
|
|
45
58
|
document.addEventListener("DOMContentLoaded", load, { once: true });
|
|
46
59
|
else load();
|
|
47
|
-
|
|
48
|
-
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describeError } from "../utils/diagnostics";
|
|
2
|
+
|
|
3
|
+
declare const __STALUX_DEBUG__: boolean;
|
|
4
|
+
|
|
5
|
+
/** Browser-only sink; not an Astro runtime logger and never forwarded to a server. */
|
|
6
|
+
interface ClientLogEnvironment {
|
|
7
|
+
dev: boolean;
|
|
8
|
+
detailed: boolean;
|
|
9
|
+
sink: Pick<Console, "debug" | "warn" | "error">;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createClientLogger(
|
|
13
|
+
scope: string,
|
|
14
|
+
environment: ClientLogEnvironment = {
|
|
15
|
+
dev: import.meta.env.DEV,
|
|
16
|
+
get detailed() {
|
|
17
|
+
return typeof __STALUX_DEBUG__ !== "undefined" && __STALUX_DEBUG__;
|
|
18
|
+
},
|
|
19
|
+
sink: console,
|
|
20
|
+
},
|
|
21
|
+
) {
|
|
22
|
+
const prefix = `[stalux/${scope}]`;
|
|
23
|
+
return {
|
|
24
|
+
debug(message: string) {
|
|
25
|
+
if (environment.dev && environment.detailed) {
|
|
26
|
+
environment.sink.debug(`${prefix} ${message}`);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
warn(message: string) {
|
|
30
|
+
environment.sink.warn(`${prefix} ${message}`);
|
|
31
|
+
},
|
|
32
|
+
error(message: string, error: unknown) {
|
|
33
|
+
environment.sink.error(`${prefix} ${message}: ${describeError(error)}`);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
+
import { createClientLogger } from "./logger";
|
|
2
|
+
|
|
1
3
|
import { registerPageLifecycle } from "./page-runtime";
|
|
2
4
|
import { upgradeAndOpenSearchDialog } from "./search-dialog";
|
|
3
5
|
|
|
6
|
+
const logger = createClientLogger("navigation");
|
|
7
|
+
|
|
4
8
|
interface SearchDialog extends HTMLElement {
|
|
5
9
|
open?: () => void;
|
|
6
10
|
close?: () => void;
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
async function openSearchDialog(): Promise<void> {
|
|
14
|
+
logger.debug("loading search UI");
|
|
10
15
|
await import("@pagefind/component-ui");
|
|
11
16
|
const dialog = document.querySelector<SearchDialog>("pagefind-modal#search");
|
|
12
17
|
await upgradeAndOpenSearchDialog(
|
|
@@ -78,7 +83,13 @@ registerPageLifecycle("navigation", () => {
|
|
|
78
83
|
async (event) => {
|
|
79
84
|
event.preventDefault();
|
|
80
85
|
closeNav();
|
|
81
|
-
|
|
86
|
+
try {
|
|
87
|
+
await openSearchDialog();
|
|
88
|
+
logger.debug("search dialog opened");
|
|
89
|
+
} catch (error) {
|
|
90
|
+
logger.error("search dialog failed to open; next click can retry", error);
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
82
93
|
},
|
|
83
94
|
listenerOptions,
|
|
84
95
|
);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createClientLogger } from "./logger";
|
|
2
|
+
|
|
1
3
|
export type PageDisposer = () => void;
|
|
2
4
|
export type PageMount = () => PageDisposer | undefined;
|
|
3
5
|
|
|
@@ -40,12 +42,17 @@ export function registerPageLifecycle(
|
|
|
40
42
|
environment = getBrowserEnvironment(),
|
|
41
43
|
): PageDisposer {
|
|
42
44
|
if (!environment) return () => undefined;
|
|
45
|
+
const logger = createClientLogger(key);
|
|
43
46
|
|
|
44
47
|
const { host, target, queueTask } = environment;
|
|
45
48
|
const registry = host.__staluxPageRuntime ?? new Map<string, RuntimeRegistration>();
|
|
46
49
|
host.__staluxPageRuntime = registry;
|
|
47
50
|
const existing = registry.get(key);
|
|
48
|
-
if (existing)
|
|
51
|
+
if (existing) {
|
|
52
|
+
logger.debug("registration reused");
|
|
53
|
+
return existing.unregister;
|
|
54
|
+
}
|
|
55
|
+
logger.debug("lifecycle registered");
|
|
49
56
|
|
|
50
57
|
let active = true;
|
|
51
58
|
const registration: RuntimeRegistration = {
|
|
@@ -53,13 +60,30 @@ export function registerPageLifecycle(
|
|
|
53
60
|
};
|
|
54
61
|
|
|
55
62
|
const disposePage = () => {
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
try {
|
|
64
|
+
if (registration.disposePage) logger.debug("disposing page");
|
|
65
|
+
registration.disposePage?.();
|
|
66
|
+
} catch (error) {
|
|
67
|
+
logger.error("page disposal failed", error);
|
|
68
|
+
throw error;
|
|
69
|
+
} finally {
|
|
70
|
+
registration.disposePage = undefined;
|
|
71
|
+
}
|
|
58
72
|
};
|
|
59
73
|
const mountPage = () => {
|
|
60
74
|
if (!active) return;
|
|
61
75
|
disposePage();
|
|
62
|
-
|
|
76
|
+
const started = performance.now();
|
|
77
|
+
logger.debug("mount started");
|
|
78
|
+
try {
|
|
79
|
+
registration.disposePage = mount() || undefined;
|
|
80
|
+
logger.debug(
|
|
81
|
+
`mount completed in ${(performance.now() - started).toFixed(1)}ms; cleanup=${Boolean(registration.disposePage)}`,
|
|
82
|
+
);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
logger.error("page mount failed", error);
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
63
87
|
};
|
|
64
88
|
const handlePageLoad = () => mountPage();
|
|
65
89
|
const handleBeforeSwap = () => disposePage();
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { createClientLogger } from "./logger";
|
|
2
|
+
|
|
1
3
|
import { registerPageLifecycle } from "./page-runtime";
|
|
2
4
|
import { createRetryableInitializer } from "./retryable-initializer";
|
|
3
5
|
|
|
6
|
+
const logger = createClientLogger("photoswipe");
|
|
7
|
+
|
|
4
8
|
const GALLERY_SELECTOR = "a.pswp-gallery-item";
|
|
5
9
|
|
|
6
10
|
async function enrichPhotoSwipeData(container: Element): Promise<void> {
|
|
@@ -20,6 +24,7 @@ async function enrichPhotoSwipeData(container: Element): Promise<void> {
|
|
|
20
24
|
try {
|
|
21
25
|
await temporaryImage.decode();
|
|
22
26
|
} catch {
|
|
27
|
+
logger.debug("image dimensions unavailable; skipping enrichment");
|
|
23
28
|
continue;
|
|
24
29
|
}
|
|
25
30
|
link.dataset.pswpWidth = String(temporaryImage.naturalWidth);
|
|
@@ -38,6 +43,7 @@ registerPageLifecycle("photoswipe", () => {
|
|
|
38
43
|
let initialized = false;
|
|
39
44
|
|
|
40
45
|
const initialize = async () => {
|
|
46
|
+
logger.debug("loading lightbox modules");
|
|
41
47
|
const [{ default: PhotoSwipeLightbox }, { default: PhotoSwipe }] = await Promise.all([
|
|
42
48
|
import("photoswipe/lightbox"),
|
|
43
49
|
import("photoswipe"),
|
|
@@ -50,7 +56,11 @@ registerPageLifecycle("photoswipe", () => {
|
|
|
50
56
|
pswpModule: PhotoSwipe,
|
|
51
57
|
initialZoomLevel: "fill",
|
|
52
58
|
});
|
|
59
|
+
lightbox.on("openingAnimationEnd", () => logger.debug("opening animation completed"));
|
|
60
|
+
lightbox.on("close", () => logger.debug("lightbox closing"));
|
|
61
|
+
lightbox.on("destroy", () => logger.debug("lightbox destroyed"));
|
|
53
62
|
lightbox.init();
|
|
63
|
+
logger.debug("lightbox initialized");
|
|
54
64
|
};
|
|
55
65
|
|
|
56
66
|
const ensureInitialized = createRetryableInitializer(initialize);
|
|
@@ -68,10 +78,11 @@ registerPageLifecycle("photoswipe", () => {
|
|
|
68
78
|
void ensureInitialized()
|
|
69
79
|
.then(() => {
|
|
70
80
|
initialized = true;
|
|
81
|
+
logger.debug(`opening image index=${clickedIndex}`);
|
|
71
82
|
lightbox?.loadAndOpen(clickedIndex, undefined, initialPoint);
|
|
72
83
|
})
|
|
73
84
|
.catch((error: unknown) => {
|
|
74
|
-
|
|
85
|
+
logger.error("initialization failed; next click can retry", error);
|
|
75
86
|
});
|
|
76
87
|
};
|
|
77
88
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { createClientLogger } from "./logger";
|
|
1
2
|
import { registerPageLifecycle } from "./page-runtime";
|
|
2
3
|
|
|
4
|
+
const logger = createClientLogger("random-posts");
|
|
5
|
+
|
|
3
6
|
const sleep = (ms: number, signal: AbortSignal) =>
|
|
4
7
|
new Promise<void>((resolve) => {
|
|
5
8
|
const timer = window.setTimeout(resolve, ms);
|
|
@@ -49,16 +52,19 @@ async function refresh(container: HTMLElement, fallback: string, signal: AbortSi
|
|
|
49
52
|
await sleep(300, signal);
|
|
50
53
|
if (signal.aborted) return;
|
|
51
54
|
try {
|
|
55
|
+
logger.debug("refresh started");
|
|
52
56
|
const posts = shuffle(await fetchPosts(signal)).slice(
|
|
53
57
|
0,
|
|
54
58
|
Number(container.dataset.count ?? 5),
|
|
55
59
|
);
|
|
56
60
|
if (signal.aborted) return;
|
|
61
|
+
logger.debug(`refresh completed; posts=${posts.length}`);
|
|
57
62
|
list.innerHTML = posts.length
|
|
58
63
|
? render(posts)
|
|
59
64
|
: `<li class="no-posts">${container.dataset.noPosts ?? fallback}</li>`;
|
|
60
|
-
} catch {
|
|
65
|
+
} catch (error) {
|
|
61
66
|
if (signal.aborted) return;
|
|
67
|
+
logger.error("refresh failed; showing fallback", error);
|
|
62
68
|
list.innerHTML = `<li class="error">${container.dataset.loadFailed ?? fallback}</li>`;
|
|
63
69
|
} finally {
|
|
64
70
|
if (!signal.aborted) list.style.opacity = "1";
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { TagCloud } from "@xingwangzhe/tags-cloud";
|
|
2
|
+
import { createClientLogger } from "./logger";
|
|
2
3
|
|
|
3
4
|
import { registerPageLifecycle } from "./page-runtime";
|
|
4
5
|
|
|
6
|
+
const logger = createClientLogger("tags-cloud");
|
|
7
|
+
|
|
5
8
|
interface LinkTag {
|
|
6
9
|
type: "link";
|
|
7
10
|
text: string;
|
|
@@ -11,7 +14,10 @@ interface LinkTag {
|
|
|
11
14
|
function parseTags(value: string | undefined): LinkTag[] {
|
|
12
15
|
try {
|
|
13
16
|
const parsed: unknown = JSON.parse(value ?? "[]");
|
|
14
|
-
if (!Array.isArray(parsed))
|
|
17
|
+
if (!Array.isArray(parsed)) {
|
|
18
|
+
logger.warn("tag data is not an array; using empty list");
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
15
21
|
return parsed.filter(
|
|
16
22
|
(tag): tag is LinkTag =>
|
|
17
23
|
typeof tag === "object" &&
|
|
@@ -24,13 +30,17 @@ function parseTags(value: string | undefined): LinkTag[] {
|
|
|
24
30
|
typeof tag.url === "string",
|
|
25
31
|
);
|
|
26
32
|
} catch {
|
|
33
|
+
logger.warn("tag data decoding failed; using empty list");
|
|
27
34
|
return [];
|
|
28
35
|
}
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
registerPageLifecycle("tags-cloud", () => {
|
|
32
39
|
const mobile = window.matchMedia("(max-width: 768px)").matches;
|
|
33
|
-
if (mobile)
|
|
40
|
+
if (mobile) {
|
|
41
|
+
logger.debug("mobile layout; cloud skipped");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
34
44
|
|
|
35
45
|
const container = document.getElementById("tags-canvas");
|
|
36
46
|
if (!container) return;
|
|
@@ -55,5 +65,6 @@ registerPageLifecycle("tags-cloud", () => {
|
|
|
55
65
|
color: "#ffffff",
|
|
56
66
|
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
57
67
|
});
|
|
68
|
+
logger.debug(`cloud initialized; tags=${tags.length}`);
|
|
58
69
|
return () => cloud.destroy();
|
|
59
70
|
});
|
package/src/scripts/vercount.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { createClientLogger } from "./logger";
|
|
2
|
+
|
|
3
|
+
const logger = createClientLogger("vercount");
|
|
4
|
+
|
|
1
5
|
declare global {
|
|
2
6
|
interface Window {
|
|
3
7
|
__staluxVerCountLoaded?: boolean;
|
|
@@ -11,6 +15,13 @@ function load() {
|
|
|
11
15
|
script.defer = true;
|
|
12
16
|
script.dataset.staluxVercount = "true";
|
|
13
17
|
script.src = "https://events.vercount.one/js";
|
|
18
|
+
script.addEventListener("load", () => logger.debug("external script loaded"), { once: true });
|
|
19
|
+
script.addEventListener(
|
|
20
|
+
"error",
|
|
21
|
+
() => logger.warn("external script failed to load (network or content blocker)"),
|
|
22
|
+
{ once: true },
|
|
23
|
+
);
|
|
24
|
+
logger.debug("loading external script");
|
|
14
25
|
document.head.appendChild(script);
|
|
15
26
|
}
|
|
16
27
|
|
|
@@ -18,5 +29,3 @@ document.addEventListener("astro:page-load", load);
|
|
|
18
29
|
if (document.readyState === "loading")
|
|
19
30
|
document.addEventListener("DOMContentLoaded", load, { once: true });
|
|
20
31
|
else load();
|
|
21
|
-
|
|
22
|
-
export {};
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { getRouteKind } from "@utils/view-transitions";
|
|
2
|
+
import { createClientLogger } from "./logger";
|
|
3
|
+
|
|
4
|
+
const logger = createClientLogger("view-transitions");
|
|
2
5
|
|
|
3
6
|
interface TransitionWindow extends Window {
|
|
4
7
|
__STALUX_VT_LISTENER__?: boolean;
|
|
@@ -7,6 +10,7 @@ interface TransitionWindow extends Window {
|
|
|
7
10
|
function setRouteState(pathname: string): void {
|
|
8
11
|
const html = document.documentElement;
|
|
9
12
|
const route = getRouteKind(pathname);
|
|
13
|
+
logger.debug(`route state=${route}`);
|
|
10
14
|
html.dataset.staluxRoute = route;
|
|
11
15
|
document.querySelector("main.stalux-main")?.setAttribute("data-stalux-route", route);
|
|
12
16
|
}
|
package/src/scripts/waline.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { init, type WalineInitOptions } from "@waline/client";
|
|
2
|
+
import { createClientLogger } from "./logger";
|
|
2
3
|
|
|
3
4
|
import { registerPageLifecycle } from "./page-runtime";
|
|
4
5
|
|
|
6
|
+
const logger = createClientLogger("waline");
|
|
7
|
+
|
|
5
8
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
6
9
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
10
|
}
|
|
@@ -18,11 +21,13 @@ registerPageLifecycle("waline", () => {
|
|
|
18
21
|
);
|
|
19
22
|
decoded = JSON.parse(new TextDecoder().decode(bytes));
|
|
20
23
|
} catch (error) {
|
|
21
|
-
|
|
24
|
+
logger.warn(
|
|
25
|
+
`config decoding failed (${error instanceof Error ? error.name : "unknown error"})`,
|
|
26
|
+
);
|
|
22
27
|
return;
|
|
23
28
|
}
|
|
24
29
|
if (!isRecord(decoded) || typeof decoded.serverURL !== "string") {
|
|
25
|
-
|
|
30
|
+
logger.warn("serverURL not configured");
|
|
26
31
|
return;
|
|
27
32
|
}
|
|
28
33
|
|
|
@@ -33,6 +38,7 @@ registerPageLifecycle("waline", () => {
|
|
|
33
38
|
path: typeof decoded.path === "string" ? decoded.path : window.location.pathname,
|
|
34
39
|
} as WalineInitOptions;
|
|
35
40
|
const instance = init(options);
|
|
41
|
+
logger.debug("comment widget initialized");
|
|
36
42
|
|
|
37
43
|
return () => instance?.destroy();
|
|
38
44
|
});
|
package/src/scripts/webmcp.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createClientLogger } from "./logger";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Stalux WebMCP 工具注册(纯前端,零后端)
|
|
3
5
|
*
|
|
@@ -20,6 +22,8 @@
|
|
|
20
22
|
// 放在模块顶部 import,保证其副作用在下方注册逻辑执行前完成。
|
|
21
23
|
import "@mcp-b/webmcp-polyfill";
|
|
22
24
|
|
|
25
|
+
const logger = createClientLogger("webmcp");
|
|
26
|
+
|
|
23
27
|
declare global {
|
|
24
28
|
interface Window {
|
|
25
29
|
__STALUX_SITE_INFO__?: {
|
|
@@ -78,18 +82,23 @@ function err(
|
|
|
78
82
|
message: string,
|
|
79
83
|
extra?: Record<string, unknown>,
|
|
80
84
|
): { ok: false; code: string; message: string } & Record<string, unknown> {
|
|
85
|
+
logger.debug(`tool result error=${code}`);
|
|
81
86
|
return { ok: false, code, message, ...extra };
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
async function fetchJSON(url: string): Promise<unknown> {
|
|
85
90
|
const r = await fetch(url, { headers: { Accept: "application/json" } });
|
|
86
|
-
if (!r.ok)
|
|
91
|
+
if (!r.ok) {
|
|
92
|
+
logger.debug(`JSON request failed; status=${r.status}`);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
87
95
|
return r.json();
|
|
88
96
|
}
|
|
89
97
|
|
|
90
98
|
/** 动态加载 Pagefind 全文索引(仅首次调用,结果跨调用缓存) */
|
|
91
99
|
let pagefindLoadPromise: Promise<{ search: (q: string) => Promise<unknown> } | null> | null = null;
|
|
92
100
|
async function loadPagefind(): Promise<{ search: (q: string) => Promise<unknown> } | null> {
|
|
101
|
+
logger.debug(pagefindLoadPromise ? "Pagefind module cache hit" : "loading Pagefind module");
|
|
93
102
|
if (!pagefindLoadPromise) {
|
|
94
103
|
pagefindLoadPromise = (async () => {
|
|
95
104
|
try {
|
|
@@ -99,6 +108,7 @@ async function loadPagefind(): Promise<{ search: (q: string) => Promise<unknown>
|
|
|
99
108
|
const mod = await import(/* @vite-ignore */ url);
|
|
100
109
|
return mod.default ?? mod;
|
|
101
110
|
} catch {
|
|
111
|
+
logger.warn("Pagefind index unavailable; search requires a production index");
|
|
102
112
|
return null;
|
|
103
113
|
}
|
|
104
114
|
})();
|
|
@@ -422,6 +432,7 @@ function searchPostsTool(): WebMCPTool {
|
|
|
422
432
|
excerpt: (data?.excerpt ?? "").slice(0, 240),
|
|
423
433
|
};
|
|
424
434
|
} catch {
|
|
435
|
+
logger.warn("search result data unavailable; skipping result");
|
|
425
436
|
return null;
|
|
426
437
|
}
|
|
427
438
|
}),
|
|
@@ -537,6 +548,7 @@ async function registerTools(): Promise<{ registered: string[]; reason?: string
|
|
|
537
548
|
const doc = document as DocumentWithModelContext;
|
|
538
549
|
const ctx = doc.modelContext;
|
|
539
550
|
if (!ctx || typeof ctx.registerTool !== "function") {
|
|
551
|
+
logger.debug("modelContext unavailable; registration skipped");
|
|
540
552
|
return { registered: [], reason: "unsupported" };
|
|
541
553
|
}
|
|
542
554
|
|
|
@@ -545,7 +557,7 @@ async function registerTools(): Promise<{ registered: string[]; reason?: string
|
|
|
545
557
|
try {
|
|
546
558
|
activeController.abort();
|
|
547
559
|
} catch {
|
|
548
|
-
|
|
560
|
+
logger.debug("previous registration cleanup unavailable");
|
|
549
561
|
}
|
|
550
562
|
}
|
|
551
563
|
const controller = new AbortController();
|
|
@@ -559,7 +571,7 @@ async function registerTools(): Promise<{ registered: string[]; reason?: string
|
|
|
559
571
|
await ctx.registerTool(tool, { signal: controller.signal });
|
|
560
572
|
registered.push(tool.name);
|
|
561
573
|
} catch (e) {
|
|
562
|
-
|
|
574
|
+
logger.error(`tool registration failed: ${tool.name}`, e);
|
|
563
575
|
}
|
|
564
576
|
}
|
|
565
577
|
return { registered };
|
|
@@ -572,16 +584,15 @@ async function registerTools(): Promise<{ registered: string[]; reason?: string
|
|
|
572
584
|
if (typeof document !== "undefined") {
|
|
573
585
|
// 首屏加载 + View Transitions 软导航都执行(soft navigation 时页面状态会重建)
|
|
574
586
|
document.addEventListener("astro:page-load", () => {
|
|
575
|
-
void registerTools()
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
r.registered.length
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
});
|
|
587
|
+
void registerTools()
|
|
588
|
+
.then((r) => {
|
|
589
|
+
if (r.reason === "unsupported") return; // 无 WebMCP 环境,静默跳过
|
|
590
|
+
if (r.registered.length) {
|
|
591
|
+
logger.debug(
|
|
592
|
+
`已注册 ${r.registered.length} 个工具: ${r.registered.join(", ")}`,
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
})
|
|
596
|
+
.catch((error: unknown) => logger.error("registration batch failed", error));
|
|
586
597
|
});
|
|
587
598
|
}
|