PageSourceSearch

https://dongjunke.cn/js/webmcp.js

js dongjunke.cn collected 2026-10-03 23:19:08 UTC 7,752 bytes, 265 lines download raw bytes

1(function () {
2  var modelContext = document.modelContext || navigator.modelContext;
3
4  if (!modelContext || typeof modelContext.registerTool !== 'function' || window.__dongjunkeWebMCP) {
5    return;
6  }
7
8  window.__dongjunkeWebMCP = true;
9
10  var searchEntriesPromise;
11
12  function result(value) {
13    return JSON.stringify(value);
14  }
15
16  function articleTitle() {
17    var title = document.querySelector('.post-content > h1');
18    return title ? title.innerText.trim() : document.title;
19  }
20
21  function pageType() {
22    var path = window.location.pathname;
23
24    if (path === '/' || path === '') {
25      return 'home';
26    }
27
28    if (document.querySelector('.post-content .markdown-body')) {
29      return 'article';
30    }
31
32    if (/^\/archives/.test(path)) {
33      return 'archives';
34    }
35
36    if (/^\/categories/.test(path)) {
37      return 'categories';
38    }
39
40    if (/^\/tags/.test(path)) {
41      return 'tags';
42    }
43
44    return 'page';
45  }
46
47  function htmlToText(html) {
48    var wrapper = document.createElement('div');
49    wrapper.innerHTML = html;
50    return (wrapper.textContent || '').replace(/\s+/g, ' ').trim();
51  }
52
53  function loadSearchEntries() {
54    if (!searchEntriesPromise) {
55      searchEntriesPromise = fetch('/local-search.xml', {
56        credentials: 'same-origin'
57      }).then(function (response) {
58        if (!response.ok) {
59          throw new Error('无法读取博客搜索索引');
60        }
61        return response.text();
62      }).then(function (xml) {
63        var documentNode = new DOMParser().parseFromString(xml, 'text/xml');
64
65        return Array.prototype.map.call(documentNode.querySelectorAll('entry'), function (entry) {
66          var title = entry.querySelector('title');
67          var url = entry.querySelector('url');
68          var link = entry.querySelector('link');
69          var content = entry.querySelector('content');
70          var categories = Array.prototype.map.call(entry.querySelectorAll('category'), function (category) {
71            return category.textContent.trim();
72          });
73
74          return {
75            title: title ? title.textContent.trim() : '',
76            url: url ? url.textContent.trim() : (link ? link.getAttribute('href') : ''),
77            content: content ? htmlToText(content.textContent) : '',
78            categories: categories
79          };
80        });
81      });
82    }
83
84    return searchEntriesPromise;
85  }
86
87  function register(tool) {
88    modelContext.registerTool(tool).catch(function (error) {
89      console.warn('[dongjunke-webmcp] tool registration failed', error);
90    });
91  }
92
93  register({
94    name: 'get_site_context',
95    description: '获取东评西就当前页面的标题、地址和页面类型',
96    inputSchema: {
97      type: 'object',
98      properties: {}
99    },
100    execute: async function () {
101      return result({
102        site: '东评西就',
103        url: window.location.href,
104        title: document.title,
105        pageType: pageType(),
106        description: (document.querySelector('meta[name="description"]') || {}).content || ''
107      });
108    }
109  });
110
111  register({
112    name: 'read_current_article',
113    description: '读取东评西就当前文章的标题、地址和正文',
114    inputSchema: {
115      type: 'object',
116      properties: {}
117    },
118    execute: async function () {
119      var content = document.querySelector('.post-content .markdown-body');
120
121      return result({
122        title: articleTitle(),
123        url: window.location.href,
124        content: content ? content.innerText.trim().slice(0, 20000) : '',
125        available: Boolean(content)
126      });
127    }
128  });
129
130  register({
131    name: 'search_posts',
132    description: '按关键词搜索东评西就的文章,支持多个关键词同时匹配',
133    inputSchema: {
134      type: 'object',
135      properties: {
136        query: {
137          type: 'string',
138          description: '要搜索的关键词,可以用空格分隔多个关键词'
139        },
140        limit: {
141          type: 'integer',
142          description: '最多返回多少篇文章,默认 10,最多 20'
143        }
144      },
145      required: ['query']
146    },
147    execute: async function (input) {
148      var query = String((input || {}).query || '').trim().toLowerCase();
149      var limit = Math.min(Math.max(Number((input || {}).limit) || 10, 1), 20);
150
151      if (!query) {
152        return result({ query: '', results: [] });
153      }
154
155      var terms = query.split(/\s+/).filter(Boolean);
156      var entries = await loadSearchEntries();
157      var results = entries.map(function (entry) {
158        var searchable = (entry.title + ' ' + entry.content + ' ' + entry.categories.join(' ')).toLowerCase();
159        var matches = terms.filter(function (term) {
160          return searchable.indexOf(term) !== -1;
161        }).length;
162
163        return {
164          entry: entry,
165          matches: matches,
166          titleMatch: entry.title.toLowerCase().indexOf(query) !== -1
167        };
168      }).filter(function (item) {
169        return item.matches === terms.length;
170      }).sort(function (left, right) {
171        return Number(right.titleMatch) - Number(left.titleMatch) || right.matches - left.matches;
172      }).slice(0, limit).map(function (item) {
173        return {
174          title: item.entry.title,
175          url: new URL(item.entry.url, window.location.origin).href,
176          categories: item.entry.categories,
177          excerpt: item.entry.content.slice(0, 400)
178        };
179      });
180
181      return result({ query: query, results: results });
182    }
183  });
184
185  register({
186    name: 'list_recent_posts',
187    description: '列出东评西就最近发布的文章',
188    inputSchema: {
189      type: 'object',
190      properties: {
191        limit: {
192          type: 'integer',
193          description: '返回数量,默认 10,最多 20'
194        }
195      }
196    },
197    execute: async function (input) {
198      var limit = Math.min(Math.max(Number((input || {}).limit) || 10, 1), 20);
199      var entries = await loadSearchEntries();
200
201      return result(entries.slice(0, limit).map(function (entry) {
202        return {
203          title: entry.title,
204          url: new URL(entry.url, window.location.origin).href,
205          categories: entry.categories,
206          excerpt: entry.content.slice(0, 240)
207        };
208      }));
209    }
210  });
211
212  register({
213    name: 'list_categories',
214    description: '列出东评西就的文章分类及文章数量',
215    inputSchema: {
216      type: 'object',
217      properties: {}
218    },
219    execute: async function () {
220      var entries = await loadSearchEntries();
221      var counts = {};
222
223      entries.forEach(function (entry) {
224        entry.categories.forEach(function (category) {
225          counts[category] = (counts[category] || 0) + 1;
226        });
227      });
228
229      return result(Object.keys(counts).sort().map(function (name) {
230        return {
231          name: name,
232          count: counts[name],
233          url: new URL('/categories/' + encodeURIComponent(name) + '/', window.location.origin).href
234        };
235      }));
236    }
237  });
238
239  register({
240    name: 'navigate_site',
241    description: '跳转到东评西就站内页面或文章,不允许跳转到外部网站',
242    inputSchema: {
243      type: 'object',
244      properties: {
245        path: {
246          type: 'string',
247          description: '站内路径,例如 /about 或文章路径'
248        }
249      },
250      required: ['path']
251    },
252    execute: async function (input) {
253      var target = new URL(String((input || {}).path || ''), window.location.origin);
254
255      if (target.origin !== window.location.origin) {
256        throw new Error('只能跳转到东评西就站内页面');
257      }
258
259      window.location.assign(target.href);
260      return result({ url: target.href });
261    }
262  });
263
264  console.info('[dongjunke-webmcp] tools registered');
265})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.