オンボーディング管理

1. 開通準備

現在の組織 / account

この画面で操作する対象を確認します。複数組織の切り替えは既存の最小画面に任せ、ここでは現在の接続先を表示します。

公開URL / 設置タグ

証明書ACTIVE後に使う正規URLです。Widgetタグはscript配信をwidget.chat、会話APIをapi.chatへ向けます。

招待 / 申込レビュー

owner専用です。managerで実行すると403になり、owner権限が必要です。
2. ナレッジ投入

FAQ生成 / 確認

URL・テキスト・CSVからFAQ案を作り、承認後に検索へ反映します。ここでは既存画面への導線だけを整理し、FAQ生成APIの仕様は変えません。

FAQ生成URLや本文からFAQ案を生成します。
FAQ確認承認待ち・公開中FAQを確認します。
検索品質FAQ候補履歴と聞き返し指標を確認します。
3. 窓口プロンプト調整

窓口プロンプト管理 / 評価

顧客・Agentごとの接客方針を編集し、保存前にテスト質問で出力形式を確認します。反映はchange set下書き作成後、下の承認パネルで実行します。

現在文
未取得
候補テスト結果
未実行
4. Widget設置

Widget設置 / タグ検知

顧客サイトへ貼るタグと、最後に検知した設置Originを確認します。embed_keyのrotationやaccount停止は既存API側の運用操作に残します。

5. Claude接続 / テスト

Claude連携 / Remote MCP

Claude連携に登録するURLと、公開toolの確認です。MCPからは読み取りとchange set下書き作成だけを公開します。

Claude連携URL
OAuth
未確認
実コネクタ登録画面での接続確認は、ユーザー側のClaude画面で実施します。
6. 運用 / 安全管理

BYOK

顧客持ち込みのAnthropic APIキーを検証・保存します。キー平文は保存後に表示しません。

管理APIキー / Claude連携

管理APIキーの一覧・発行・失効はowner専用です。managerで実行すると403になり、owner権限が必要です。Claude連携の再認可は既存どおりmanagerでも使えます。
未確認

Change set 承認

Claude/MCPが作成した下書きを人が確認してから反映します。破棄・rollbackを含む破壊的操作は確認ダイアログを残します。

Before / rollback input
未選択
After / apply input
未選択
'; } function idempotencyKey(prefix) { if (window.crypto && typeof window.crypto.randomUUID === 'function') return prefix + '-' + window.crypto.randomUUID(); return prefix + '-' + Date.now() + '-' + Math.random().toString(16).slice(2); } function fmt(ts) { if (!ts) return '-'; try { return new Date(ts).toLocaleString('ja-JP', { month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit' }); } catch { return String(ts); } } function json(v) { return JSON.stringify(v == null ? null : v, null, 2); } async function fetchJson(url, opts) { const res = await fetch(url, opts); const body = await res.json().catch(() => null); if (!res.ok || !body || body.success === false) { const msg = body && body.error && body.error.message ? body.error.message : 'HTTP ' + res.status; const hint = res.status === 403 && ownerOnlyEndpoint(url) ? ' owner権限が必要です。' : ''; const err = new Error(msg + hint); err.status = res.status; err.code = body && body.error && body.error.code ? body.error.code : undefined; throw err; } return body.data; } function ownerOnlyEndpoint(url) { return url.indexOf('/v1/admin/api-keys') === 0 || url.indexOf('/v1/admin/invitations') === 0 || url.indexOf('/v1/admin/signup-requests') === 0; } function badge(label, kind) { return '' + esc(label) + ''; } function valueStatus(done, warn) { if (done) return { label: '完了', kind: 'ok' }; if (warn) return { label: '要確認', kind: 'warn' }; return { label: '未確認', kind: '' }; } function renderSteps() { const progress = onboardingState.progress || {}; const mcp = valueStatus(mcpToolList.length > 0 || progress.mcp === 'connected'); const knowledge = valueStatus(progress.knowledge === 'completed' || progress.knowledge_ingestion === 'completed', progress.knowledge === 'blocked'); const faq = valueStatus(progress.faq === 'reviewed' || progress.faq_review === 'completed', progress.faq === 'blocked'); const widget = valueStatus(accountStatus && accountStatus.widget_installed, accountStatus && accountStatus.allowed_origins && accountStatus.allowed_origins.length === 0); const testChat = valueStatus(conversations.length > 0 || progress.test_chat === 'completed'); const launch = valueStatus(accountStatus && accountStatus.status === 'active' && widget.label === '完了', byokState && byokState.enabled && byokState.validation_status !== 'valid'); const stateByField = { mcp, knowledge, faq, widget, test_chat: testChat, launch }; $('steps').innerHTML = STEPS.map((step, i) => { const state = stateByField[step.field] || valueStatus(false); const active = selectedStep === step.id ? ' active' : ''; const sub = step.id === onboardingState.current_step ? '保存中の現在Step' : 'クリックで現在Stepに設定'; return ''; }).join(''); document.querySelectorAll('.step').forEach((el) => el.addEventListener('click', () => { selectedStep = el.getAttribute('data-step') || 'mcp_connector'; renderSteps(); })); $('onboardingStatus').value = onboardingState.status || 'not_started'; } function renderAccount() { const a = accountStatus; if (!a) { $('accountMeta').innerHTML = [ ['組織', meState ? meState.tenant_id : '-'], ['role', meState ? meState.role : '-'], ['auth', meState ? meState.auth_type : '-'], ['account', code() || '-'], ['status', '未取得'], ].map(([k, v]) => '
' + esc(k) + '' + esc(v) + '
').join(''); $('embedTag').value = ''; renderPublicUrls(); renderTenantBadge(); return; } $('accountMeta').innerHTML = [ ['組織ID', meState ? meState.tenant_id : '-'], ['role', meState ? meState.role : '-'], ['auth type', meState ? meState.auth_type : '-'], ['account_code', a.account_code], ['account status', a.status], ['widget installed', a.widget_installed ? 'yes' : 'no'], ['widget seen at', fmt(a.widget_seen_at)], ['widget seen origin', a.widget_seen_origin || '-'], ['allowed origins', (a.allowed_origins || []).join('\n') || '-'], ].map(([k, v]) => '
' + esc(k) + '' + esc(v) + '
').join(''); $('embedTag').value = a.embed_tag || ''; renderPublicUrls(); renderTenantBadge(); } function renderPublicUrls() { const embedTag = accountStatus && accountStatus.embed_tag ? accountStatus.embed_tag : fallbackEmbedTag(); const values = [ ['現在の公開URL', adminPublicBase()], ['Widget CDN URL', widgetPublicBase()], ['API URL', apiPublicBase()], ['MCP URL', mcpExternalBase()], ['Widget config URL', widgetConfigUrl()], ]; $('publicUrlsMeta').innerHTML = values.map(([k, v]) => '
' + esc(k) + '' + esc(v) + '
').join(''); $('finalEmbedTag').value = embedTag; } function renderAccountPrompt() { const current = accountPrompt && accountPrompt.ccc ? accountPrompt.ccc : ''; $('currentCcc').value = current; if (!$('draftCcc').value.trim() && current) $('draftCcc').value = current; $('promptBeforePreview').textContent = current || '未取得'; if (!promptPreviewRows.length) { $('promptAfterPreview').textContent = '未実行'; return; } $('promptAfterPreview').textContent = promptPreviewRows.map((row, index) => { const summary = row.block_summary || {}; const blocks = Array.isArray(summary.block_types) ? summary.block_types.join(', ') : '-'; return [ '#' + (index + 1) + ' ' + row.question, 'outcome=' + (row.outcome || '-') + ' / blocks=' + blocks + ' / ui=' + (summary.ui_block_count || 0), row.text || '', '', JSON.stringify(row.blocks || [], null, 2), ].join('\n'); }).join('\n\n---\n\n'); } function renderTenantBadge() { if (!meState) { $('tenantBadge').textContent = '組織未確認'; return; } $('tenantBadge').textContent = '組織: ' + meState.tenant_id + ' / role: ' + meState.role + ' / ' + meState.auth_type; } function renderMcp() { $('mcpUrl').textContent = mcpExternalBase(); $('oauthUrl').textContent = mcpOAuthUrl(); if (!mcpToolList.length) return; $('mcpTools').textContent = mcpToolList.map((t) => '- ' + (t.name || t)).join('\n'); $('mcpStatus').textContent = '公開tool ' + mcpToolList.length + '件を取得しました。'; } function renderAdminKeys() { if (!adminKeys.length) { $('adminKeys').textContent = '未確認'; return; } $('adminKeys').textContent = adminKeys.map((key) => [ key.active ? 'active' : 'revoked', key.role, key.name || '(no name)', key.key_hint ? 'hint=' + key.key_hint : 'hint=-', key.id, 'created=' + fmt(key.created_at), 'revoked=' + fmt(key.revoked_at), ].join(' / ')).join('\n'); } function renderInvitations() { const el = $('invitationList'); if (!tenantInvitations.length) { el.innerHTML = '
招待はありません。
'; return; } el.innerHTML = tenantInvitations.map((inv) => { const actions = inv.status === 'pending' ? '
' : ''; const kind = inv.status === 'accepted' ? 'ok' : inv.status === 'revoked' ? 'bad' : 'warn'; return '
' + esc(inv.email) + '' + badge(inv.status, kind) + '
role=' + esc(inv.role) + ' / expires=' + esc(fmt(inv.expires_at)) + ' / invited_by=' + esc(inv.invited_by_id || '-') + ' / id=' + esc(inv.id) + '
' + actions + '
'; }).join(''); document.querySelectorAll('.revoke-invitation').forEach((el) => el.addEventListener('click', () => revokeInvitation(el.getAttribute('data-id') || ''))); } function renderSignupRequests() { const el = $('signupRequestList'); if (!signupRequests.length) { el.innerHTML = '
申込はありません。
'; return; } el.innerHTML = signupRequests.map((req) => { const kind = req.status === 'reviewed' ? 'ok' : req.status === 'rejected' ? 'bad' : 'warn'; const origins = Array.isArray(req.allowed_origins) && req.allowed_origins.length ? req.allowed_origins.join(', ') : '-'; const provision = req.status === 'reviewed' ? '' : ''; return '
' + esc(req.business_name) + '' + badge(req.status, kind) + '
owner=' + esc(req.owner_email) + ' / requested_code=' + esc(req.requested_account_code || '-') + ' / origins=' + esc(origins) + ' / created=' + esc(fmt(req.created_at)) + ' / id=' + esc(req.id) + '
' + provision + '
'; }).join(''); document.querySelectorAll('.signup-status').forEach((el) => el.addEventListener('click', () => updateSignupRequestStatus(el.getAttribute('data-id') || '', el.getAttribute('data-status') || 'pending'))); document.querySelectorAll('.signup-provision').forEach((el) => el.addEventListener('click', () => provisionSignupRequest(el.getAttribute('data-id') || ''))); } function changeSetAllowedActions(status) { return { apply: status === 'draft', discard: status === 'draft', rollback: status === 'applied', }; } function syncChangeSetActions() { const actions = changeSetAllowedActions(selectedChangeSetDetail && selectedChangeSetDetail.status); $('applyChangeSet').disabled = !actions.apply; $('discardChangeSet').disabled = !actions.discard; $('rollbackChangeSet').disabled = !actions.rollback; } function renderByok() { const b = byokState || { provider: 'anthropic', enabled: false }; $('byokMeta').innerHTML = [ ['provider', b.provider || 'anthropic'], ['enabled', b.enabled ? 'yes' : 'no'], ['key hint', b.key_hint || '-'], ['validation', b.validation_status || '-'], ['last validated', fmt(b.last_validated_at)], ['updated', fmt(b.updated_at)], ].map(([k, v]) => '
' + esc(k) + '' + esc(v) + '
').join(''); } function renderChangeSets() { if (!changeSets.length) { $('changeList').innerHTML = '
change setはありません。
'; syncChangeSetActions(); return; } $('changeList').innerHTML = changeSets.map((cs) => { const active = cs.id === selectedChangeSetId ? ' active' : ''; const targetTypes = Array.isArray(cs.items) ? Array.from(new Set(cs.items.map((item) => item && item.target_type).filter(Boolean))).join(', ') : '-'; return ''; }).join(''); document.querySelectorAll('.change-row').forEach((el) => el.addEventListener('click', () => loadChangeSet(el.getAttribute('data-id') || ''))); } function renderSelectedChangeSet() { const d = selectedChangeSetDetail; if (!d) { $('changeMeta').innerHTML = '
selected未選択
'; $('diffBefore').textContent = '未選択'; $('diffAfter').textContent = '未選択'; syncChangeSetActions(); return; } const itemCount = Array.isArray(d.items) ? d.items.length : 0; const targetTypes = Array.isArray(d.items) ? Array.from(new Set(d.items.map((item) => item && item.target_type).filter(Boolean))).join(', ') : '-'; const actions = changeSetAllowedActions(d.status); const availableActions = [ actions.apply ? 'apply' : '', actions.discard ? 'discard' : '', actions.rollback ? 'rollback' : '', ].filter(Boolean).join(', ') || '-'; $('changeMeta').innerHTML = [ ['id', d.id], ['status', d.status], ['available_actions', availableActions], ['items', String(itemCount)], ['target_types', targetTypes || '-'], ['created_by', (d.created_by_type || '-') + ':' + (d.created_by_id || '-')], ['approved_by', d.approved_by_id || '-'], ['applied_at', fmt(d.applied_at)], ['rolled_back_at', fmt(d.rolled_back_at)], ['description', d.description || '-'], ].map(([k, v]) => '
' + esc(k) + '' + esc(v) + '
').join(''); const items = selectedChangeSetDiff && selectedChangeSetDiff.items ? selectedChangeSetDiff.items : d.items || []; $('diffBefore').textContent = json(items.map((item) => ({ target_type: item.target_type, target_id: item.target_id, operation: item.operation, before: item.before, }))); $('diffAfter').textContent = json(items.map((item) => ({ target_type: item.target_type, target_id: item.target_id, operation: item.operation, after: item.after, }))); syncChangeSetActions(); } async function refreshAll() { saveInputs(); if (!code()) { $('progressStatus').textContent = 'account_codeを入力してください。APIキー空欄ならログインsessionを使います。'; $('progressStatus').className = 'status error'; return; } $('progressStatus').textContent = '読み込み中...'; $('progressStatus').className = 'status'; const headers = authHeaders(); const settled = await Promise.allSettled([ fetchJson(adminBase() + '/status', { headers }), fetchJson(adminBase() + '/onboarding', { headers }), fetchJson(mcpBase() + '/tools', { headers }), fetchJson(adminBase() + '/llm-credential', { headers }), fetchJson(adminBase() + '/conversations', { headers }), fetchJson(adminBase() + '/change-sets', { headers }), fetchJson(adminKeysBase(), { headers }), fetchJson('/v1/admin/me', { headers }), fetchJson(promptBase(), { headers }), ]); if (settled[0].status === 'fulfilled') accountStatus = settled[0].value; if (settled[1].status === 'fulfilled') { onboardingState = settled[1].value || onboardingState; selectedStep = onboardingState.current_step || selectedStep; } if (settled[2].status === 'fulfilled') mcpToolList = Array.isArray(settled[2].value.tools) ? settled[2].value.tools : Array.isArray(settled[2].value) ? settled[2].value : []; if (settled[3].status === 'fulfilled') byokState = settled[3].value; if (settled[4].status === 'fulfilled') conversations = Array.isArray(settled[4].value) ? settled[4].value : []; if (settled[5].status === 'fulfilled') changeSets = Array.isArray(settled[5].value) ? settled[5].value : []; if (settled[6].status === 'fulfilled') adminKeys = Array.isArray(settled[6].value) ? settled[6].value : []; if (settled[7].status === 'fulfilled') meState = settled[7].value; if (settled[8].status === 'fulfilled') accountPrompt = settled[8].value; const errors = settled.filter((r) => r.status === 'rejected').map((r) => r.reason && r.reason.message ? r.reason.message : String(r.reason)); renderAccount(); renderMcp(); renderByok(); renderAdminKeys(); renderAccountPrompt(); renderChangeSets(); renderSelectedChangeSet(); renderSteps(); $('progressStatus').textContent = errors.length ? '一部取得できませんでした: ' + errors.join(' / ') : '状態を更新しました。'; $('progressStatus').className = errors.length ? 'status error' : 'status oktext'; } async function callMcpTool(name, args) { saveInputs(); const res = await fetch(mcpBase(), { method: 'POST', headers: authHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ jsonrpc: '2.0', id: 'admin-smoke-' + name, method: 'tools/call', params: { name, arguments: args || {} } }), }); const body = await res.json().catch(() => null); if (!res.ok || !body || body.error) { const msg = body && body.error && body.error.message ? body.error.message : 'HTTP ' + res.status; throw new Error(msg); } return body.result; } async function smokeMcp(kind) { const map = { search: { name: 'search_faqs', args: { query: '営業時間', limit: 3 } }, usage: { name: 'get_recent_usage', args: {} }, draft: { name: 'create_change_set', args: { title: 'MCP smoke draft ' + new Date().toISOString(), description: 'Created from onboarding smoke test', items: [] } }, }; const target = map[kind]; if (!target) return; if (kind === 'draft' && !confirm('空のchange set下書きを作成しますか?')) return; $('mcpStatus').textContent = target.name + ' 実行中...'; $('mcpStatus').className = 'status'; try { const result = await callMcpTool(target.name, target.args); $('mcpTools').textContent = json(result); $('mcpStatus').textContent = target.name + ' が成功しました。'; $('mcpStatus').className = 'status oktext'; if (kind === 'draft') await loadChangeSets(); } catch (err) { $('mcpStatus').textContent = err.message || String(err); $('mcpStatus').className = 'status error'; } } async function loadAdminKeys() { saveInputs(); $('adminKeyStatus').textContent = 'キー一覧取得中...'; $('adminKeyStatus').className = 'status'; try { adminKeys = await fetchJson(adminKeysBase(), { headers: authHeaders() }); if (!Array.isArray(adminKeys)) adminKeys = []; renderAdminKeys(); $('adminKeyStatus').textContent = 'キー一覧を取得しました。'; $('adminKeyStatus').className = 'status oktext'; } catch (err) { $('adminKeyStatus').textContent = err.message || String(err); $('adminKeyStatus').className = 'status error'; } } async function loadInvitations() { saveInputs(); $('memberOpsStatus').textContent = '招待一覧取得中...'; $('memberOpsStatus').className = 'status'; try { tenantInvitations = await fetchJson(invitationsBase(), { headers: authHeaders() }); if (!Array.isArray(tenantInvitations)) tenantInvitations = []; renderInvitations(); $('memberOpsStatus').textContent = '招待一覧を取得しました。'; $('memberOpsStatus').className = 'status oktext'; } catch (err) { $('memberOpsStatus').textContent = err.message || String(err); $('memberOpsStatus').className = 'status error'; } } async function createInvitation() { saveInputs(); const email = $('inviteEmail').value.trim(); const role = $('inviteRole').value; const expires = Number($('inviteExpires').value || '7'); if (!email) { $('memberOpsStatus').textContent = '招待メールアドレスを入力してください。'; $('memberOpsStatus').className = 'status error'; return; } if (!confirm(email + ' を ' + role + ' として招待しますか?')) return; $('memberOpsStatus').textContent = '招待作成中...'; try { const data = await fetchJson(invitationsBase(), { method: 'POST', headers: authHeaders({ 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey('invite') }), body: JSON.stringify({ email, role, expires_in_days: Number.isFinite(expires) ? expires : 7 }), }); $('inviteToken').value = data.accept_url || data.invite_token || ''; $('inviteToken').type = 'text'; $('toggleInviteToken').textContent = '非表示'; const delivery = invitationEmailDeliveryText(data.email_delivery); $('memberOpsStatus').textContent = '招待を作成しました。' + delivery.message + ' accept URLは一度だけ表示されます。'; $('memberOpsStatus').className = delivery.ok ? 'status oktext' : 'status error'; await loadInvitations(); } catch (err) { $('memberOpsStatus').textContent = err.message || String(err); $('memberOpsStatus').className = 'status error'; } } async function revokeInvitation(id) { saveInputs(); if (!id) return; if (!confirm('招待 ' + id + ' を失効しますか?')) return; $('memberOpsStatus').textContent = '招待失効中...'; try { await fetchJson(invitationsBase() + '/' + encodeURIComponent(id) + '/revoke', { method: 'POST', headers: authHeaders({ 'Idempotency-Key': idempotencyKey('revoke-invite') }), }); $('memberOpsStatus').textContent = '招待を失効しました。'; $('memberOpsStatus').className = 'status oktext'; await loadInvitations(); } catch (err) { $('memberOpsStatus').textContent = err.message || String(err); $('memberOpsStatus').className = 'status error'; } } async function loadSignupRequests() { saveInputs(); const status = $('signupStatusFilter').value; const url = signupRequestsBase() + (status ? '?status=' + encodeURIComponent(status) : ''); $('memberOpsStatus').textContent = '申込一覧取得中...'; $('memberOpsStatus').className = 'status'; try { signupRequests = await fetchJson(url, { headers: authHeaders() }); if (!Array.isArray(signupRequests)) signupRequests = []; renderSignupRequests(); $('memberOpsStatus').textContent = '申込一覧を取得しました。'; $('memberOpsStatus').className = 'status oktext'; } catch (err) { $('memberOpsStatus').textContent = err.message || String(err); $('memberOpsStatus').className = 'status error'; } } async function updateSignupRequestStatus(id, status) { saveInputs(); if (!id) return; if (!confirm('申込 ' + id + ' を ' + status + ' に変更しますか?')) return; $('memberOpsStatus').textContent = '申込status更新中...'; try { await fetchJson(signupRequestsBase() + '/' + encodeURIComponent(id) + '/status', { method: 'POST', headers: authHeaders({ 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey('signup-status') }), body: JSON.stringify({ status }), }); $('memberOpsStatus').textContent = '申込statusを更新しました。'; $('memberOpsStatus').className = 'status oktext'; await loadSignupRequests(); } catch (err) { $('memberOpsStatus').textContent = err.message || String(err); $('memberOpsStatus').className = 'status error'; } } async function provisionSignupRequest(id) { saveInputs(); if (!id) return; if (!confirm('申込 ' + id + ' から組織/accountを作成し、owner招待を発行しますか?')) return; $('memberOpsStatus').textContent = '開通処理中...'; try { const data = await fetchJson(signupRequestsBase() + '/' + encodeURIComponent(id) + '/provision', { method: 'POST', headers: authHeaders({ 'Idempotency-Key': idempotencyKey('signup-provision') }), }); $('inviteToken').value = data.owner_invitation && (data.owner_invitation.accept_url || data.owner_invitation.invite_token) ? (data.owner_invitation.accept_url || data.owner_invitation.invite_token) : ''; $('inviteToken').type = 'text'; $('toggleInviteToken').textContent = '非表示'; const delivery = invitationEmailDeliveryText(data.owner_invitation && data.owner_invitation.email_delivery); $('memberOpsStatus').textContent = '開通しました。account_code=' + data.account_code + ' / ' + delivery.message + ' owner招待URLは一度だけ表示されます。'; $('memberOpsStatus').className = delivery.ok ? 'status oktext' : 'status error'; if (data.embed_tag) { $('embedTag').value = data.embed_tag; renderPublicUrls(); } await loadSignupRequests(); } catch (err) { $('memberOpsStatus').textContent = err.message || String(err); $('memberOpsStatus').className = 'status error'; } } function invitationEmailDeliveryText(delivery) { if (delivery && delivery.status === 'sent') { return { ok: true, message: '招待メールを送信しました。' }; } if (delivery && delivery.status === 'failed') { return { ok: false, message: '招待メール送信に失敗しました。URLを手動共有してください。' }; } return { ok: false, message: '招待メール送信は未設定です。URLを手動共有してください。' }; } async function copyFieldValue(inputId, statusId, label) { const el = $(inputId); const value = el.value || ''; if (!value) { $(statusId).textContent = label + 'が空です。'; $(statusId).className = 'status error'; return; } try { await navigator.clipboard.writeText(value); } catch { el.focus(); el.select(); document.execCommand('copy'); } $(statusId).textContent = label + 'をコピーしました。'; $(statusId).className = 'status oktext'; } async function copyTextValue(value, statusId, label) { if (!value) { $(statusId).textContent = label + 'が空です。'; $(statusId).className = 'status error'; return; } try { await navigator.clipboard.writeText(value); $(statusId).textContent = label + 'をコピーしました。'; $(statusId).className = 'status oktext'; } catch { $(statusId).textContent = label + 'をコピーできませんでした。'; $(statusId).className = 'status error'; } } function toggleSecretField(inputId, buttonId) { const el = $(inputId); const button = $(buttonId); const hidden = el.type === 'password'; el.type = hidden ? 'text' : 'password'; button.textContent = hidden ? '非表示' : '表示'; } function clearFieldValue(inputId, statusId, label) { $(inputId).value = ''; $(statusId).textContent = label + 'の表示欄をクリアしました。'; $(statusId).className = 'status oktext'; } function promptQuestions() { return $('promptTestQuestions').value .split('\n') .map((line) => line.trim()) .filter(Boolean) .slice(0, 5); } async function loadAccountPrompt() { saveInputs(); $('promptStatus').textContent = '窓口プロンプト取得中...'; $('promptStatus').className = 'status'; try { accountPrompt = await fetchJson(promptBase(), { headers: authHeaders() }); promptPreviewRows = []; renderAccountPrompt(); $('promptStatus').textContent = '現在の窓口プロンプトを取得しました。'; $('promptStatus').className = 'status oktext'; } catch (err) { $('promptStatus').textContent = err.message || String(err); $('promptStatus').className = 'status error'; } } function copyCurrentPromptToDraft() { const current = $('currentCcc').value.trim(); if (!current) { $('promptStatus').textContent = '現在文が未取得です。'; $('promptStatus').className = 'status error'; return; } $('draftCcc').value = current; $('promptStatus').textContent = '現在文を候補へコピーしました。'; $('promptStatus').className = 'status oktext'; } function insertPromptGuide() { const guide = [ '', '# 出力デザイン方針', '- 比較が必要な質問では table を使う。', '- ユーザーが選択肢から選べる質問では choices を使い、label は短く、value は検索に十分な文にする。', '- 手順説明では steps を使い、1項目を短くする。', '- 問い合わせ送信や担当者連絡など、ユーザーの明示確認が必要な操作では confirm を使う。', '- FAQに根拠がない内容は推測せず、分かる範囲と次の確認方法を短く案内する。', ].join('\n'); const target = $('draftCcc'); target.value = target.value.trim() ? target.value.trim() + '\n' + guide : guide.trim(); $('promptStatus').textContent = '出力方針テンプレートを候補へ追加しました。'; $('promptStatus').className = 'status oktext'; } async function previewAccountPrompt() { saveInputs(); const ccc = $('draftCcc').value.trim(); const questions = promptQuestions(); if (!ccc) { $('promptStatus').textContent = '候補プロンプトを入力してください。'; $('promptStatus').className = 'status error'; return; } if (!questions.length) { $('promptStatus').textContent = 'テスト質問を1件以上入力してください。'; $('promptStatus').className = 'status error'; return; } $('promptStatus').textContent = '候補プロンプトでテスト実行中...'; $('promptStatus').className = 'status'; try { const data = await fetchJson(promptBase() + '/preview', { method: 'POST', headers: authHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ ccc, questions }), }); promptPreviewRows = Array.isArray(data.preview) ? data.preview : []; renderAccountPrompt(); $('promptStatus').textContent = '候補プロンプトのテスト結果を取得しました。'; $('promptStatus').className = 'status oktext'; } catch (err) { $('promptStatus').textContent = err.message || String(err); $('promptStatus').className = 'status error'; } } async function createPromptChangeSet() { saveInputs(); const ccc = $('draftCcc').value.trim(); if (!ccc) { $('promptStatus').textContent = '候補プロンプトを入力してください。'; $('promptStatus').className = 'status error'; return; } if (!confirm('候補プロンプトをchange set下書きとして作成しますか?反映は下の承認パネルで別途行います。')) return; $('promptStatus').textContent = 'change set下書き作成中...'; $('promptStatus').className = 'status'; try { const data = await fetchJson(promptBase() + '/change-set', { method: 'POST', headers: authHeaders({ 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey('prompt-change-set') }), body: JSON.stringify({ ccc, title: '窓口プロンプト更新 ' + new Date().toLocaleString('ja-JP'), description: '管理画面の窓口プロンプト評価から作成', }), }); selectedChangeSetId = data.id || ''; $('promptStatus').textContent = 'change set下書きを作成しました。下の承認パネルで差分確認後に反映できます。'; $('promptStatus').className = 'status oktext'; await loadChangeSets(); } catch (err) { $('promptStatus').textContent = err.message || String(err); $('promptStatus').className = 'status error'; } } async function createAdminKey() { saveInputs(); const role = $('newAdminKeyRole').value; const name = $('newAdminKeyName').value.trim(); if (!confirm(role + ' の管理APIキーを発行しますか?キー本体は一度だけ表示されます。')) return; $('adminKeyStatus').textContent = 'キー発行中...'; try { const data = await fetchJson(adminKeysBase(), { method: 'POST', headers: authHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify(Object.assign({ role }, name ? { name } : {})), }); $('newAdminKey').value = data.api_key || ''; $('newAdminKey').type = 'text'; $('toggleNewAdminKey').textContent = '非表示'; $('adminKeyStatus').textContent = 'キーを発行しました。コピー後は表示欄をクリアしてください。'; $('adminKeyStatus').className = 'status oktext'; await loadAdminKeys(); } catch (err) { $('adminKeyStatus').textContent = err.message || String(err); $('adminKeyStatus').className = 'status error'; } } async function revokeAdminKeyById() { saveInputs(); const id = $('revokeAdminKeyId').value.trim(); if (!id) { $('adminKeyStatus').textContent = 'key idを入力してください。'; return; } if (!confirm('APIキー ' + id + ' を失効しますか?')) return; $('adminKeyStatus').textContent = '失効中...'; try { await fetchJson(adminKeysBase() + '/' + encodeURIComponent(id) + '/revoke', { method: 'POST', headers: authHeaders() }); $('adminKeyStatus').textContent = 'APIキーを失効しました。'; $('adminKeyStatus').className = 'status oktext'; await loadAdminKeys(); } catch (err) { $('adminKeyStatus').textContent = err.message || String(err); $('adminKeyStatus').className = 'status error'; } } async function revokeAdminKeyByRaw() { saveInputs(); const apiKey = $('revokeRawAdminKey').value.trim(); if (!apiKey) { $('adminKeyStatus').textContent = '失効するAPIキー本体を入力してください。'; return; } if (!confirm('入力したAPIキーを失効しますか?')) return; $('adminKeyStatus').textContent = '失効中...'; try { await fetchJson(adminKeysBase() + '/revoke-by-key', { method: 'POST', headers: authHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ api_key: apiKey }), }); $('revokeRawAdminKey').value = ''; $('adminKeyStatus').textContent = 'APIキーを失効しました。'; $('adminKeyStatus').className = 'status oktext'; await loadAdminKeys(); } catch (err) { $('adminKeyStatus').textContent = err.message || String(err); $('adminKeyStatus').className = 'status error'; } } async function revokeOAuthTokens() { saveInputs(); if (!confirm('このaccountのClaude連携を再認可しますか? 連携は一度切れて、再接続が必要になります。')) return; $('adminKeyStatus').textContent = 'Claude連携の再認可中...'; try { const data = await fetchJson(adminBase() + '/oauth/tokens/revoke', { method: 'POST', headers: authHeaders() }); $('adminKeyStatus').textContent = 'Claude連携の再認可を完了しました: ' + data.revoked + '件'; $('adminKeyStatus').className = 'status oktext'; } catch (err) { $('adminKeyStatus').textContent = err.message || String(err); $('adminKeyStatus').className = 'status error'; } } async function saveProgress() { saveInputs(); const progress = Object.assign({}, onboardingState.progress || {}); progress.last_ui_update = new Date().toISOString(); progress.selected_step = selectedStep; $('progressStatus').textContent = '保存中...'; try { onboardingState = await fetchJson(adminBase() + '/onboarding', { method: 'PUT', headers: authHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ current_step: selectedStep, status: $('onboardingStatus').value, progress }), }); renderSteps(); $('progressStatus').textContent = '進捗を保存しました。'; $('progressStatus').className = 'status oktext'; } catch (err) { $('progressStatus').textContent = err.message || String(err); $('progressStatus').className = 'status error'; } } async function loadMcpTools() { saveInputs(); $('mcpStatus').textContent = '公開tool確認中...'; try { const data = await fetchJson(mcpBase() + '/tools', { headers: authHeaders() }); mcpToolList = Array.isArray(data.tools) ? data.tools : Array.isArray(data) ? data : []; renderMcp(); renderSteps(); } catch (err) { $('mcpStatus').textContent = err.message || String(err); $('mcpStatus').className = 'status error'; } } async function loadByok() { saveInputs(); $('byokStatus').textContent = 'BYOK状態取得中...'; try { byokState = await fetchJson(adminBase() + '/llm-credential', { headers: authHeaders() }); renderByok(); renderSteps(); $('byokStatus').textContent = 'BYOK状態を取得しました。'; $('byokStatus').className = 'status oktext'; } catch (err) { $('byokStatus').textContent = err.message || String(err); $('byokStatus').className = 'status error'; } } async function submitByok(mode) { saveInputs(); const apiKey = $('byokKey').value.trim(); if ((mode === 'validate' || mode === 'save') && !apiKey) { $('byokStatus').textContent = 'API keyを入力してください。'; $('byokStatus').className = 'status error'; return; } if (mode === 'disable' && !confirm('このaccountのBYOKを無効化しますか?')) return; const url = mode === 'validate' ? adminBase() + '/llm-credential/validate' : adminBase() + '/llm-credential'; const opts = mode === 'disable' ? { method: 'DELETE', headers: authHeaders() } : { method: mode === 'save' ? 'PUT' : 'POST', headers: authHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ provider: 'anthropic', api_key: apiKey }) }; $('byokStatus').textContent = mode === 'validate' ? '検証中...' : mode === 'save' ? '検証して保存中...' : '無効化中...'; try { byokState = await fetchJson(url, opts); if (mode === 'save') $('byokKey').value = ''; renderByok(); renderSteps(); $('byokStatus').textContent = mode === 'validate' ? '検証結果を取得しました。' : mode === 'save' ? 'BYOKを保存しました。' : 'BYOKを無効化しました。'; $('byokStatus').className = byokState.validation_status === 'invalid' ? 'status error' : 'status oktext'; } catch (err) { $('byokStatus').textContent = err.message || String(err); $('byokStatus').className = 'status error'; } } async function loadChangeSets() { saveInputs(); $('changeStatus').textContent = 'change set一覧取得中...'; try { changeSets = await fetchJson(adminBase() + '/change-sets', { headers: authHeaders() }); if (!Array.isArray(changeSets)) changeSets = []; if (selectedChangeSetId && !changeSets.some((cs) => cs.id === selectedChangeSetId)) { selectedChangeSetId = ''; selectedChangeSetDetail = null; selectedChangeSetDiff = null; } if (!selectedChangeSetId && changeSets[0]) selectedChangeSetId = changeSets[0].id; renderChangeSets(); if (selectedChangeSetId) await loadChangeSet(selectedChangeSetId); $('changeStatus').textContent = '一覧を取得しました。'; $('changeStatus').className = 'status oktext'; } catch (err) { $('changeStatus').textContent = err.message || String(err); $('changeStatus').className = 'status error'; syncChangeSetActions(); } } async function loadChangeSet(id) { if (!id) return; selectedChangeSetId = id; $('changeStatus').textContent = 'change set詳細取得中...'; try { selectedChangeSetDetail = await fetchJson(adminBase() + '/change-sets/' + encodeURIComponent(id), { headers: authHeaders() }); selectedChangeSetDiff = await fetchJson(adminBase() + '/change-sets/' + encodeURIComponent(id) + '/diff', { headers: authHeaders() }); renderChangeSets(); renderSelectedChangeSet(); $('changeStatus').textContent = '差分を取得しました。'; $('changeStatus').className = 'status oktext'; } catch (err) { $('changeStatus').textContent = err.message || String(err); $('changeStatus').className = 'status error'; syncChangeSetActions(); } } async function executeChangeSet(mode) { if (!selectedChangeSetId) { $('changeStatus').textContent = 'change setを選択してください。'; $('changeStatus').className = 'status error'; return; } const currentStatus = selectedChangeSetDetail && selectedChangeSetDetail.status; const allowed = changeSetAllowedActions(currentStatus); if (!allowed[mode]) { $('changeStatus').textContent = '現在のstatusではこの操作は実行できません。status=' + (currentStatus || 'unknown'); $('changeStatus').className = 'status error'; syncChangeSetActions(); return; } const verb = mode === 'apply' ? '反映' : mode === 'discard' ? '破棄' : 'rollback'; if (!confirm('選択中のchange setを' + verb + 'しますか?')) return; $('changeStatus').textContent = verb + '中...'; try { selectedChangeSetDetail = await fetchJson(adminBase() + '/change-sets/' + encodeURIComponent(selectedChangeSetId) + '/' + mode, { method: 'POST', headers: authHeaders(), }); selectedChangeSetDiff = null; await loadChangeSets(); $('changeStatus').textContent = verb + 'しました。'; $('changeStatus').className = 'status oktext'; } catch (err) { $('changeStatus').textContent = err.message || String(err); $('changeStatus').className = 'status error'; } } $('refresh').addEventListener('click', refreshAll); $('saveProgress').addEventListener('click', saveProgress); $('loadMcpTools').addEventListener('click', loadMcpTools); $('smokeSearchFaqs').addEventListener('click', () => smokeMcp('search')); $('smokeUsage').addEventListener('click', () => smokeMcp('usage')); $('smokeDraft').addEventListener('click', () => smokeMcp('draft')); $('copyMcpUrl').addEventListener('click', async () => { await navigator.clipboard.writeText(mcpExternalBase()).catch(() => {}); $('mcpStatus').textContent = 'Claude連携URLをコピーしました。'; }); $('copyFinalEmbedTag').addEventListener('click', () => copyFieldValue('finalEmbedTag', 'progressStatus', '最終設置タグ')); $('copyWidgetCdnUrl').addEventListener('click', () => copyTextValue(widgetPublicBase(), 'progressStatus', 'Widget URL')); $('copyApiUrl').addEventListener('click', () => copyTextValue(apiPublicBase(), 'progressStatus', 'API URL')); $('copyMcpPublicUrl').addEventListener('click', () => copyTextValue(mcpExternalBase(), 'progressStatus', 'MCP URL')); $('loadAccountPrompt').addEventListener('click', loadAccountPrompt); $('copyCurrentPrompt').addEventListener('click', copyCurrentPromptToDraft); $('insertPromptGuide').addEventListener('click', insertPromptGuide); $('previewAccountPrompt').addEventListener('click', previewAccountPrompt); $('createPromptChangeSet').addEventListener('click', createPromptChangeSet); $('loadAdminKeys').addEventListener('click', loadAdminKeys); $('createAdminKey').addEventListener('click', createAdminKey); $('copyNewAdminKey').addEventListener('click', () => copyFieldValue('newAdminKey', 'adminKeyStatus', '発行されたキー')); $('toggleNewAdminKey').addEventListener('click', () => toggleSecretField('newAdminKey', 'toggleNewAdminKey')); $('clearNewAdminKey').addEventListener('click', () => clearFieldValue('newAdminKey', 'adminKeyStatus', '発行されたキー')); $('revokeAdminKey').addEventListener('click', revokeAdminKeyById); $('revokeAdminKeyByRaw').addEventListener('click', revokeAdminKeyByRaw); $('revokeOAuthTokens').addEventListener('click', revokeOAuthTokens); $('loadInvitations').addEventListener('click', loadInvitations); $('createInvitation').addEventListener('click', createInvitation); $('copyInviteToken').addEventListener('click', () => copyFieldValue('inviteToken', 'memberOpsStatus', '招待URL')); $('toggleInviteToken').addEventListener('click', () => toggleSecretField('inviteToken', 'toggleInviteToken')); $('clearInviteToken').addEventListener('click', () => clearFieldValue('inviteToken', 'memberOpsStatus', '招待URL')); $('loadSignupRequests').addEventListener('click', loadSignupRequests); $('loadByok').addEventListener('click', loadByok); $('validateByok').addEventListener('click', () => submitByok('validate')); $('saveByok').addEventListener('click', () => submitByok('save')); $('disableByok').addEventListener('click', () => submitByok('disable')); $('loadChangeSets').addEventListener('click', loadChangeSets); $('applyChangeSet').addEventListener('click', () => executeChangeSet('apply')); $('discardChangeSet').addEventListener('click', () => executeChangeSet('discard')); $('rollbackChangeSet').addEventListener('click', () => executeChangeSet('rollback')); renderSteps(); renderAccount(); renderMcp(); renderAdminKeys(); renderAccountPrompt(); renderInvitations(); renderSignupRequests(); renderByok(); renderChangeSets(); renderSelectedChangeSet(); syncChangeSetActions(); (function initSidebar() { var KEY = 'smb_admin_sidebar_collapsed'; var body = document.body; var mq = window.matchMedia('(max-width: 720px)'); function isCollapsed() { return !mq.matches && localStorage.getItem(KEY) === '1'; } body.classList.toggle('sidebar-collapsed', isCollapsed()); var btn = document.getElementById('sidebarToggle'); if (btn) btn.addEventListener('click', function () { if (mq.matches) { var open = body.classList.toggle('sidebar-open'); btn.setAttribute('aria-expanded', open ? 'true' : 'false'); } else { var c = !isCollapsed(); localStorage.setItem(KEY, c ? '1' : '0'); body.classList.toggle('sidebar-collapsed', c); } }); document.addEventListener('click', function (e) { if (!mq.matches || !body.classList.contains('sidebar-open')) return; var aside = document.querySelector('.sidebar'); var t = e.target; if (aside && !aside.contains(t) && t !== btn && !(btn && btn.contains(t))) body.classList.remove('sidebar-open'); }); })(); async function loadTenantBadge() { const badge = $('tenantBadge'); if (!badge) return; badge.textContent = '現在の組織を確認中...'; try { const res = await fetch('/v1/admin/me', { headers: authHeaders() }); const j = await res.json().catch(() => null); if (!res.ok || !j || !j.data) { if (res.status === 401) { badge.textContent = '未ログイン / APIキー未設定'; } else if (res.status === 403) { badge.textContent = '権限がありません'; } else { badge.textContent = '現在の組織を確認できません'; } return; } const me = j.data; badge.textContent = '現在の組織: ' + me.tenant_id + ' / role: ' + me.role + ' / ' + me.auth_type; } catch { badge.textContent = '現在の組織を確認できません'; } } loadTenantBadge();