PageSourceSearch

https://coffee-labo.co.jp/wp-content/cache/min/1/wp-content/plugin…/js/coffee-labo-webmcp.js?ver=1790866390

js coffee-labo.co.jp collected 2026-10-02 10:35:46 UTC 11,255 bytes, 13 lines download raw bytes

1(()=>{'use strict';const config=window.CoffeeLaboWebMCPConfig||{};const TOOL_NAMES=['search_coffee_articles','get_coffee_article','browse_coffee_topics','answer_coffee_question',];const OUTPUT_BUDGET=1450;const state={initialized:!1,supported:!1,registered:[],error:null,};let initializationPromise=null;const clampText=(value,max=OUTPUT_BUDGET)=>{const text=String(value??'').trim();return text.length<=max?text:`${text.slice(0, Math.max(1, max - 1)).trimEnd()}…`};const normalizeInteger=(value,fallback,min,max)=>{const parsed=Number.parseInt(value,10);if(!Number.isFinite(parsed))return fallback;return Math.max(min,Math.min(max,parsed))};const pushMetric=(toolName,status,resultCount=0)=>{if(!config.analytics)return;const analytics=window.__clwmcpAnalyticsRuntime;if(!analytics||typeof analytics.send!=='function')return;analytics.send('webmcp_tool_call',{webmcp_tool_name:toolName,webmcp_status:status,webmcp_result_count:Number(resultCount)||0,})};const fetchWithTimeout=async(url,options={},timeoutMs=38000)=>{const controller=new AbortController();const timer=window.setTimeout(()=>controller.abort(),timeoutMs);try{return await fetch(url,{...options,signal:controller.signal})}catch(error){if(error?.name==='AbortError'){throw new Error('Request timed out.')}
2throw error}finally{window.clearTimeout(timer)}};const apiRequest=async(path,params={})=>{const base=String(config.restBase||'');if(!base)throw new Error('WebMCP REST endpoint is not configured.');const url=new URL(path.replace(/^\//,''),base);Object.entries(params).forEach(([key,value])=>{if(value!==undefined&&value!==null&&value!==''){url.searchParams.set(key,String(value))}});const response=await fetchWithTimeout(url.toString(),{method:'GET',credentials:'same-origin',headers:{Accept:'application/json'},},15000);let payload=null;try{payload=await response.json()}catch(_){}
3if(!response.ok){throw new Error(payload?.message||`Request failed (${response.status}).`)}
4return payload};const apiPost=async(urlValue,body={})=>{const url=String(urlValue||'');if(!url)throw new Error('AI answer endpoint is not configured.');const response=await fetchWithTimeout(url,{method:'POST',credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json',},body:JSON.stringify(body),},42000);let payload=null;try{payload=await response.json()}catch(_){}
5if(!response.ok){throw new Error(payload?.message||`Request failed (${response.status}).`)}
6return payload};const formatSearch=(data)=>{const items=Array.isArray(data?.items)?data.items:[];if(!items.length){return `「${data?.query || ''}」に一致する公開記事は見つかりませんでした。別の語句またはbrowse_coffee_topicsのテーマで再検索してください。`}
7const lines=[`検索結果: 全${data.total_found ?? items.length}件中${items.length}件`];items.forEach((item,index)=>{const topics=Array.isArray(item.categories)?item.categories.map((term)=>term.name).filter(Boolean).slice(0,3).join('、'):'';lines.push(`${index + 1}. [ID ${item.id}] ${item.title}`,`URL: ${item.url}`,topics?`テーマ: ${topics}`:'',item.summary?`要約: ${item.summary}`:'',item.modified?`更新: ${item.modified}`:'')});return clampText(lines.filter(Boolean).join('\n'))};const formatArticle=(data)=>{const topics=Array.isArray(data?.categories)?data.categories.map((term)=>term.name).filter(Boolean).join('、'):'';return clampText([data?.title||'記事',data?.url?`URL: ${data.url}`:'',data?.author?`著者: ${data.author}`:'',topics?`テーマ: ${topics}`:'',data?.modified?`更新: ${data.modified}`:'',data?.content?`本文抜粋: ${data.content}`:(data?.excerpt?`要約: ${data.excerpt}`:''),data?.truncated?'注: 本文はWebMCPの出力上限に合わせて抜粋しています。':'',].filter(Boolean).join('\n'))};const formatTopics=(data)=>{const items=Array.isArray(data?.items)?data.items:[];if(!items.length)return'一致する記事テーマは見つかりませんでした。';return clampText([`記事テーマ ${items.length}件:`,...items.map((item)=>`- ${item.name}(${item.count}記事 / slug: ${item.slug}) ${item.url}`),].join('\n'))};const formatAnswer=(data)=>{const sources=Array.isArray(data?.sources)?data.sources:[];const lines=[data?.answer||'回答を取得できませんでした。'];if(data?.response_mode==='free_fallback')lines.push('注: AI回答の代わりに記事本文の関連箇所を表示しています。');if(data?.safety_note)lines.push(`注意: ${data.safety_note}`);if(sources.length){lines.push('参考記事:');
7sources.forEach((source,index)=>{lines.push(`${index + 1}. ${source.title} ${source.url}`)})}
8if(data?.shop_cta?.url){lines.push(`${data.shop_cta.label || 'ラボカフェを見る'}: ${data.shop_cta.url}`)}
9return clampText(lines.join('\n'))};const tools=[{name:'search_coffee_articles',description:'コーヒー豆研究所の公開記事をキーワードで検索します。コーヒー豆、器具、淹れ方、カフェ、健康、スターバックスなどの解説記事を探すときに使います。',inputSchema:{type:'object',additionalProperties:!1,properties:{query:{type:'string',minLength:2,maxLength:100,description:'利用者が知りたい内容を自然な日本語で指定します。',},topic:{type:'string',description:'任意。browse_coffee_topicsで得たテーマ名またはslugです。',},sort:{type:'string',enum:['relevance','newest','updated'],description:'関連度、新着、更新日のいずれか。既定は関連度です。',},limit:{type:'integer',minimum:1,maximum:5,description:'返す記事数。1〜5件です。',},},required:['query'],},execute:async({query,topic='',sort='relevance',limit=config.maxResults||5})=>{const toolName='search_coffee_articles';try{const data=await apiRequest('search',{q:String(query||'').trim(),topic:String(topic||'').trim(),sort,limit:normalizeInteger(limit,config.maxResults||5,1,5),});pushMetric(toolName,'success',data?.returned||0);return formatSearch(data)}catch(error){pushMetric(toolName,'error',0);throw new Error(`記事検索に失敗しました: ${error.message}`)}},annotations:{readOnlyHint:!0,untrustedContentHint:!0},},{name:'get_coffee_article',description:'コーヒー豆研究所の公開記事1件から、タイトル、URL、著者、更新日、カテゴリ、本文抜粋を取得します。検索結果を詳しく確認するときに使います。',inputSchema:{type:'object',additionalProperties:!1,properties:{article:{type:'string',maxLength:500,description:'記事ID、記事slug、またはcoffee-labo.co.jpの記事URLです。',},max_chars:{type:'integer',minimum:500,maximum:1200,description:'本文抜粋の最大文字数。既定はサイト設定値です。',},},required:['article'],},execute:async({article,max_chars=config.maxArticleChars||1000})=>{const toolName='get_coffee_article';try{const data=await apiRequest('article',{article:String(article||'').trim(),max_chars:normalizeInteger(max_chars,config.maxArticleChars||1000,500,1200),});pushMetric(toolName,'success',1);return formatArticle(data)}catch(error){pushMetric(toolName,'error',0);throw new Error(`記事取得に失敗しました: ${error.message}`)}},annotations:{readOnlyHint:!0,untrustedContentHint:!0},},{name:'browse_coffee_topics',description:'コーヒー豆研究所の記事カテゴリを、記事数、slug、カテゴリURLとともに表示します。検索対象のテーマを決めるときに使います。',inputSchema:{type:'object',additionalProperties:!1,properties:{query:{type:'string',maxLength:50,description:'任意。カテゴリ名を絞る語句です。例: 器具、スタバ、淹れ方。',},limit:{type:'integer',minimum:1,maximum:12,description:'返すテーマ数。1〜12件です。',},},},execute:async({query='',limit=12}={})=>{const toolName='browse_coffee_topics';try{const data=await apiRequest('topics',{q:String(query||'').trim(),limit:normalizeInteger(limit,12,1,12),});pushMetric(toolName,'success',data?.returned||0);return formatTopics(data)}catch(error){pushMetric(toolName,'error',0);throw new Error(`テーマ取得に失敗しました: ${error.message}`)}},annotations:{readOnlyHint:!0,untrustedContentHint:!1},},{name:'answer_coffee_question',description:config.aiWebMcpMode==='openai'?'コーヒー豆研究所の公開記事だけを根拠に、コーヒーに関する質問へ短く回答し、参考記事を提示します。現在の記事がある場合はその内容を優先します。':'コーヒー豆研究所の公開記事から質問に関連する本文箇所と参考記事を提示します。外部の生成AIは使用しません。',inputSchema:{type:'object',additionalProperties:!1,properties:{question:{type:'string',minLength:6,maxLength:300,description:'利用者のコーヒーに関する質問です。',},},required:['question'],},execute:async({question})=>{const toolName='answer_coffee_question';if(!config.aiEnabled)throw new Error('サイト内の質問・検索機能は現在無効です。');try{const data=await apiPost(config.aiRestUrl,{question:String(question||'').trim(),post_id:Number(config.aiPostId||0),token:String(config.aiToken||''),channel:'webmcp',});pushMetric(toolName,'success',Array.isArray(data?.sources)?data.sources.length:0);return formatAnswer(data)}catch(error){pushMetric(toolName,'error',0);throw new Error(`記事からの回答取得に失敗しました: ${error.message}`)}},annotations:{readOnlyHint:!0,untrustedContentHint:!0},},];const activeTools=config.aiEnabled?tools:tools.filter((tool)=>tool.name!=='answer_coffee_question');const activeToolNames=activeTools.map((tool)=>tool.name);const waitForModelContext=async()=>{for(let attempt=0;attempt<20;attempt+=1){if(document.modelContext?.registerTool)return document.modelContext;await new Promise((resolve)=>window.setTimeout(resolve,250))}
10return null};const initialize=()=>{if(initializationPromise)return initializationPromise;initializationPromise=(async()=>{state.initialized=!0;try{const modelContext=await waitForModelContext();if(!modelContext){state.supported=!1;return state}
11state.supported=!0;for(const tool of activeTools){await modelContext.registerTool(tool);
11state.registered.push(tool.name)}}catch(error){state.error=error instanceof Error?error.message:String(error);console.error('[Coffee Labo WebMCP]',error)}
12return state})();return initializationPromise};window.CoffeeLaboWebMCP=Object.freeze({version:config.version||'0.3.9',toolNames:[...activeToolNames],initialize,getStatus:async()=>{await initialize();const available=document.modelContext?.getTools?(await document.modelContext.getTools()).filter((tool)=>activeToolNames.includes(tool.name)).map((tool)=>tool.name):[];return{...state,available}},execute:async(name,args={})=>{await initialize();if(!document.modelContext?.getTools||!document.modelContext?.executeTool){throw new Error('このブラウザではWebMCPを実行できません。')}
13const available=await document.modelContext.getTools();const tool=available.find((candidate)=>candidate.name===name&&activeToolNames.includes(candidate.name));if(!tool)throw new Error(`WebMCP tool not found: ${name}`);return document.modelContext.executeTool(tool,JSON.stringify(args))},});void initialize()})()

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.