@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.
Files changed (44) hide show
  1. package/README.md +29 -0
  2. package/package.json +20 -18
  3. package/src/components/stalux/categories/categoriesList.astro +1 -1
  4. package/src/components/stalux/footer/FooterBadges.astro +1 -1
  5. package/src/components/stalux/layout/footer.astro +1 -1
  6. package/src/components/stalux/layout/navs.astro +3 -3
  7. package/src/components/stalux/tags/tagsCloud.astro +1 -1
  8. package/src/index.ts +54 -23
  9. package/src/internal/components-plugin.ts +7 -1
  10. package/src/internal/font-slices.ts +14 -2
  11. package/src/pages/api/posts.json.ts +2 -2
  12. package/src/pages/archives.md.ts +1 -1
  13. package/src/pages/atom.xml.ts +2 -2
  14. package/src/pages/categories/index.md.ts +1 -1
  15. package/src/pages/index.md.ts +1 -1
  16. package/src/pages/llms-full.txt.ts +1 -1
  17. package/src/pages/llms.txt.ts +1 -1
  18. package/src/pages/posts/[post].astro +1 -1
  19. package/src/pages/rss.xml.ts +2 -2
  20. package/src/pages/tags/index.md.ts +1 -1
  21. package/src/plugins/feature-flags.ts +11 -2
  22. package/src/plugins/satteri-temml.ts +10 -3
  23. package/src/schemas/collections.ts +48 -24
  24. package/src/scripts/background.ts +8 -2
  25. package/src/scripts/clarity.ts +11 -2
  26. package/src/scripts/google-analytics.ts +13 -2
  27. package/src/scripts/logger.ts +36 -0
  28. package/src/scripts/navigation.ts +12 -1
  29. package/src/scripts/page-runtime.ts +28 -4
  30. package/src/scripts/photoswipe.ts +12 -1
  31. package/src/scripts/random-posts.ts +7 -1
  32. package/src/scripts/tags-cloud.ts +13 -2
  33. package/src/scripts/vercount.ts +11 -2
  34. package/src/scripts/view-transitions.ts +4 -0
  35. package/src/scripts/waline.ts +8 -2
  36. package/src/scripts/webmcp.ts +25 -14
  37. package/src/utils/ai-discovery.ts +32 -13
  38. package/src/utils/badge-generator.ts +4 -2
  39. package/src/utils/collections-stats.ts +5 -4
  40. package/src/utils/content-index.ts +22 -6
  41. package/src/utils/diagnostics.ts +45 -0
  42. package/src/utils/feed.ts +5 -1
  43. package/src/utils/lucide-icons.ts +7 -3
  44. 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: glob({
75
- pattern: ["*.{md,mdx}"],
76
- base: `${root}/posts/`,
77
- generateId: ({ data }) => String((data as Record<string, unknown>).abbrlink ?? ""),
78
- retainBody: true,
79
- deferRender: true,
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: glob({
86
- pattern: ["*.yml"],
87
- base: `${root}/config/`,
88
- generateId: ({ data }) => String((data as Record<string, unknown>).id ?? ""),
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: glob({
95
- base: `${root}/about`,
96
- pattern: "**/*.{md,mdx}",
97
- retainBody: true,
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: glob({
107
- pattern: ["*.md"],
108
- base: `${root}/words/`,
109
- retainBody: true,
110
- deferRender: true,
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
- export {};
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]) return;
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);
@@ -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
- await openSearchDialog();
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) return existing.unregister;
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
- registration.disposePage?.();
57
- registration.disposePage = undefined;
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
- registration.disposePage = mount() || undefined;
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
- console.error("[stalux photoswipe] 初始化失败", error);
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)) return [];
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) return;
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
  });
@@ -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
  }
@@ -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
- console.error("Failed to decode Waline config:", error);
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
- console.warn("Waline serverURL not configured");
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
  });
@@ -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) return null;
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
- console.warn(`[stalux/webmcp] 注册失败: ${tool.name}`, e);
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().then((r) => {
576
- if (r.reason === "unsupported") return; // 无 WebMCP 环境,静默跳过
577
- if (r.registered.length) {
578
- console.info(
579
- "[stalux/webmcp] 已注册 " +
580
- r.registered.length +
581
- " 个工具: " +
582
- r.registered.join(", "),
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
  }