# Minion - 言語モデル向けドキュメント Minion は AI エージェントのワークスペースです。自前のホスト上で動くエージェント (ミニオン) を 管理し、エージェントと人が共有するワークスペース機能 - ノート、フォーム、カレンダー、 ドライブ、会計、アセスメント、ヘッドレス CMS - を持ちます。 このファイルにはアセスメントの組み込みに要るページだけが入っています。各ページの先頭に正規 URL があります。 ドキュメント全文: https://docs.minionworkspace.com/ja/llms-full.txt English: https://docs.minionworkspace.com/llms-assessment.txt ## 間違えやすい規約 コードを書く前にここを読んでください。**一見もっともらしい実装のほうが誤りになる**箇所です。 1. **アセスメントの API はサーバーから呼びます。ブラウザからは呼びません。** CORS ヘッダを 返しません。連携キー (`asm_...`) をクライアント側のコードや `NEXT_PUBLIC_` / `PUBLIC_` の 変数に置いてはいけません。キーは `Authorization: Bearer` でだけ送ります。 2. **設問は返ってきた順に聞き、`questions` が空配列になったら終わります。** 完了フラグは ありません。並べ替え・飛ばし・追加をせず、代わりに `/catalog/facts` を上から聞くことも しないでください。 3. **回答は設問の型に正確に合わせます。** `boolean` は `true` / `false`、`number` は JSON の数値、 `select` は選択肢の `value` (ラベルではない)。形の崩れた本文 (JSON でない、`answers` が オブジェクトでない、`unknown` が配列でない) は無視され、`200` が返って何も記録されません。 4. **回答を推測で埋めません。** 返事が設問にはっきり当てはまらなければ、聞き直すか `unknown` で 送ります。推測は、空白の見えない間違った結果になります。 5. **ヒアリングの開始は冪等ではありません。** 訪問者が始めたときに作り、`session.id` を (httpOnly Cookie などに) 保存して `GET /sessions/{id}` で再開します。 6. **サイトからヒアリングを確定することはできません。** サイトは閉じるだけで、確定は ワークスペースの人が行います。閉じたか確定したあとの回答は `409` で拒否されます。 --- # アセスメントの組み込み Source: https://docs.minionworkspace.com/ja/guides/assessment-integration/ Summary: 自社の Web サイトやボットでアセスメントのヒアリングを回すために必要なことを、1 ページに閉じたもの。 このページは意図的に 1 ページで完結させてあります。[アセスメントのガイド](https://docs.minionworkspace.com/ja/guides/assessment/)と 重複する内容も繰り返しているのは、**これだけを渡せば Web サイトやボットにヒアリングを組み込める** 状態にするためです (人に渡す場合も、コーディング支援の AI に渡す場合も)。 言語モデル向けのプレーンテキスト版は [`/ja/llms-assessment.txt`](https://docs.minionworkspace.com/ja/llms-assessment.txt) にあります。 ## この構成を 3 行で 制作したサイトのサーバーが連携キーを持ち、API を呼びます。次にどの設問を聞くかと結果の算出は Minion が行い、サイトは設問を表示し、訪問者の返事を値に変換して送り返します。聞くことが なくなったらサイトがヒアリングを閉じ、ワークスペースの担当者が確認します。 Minion は自由記述を受け取らず、代わりに言語モデルを動かすこともありません。だから同じ API が、 ただのフォームにも、チャットボットにも、人が手で入力する画面にもなります。サイトが決めないのは **設問の順番**です。返ってきた順に聞くことが、聞き漏れを防ぐ仕組みそのものです。 ## 始める前に必要な値 | 値 | 見た目 | どこから | |---|---|---| | API のベース URL | `https://minionworkspace.com/api/public/assessment` | Minion を開いているオリジン + `/api/public/assessment` | | 連携キー | `asm_…` | カタログの **連携** → **外部連携キー**。表示されるのは発行時の 1 回だけ | | カタログ識別子 (任意) | `web-production` | 同じ **連携** の画面、キーの上。カタログ一覧の名前の下にも出ています | いずれも**サーバー側の**環境変数に置いてください (例: `ASSESSMENT_API_BASE`、`ASSESSMENT_API_KEY`)。 `NEXT_PUBLIC_`・`PUBLIC_`・`VITE_` を付けてはいけません。付けるとブラウザに配る JavaScript に 埋め込まれます。 カタログが公開済みで、ワークスペースでアセスメントが有効になっている必要があります。 **キーがカタログを決めます。** カタログを指定するリクエストはありません。別の連携のキーを貼っても エラーにはならず、**黙って別のカタログの設問が出てきます**。起動時に `GET /catalog` を 1 回呼び、 `catalog.key` が想定どおりかを確かめてください。 カタログ識別子は、この照合の当て先です。設定する理由はそれだけなので、**任意の設定として扱って ください。** リクエストには一切乗らないため、未設定なら照合を飛ばすのが正しく、アプリを起動 できなくするのは筋が違います (キーが正しければ識別子が無くても動くので、必須にすると落ちる 理由が増えるだけです)。識別子はカタログを作るときに決まり、後から変わることはないので、 環境変数に焼いて構いません。 ## どこから呼ぶか ``` ブラウザ ──(自分のサイトのルート + Cookie)──▶ サイトのサーバー ──(Authorization: Bearer asm_…)──▶ Minion ``` - **API は CORS ヘッダを返しません。** ブラウザからは呼べず、それは意図したものです (呼べるなら キーがページに載ってしまう)。静的ホスティングだけでは足りません。SSR・ルートハンドラ・ サーバーレス関数・エッジ関数など、サーバーで動く部分が必要です。 - **キーは `Authorization: Bearer` ヘッダでだけ送ります。** クエリ文字列に入れたキーは読まれず、 `api_key_required` で失敗します。 - **サイトのルートは、その訪問者のヒアリングにだけ作用させます。** セッション ID は httpOnly Cookie に 置き、「はじめる・答える・わからない・送信する」といった決まった操作だけを用意してください。 ブラウザから来たパスやセッション ID をそのまま API へ転送してはいけません。ID さえ分かれば、 他人のヒアリングを読んだり削除したりできてしまいます。 ## ループ ``` 1. POST /sessions → session.id, questions, result 2. questions[0] を表示する 3. POST /sessions/{id}/answers → 次の questions, 新しい result (回答、または「わからない」) 4. questions が空になるまで 2〜3 を繰り返す 5. POST /sessions/{id}/close ``` - **`questions` が空配列なら、聞くことは残っていません。** `done` のようなフラグはありません。 設問の数は回答によって変わります。ある回答によって他の設問が無関係になれば、それらは消えます。 - **返ってきた順に聞いてください。** 並べ替え・飛ばし・追加をしてはいけません。 - **一覧は回答のたびに計算し直されます。** 複数の設問を同時に見せると、1 問目に答えた時点で後ろの 設問が不要になることがあります。1 問か数問ずつ見せ、答えを送ってから次を聞いてください。 - **いつ閉じても構いません。** 残った未確定の事実は、確認する人に見える形で残ります。 `limit` で返ってくる設問の数を決めます。既定は 3、`0` で全件です。1 画面 1 問、チャットの 1 発言 1 問にするなら `limit: 1` です。 ## エンドポイント パスはすべてベース URL の下で、どのリクエストにもヘッダが必要です。 | メソッド | パス | 返すもの | |---|---|---| | `POST` | `/sessions` | `201` `{ session, catalog, questions, result, progress }` | | `GET` | `/sessions/{id}?limit=` | `{ session, catalog, questions, result, progress }` — 再開用に全部入り | | `POST` | `/sessions/{id}/answers` | `{ questions, topic_changed?, result, progress }` | | `GET` | `/sessions/{id}/answers` | `{ answers, unknown }` | | `DELETE` | `/sessions/{id}/answers/{factId}` | `{ questions, result, progress }` | | `GET` | `/sessions/{id}/questions?limit=` | `{ questions, progress }` | | `GET` | `/sessions/{id}/facts` | `{ facts }` — 全事実と、このヒアリングでの状態 | | `GET` | `/sessions/{id}/result` | `{ result }` | | `POST` | `/sessions/{id}/close` | `{ session }` | | `DELETE` | `/sessions/{id}` | `{ ok: true }` | | `GET` | `/catalog` | `{ catalog: { key, name, description, fact_count, measure_count } }` | | `GET` | `/catalog/facts` | `{ facts }` — 全設問をカタログの順で | | `GET` | `/catalog/measures` | `{ measures }` — 全軸をカタログの順で | **キーが届くのは、そのキーで始めたヒアリングだけです。** 別のキーで始めたもの、Minion の画面で 作ったアセスメントの ID を指定すると、存在しない ID と同じく `404` が返ります。 ## ヒアリングを始める ```http POST /api/public/assessment/sessions Authorization: Bearer asm_… Content-Type: application/json { "title": "株式会社◯◯ サイトリニューアル", "limit": 1 } ``` どちらのフィールドも任意です。 - **`title` は、確認する人が一覧で見る名前です。** 後から変更できません。省略するとカタログ名と 日時になり、見分けにくくなります。ヒアリングが保存できる唯一の自由記述でもあります。 - **ヒアリングは訪問者が始めたときに作ってください。** ページを開いたときではありません。 1 件ごとにワークスペースに記録が残ります。 - **開始は冪等ではありません。** `POST /sessions` のたびに新しいヒアリングが作られます。 タイムアウト後の再試行が実は成功していた場合も同じです。`session.id` を保存し、再開には `GET /sessions/{id}` を使ってください。 ```json { "session": { "id": "0b8f4c7e-6d0a-4f5e-9d61-2c3a1e7b9f10", "title": "株式会社◯◯ サイトリニューアル", "state": "in_progress", "closed_at": null, "created_at": "2026-09-16T04:12:30.000Z" }, "catalog": { "key": "web-production", "name": "Web サイト制作" }, "questions": [ { "fact_id": "fct_pages", "label": "固定ページ数", "description": "トップを含む固定ページの数。", "type": "number", "unit": "ページ", "topic": "デザイン" } ], "result": { "measures": [ { "measure_id": "msr_initial", "label": "初期費用", "group": null, "primary": true, "display": "currency", "value": { "min": 1130000, "max": 3300000 }, "settled": false }, { "measure_id": "msr_monthly_expense", "label": "実費", "group": "月額", "primary": false, "display": "currency", "value": { "min": 0, "max": 14900 }, "settled": false }, { "measure_id": "msr_monthly_service", "label": "自社分", "group": "月額", "primary": false, "display": "currency", "value": { "min": 0, "max": 25000 }, "settled": false } ], "verdict": null, "pending": [ { "fact_id": "fct_pages", "label": "固定ページ数" }, { "fact_id": "fct_design", "label": "デザインの進め方" } ] }, "progress": { "answered": 0, "unknown": 0 } } ``` ## 設問 | フィールド | | |---|---| | `fact_id` | 回答のキーとしてそのまま送り返す | | `label` | 設問。カタログに書かれたまま | | `description` | 任意。補足の説明 | | `type` | `boolean`・`number`・`select` のいずれか | | `unit` | `number` のみ、任意。入力欄の横に表示する | | `options` | `select` のみ。`[{ "value": "opt_new_design", "label": "新規デザイン" }]` | | `topic` | 任意。設問が属する話題 | **`topic_changed`** は、1 件以上の回答を含む `POST /answers` のレスポンスにだけ付きます。次の設問が 直前に答えた設問と別の話題なら `true` です。見出しや「続いて、〜について」という前置きに使うか、 無視してください。ほかのレスポンスには付きません。 設問はカタログを書いた言語のままです。翻訳はありません。 どの寄与項目からも使われていない事実は、`GET /catalog/facts` には出ますが `questions` には一度も 出ません。答えても結果が変わらないからです。これはサイトではなくカタログ側の不備で、カタログの 編集画面で **未使用** の印が付いています。 ## 回答する ```http POST /api/public/assessment/sessions/{id}/answers Content-Type: application/json { "answers": { "fct_pages": 12, "fct_design": "opt_new_design" }, "unknown": ["fct_hosting"], "limit": 1 } ``` どのフィールドも任意です。`answers` は `fact_id` をキーにしたオブジェクト、`unknown` は `fact_id` の配列です。 | `type` | 送るもの | 送ってはいけないもの | |---|---|---| | `boolean` | `true` / `false` | `"true"`、`"はい"`、`1` | | `number` | JSON の数値: `12` | `"12"`、`"10〜20"`、`null` | | `select` | 選択肢の `value`: `"opt_new_design"` | ラベルの `"新規デザイン"` | - **回答は差分でマージされます。** 変わるのは送った事実だけで、同じ事実に答え直すと上書きされます。 - **`unknown` は「聞いたが、分からない・答えたくない」です。** これも回答のひとつで、その事実は 二度と聞かれず、`result.pending` に確認事項として残ります。後から答えると `unknown` から外れ、 答えた事実を `unknown` にすると回答は消えます。同じリクエストで両方に入れた場合は回答が優先されます。 - **回答を完全に取り消す** (回答でも不明でもない状態に戻す) には `DELETE /sessions/{id}/answers/{factId}` を使います。 - **全部通るか、全部通らないかです。** 1 件でも不正なら `400 validation_failed` になり、何も保存されません。 - **形の崩れた本文はエラーにならず、無視されます。** 本文が JSON でない、`answers` がオブジェクトで ない、`unknown` が配列でない場合、その部分は無かったものとして扱われ、`200` が返って**何も記録 されません**。必ず `Content-Type: application/json` で JSON オブジェクトを送り、想定どおり `progress` が動いたかを確かめてください。 - **推測で埋めないでください。** 返事が設問にはっきり当てはまらないなら、聞き直すか「不明」として 送ります。推測で埋めると、**未確定が無いように見えて中身が間違っている結果**になります。 - **数値は 1 つの数です。** 「10〜20 ページ」は受け付けられません。1 つの数で聞き直すか、不明として送ります。 ```json { "questions": [ { "fact_id": "fct_multilang", "label": "多言語対応", "type": "boolean", "topic": "実装" } ], "topic_changed": true, "result": { "measures": [ { "measure_id": "msr_initial", "label": "初期費用", "group": null, "primary": true, "display": "currency", "value": { "min": 1930000, "max": 2640000 }, "settled": false } ], "verdict": null, "pending": [ { "fact_id": "fct_multilang", "label": "多言語対応" }, { "fact_id": "fct_hosting", "label": "ホスティング" } ] }, "progress": { "answered": 2, "unknown": 1 } } ``` (省略しています。実際のレスポンスにはすべての軸と、すべての確認事項が入ります。) 検証に失敗すると、問題ごとに理由が返ります。 ```json { "error": "validation_failed", "message": "…", "errors": [ { "fact_id": "fct_pages", "code": "type_mismatch", "message": "…" }, { "fact_id": "fct_design", "code": "unknown_option", "message": "…" } ] } ``` | `code` | | |---|---| | `unknown_fact` | このカタログにその ID の事実が無い | | `archived_fact` | その事実はカタログから削除された | | `type_mismatch` | 設問に対して JSON の型が違う | | `unknown_option` | 設問の `options[].value` のどれでもない | | `archived_option` | その選択肢はカタログから削除された | | `not_finite` | `NaN` または `Infinity` | ## 結果 `result` を持つレスポンスには、1 問目から必ず結果が入っています。 | フィールド | | |---|---| | `measures[]` | カタログの出力の軸ごとに 1 つ、カタログの順で | | `measures[].label`、`.group` | 同じ `group` の軸はひとまとまり: 「月額 · 実費」「月額 · 自社分」 | | `measures[].primary` | 主役の数字。一番大きく見せる | | `measures[].display` | `currency`: 金額。レスポンスに通貨コードはなく、Minion はこの金額を円として表示しています。`number`: ただの数値で、`unit` があれば添える | | `measures[].value` | `{ min, max }`。表示するときは丸める | | `measures[].settled` | `min` と `max` が等しければ `true` | | `verdict` | `null`、または `{ level, label }`。`level` は `pass`・`borderline`・`fail`・`info`。`label` はカタログの作者が書いた文言。結果に効く未確定が残るうちは `borderline` のまま | | `pending[]` | 幅の理由になっている未確定の事実。答えれば幅が縮むものと、「不明」と記録されたもの。「確認が必要な点」として見せる | **金額をいつ見せるかは、サイトが決めてください。** 序盤の幅は広いことがあります。当社の Web サイト制作 サンプルでは初期費用が ¥1,130,000 〜 ¥3,300,000 から始まり、21 問あるより大きな見積カタログでは 上限が下限の 15 倍から始まりました。広すぎる幅をそのまま見せると「見当がついていない」と読まれます。 最後に見せる、何問か答えてから見せる、基準の軸の `max / min` が十分小さくなってから見せる、などを 選んでください。月額の軸では `min` が `0` のことがよくあるので、割り算には注意してください。 **設問の順位は、基準の軸 (`primary`) だけで決まります。** 基準以外の軸にしか効かない事実は `questions` に一度も出ないので、`questions` が尽きても基準以外の軸は幅のまま残ることがあり、その理由は `pending` にも出ません。Web サイト制作サンプルでは「月額保守契約」が一度も聞かれず、「月額 · 自社分」は ¥0 〜 ¥25,000 のまま終わります。そういう軸も見せるなら、`questions` が空になったあとで `GET /sessions/{id}/facts` を取り、`unanswered` のまま残っている事実を聞いてください。 そうでなければ、基準の軸だけを見せてください。 寄与量・賃率・内訳・工数は、意図的にレスポンスに含めていません。金額から逆算しようとしないでください。 ## ヒアリングを閉じる ```http POST /api/public/assessment/sessions/{id}/close ``` 本文は不要です。レスポンスは `{ session }` で、`state` が `awaiting_review` になります。 | `session.state` | 意味 | |---|---| | `in_progress` | 進行中。回答を受け付ける | | `awaiting_review` | サイトが閉じた。ワークスペースの担当者の確認待ち | | `fixed` | 人が確かめて確定した。結果は凍結されている | - **閉じたあとの回答と取り消しは** `409 session_closed` **で失敗します。** 読み取りと、 ヒアリングの削除はそのまま使えます。 - **回答が 1 件以上あるヒアリングを閉じると、ワークスペースに通知されます。** - **人はいつでも確定できます。** 訪問者が答えている途中でもです。その場合、次の回答は `409 assessment_fixed` で失敗します。閉じられたヒアリングと同じに扱ってください。 - **サイトからは確定できません。** 確定は人の判断で、そう設計してあります。 - 閉じなかったヒアリングは、ワークスペースで進行中のまま残ります。訪問者が送信したときに閉じてください。 途中で離脱した訪問者のために何かする必要はありません。 ## 再開する セッション ID を保存しておき (httpOnly Cookie、ボットなら自前のデータベース)、訪問者が戻ってきたら `GET /sessions/{id}?limit=1` を呼びます。開始時と同じものが全部返ります。 `404` が返るか、`session.state` が `in_progress` でなくなっていたら、ID を捨てて最初から始められるようにしてください。 ## 設問の別の並べ方 - **全設問を 1 ページに並べる。** `GET /sessions/{id}/facts` は全事実をカタログの順に、`state` 付きで 返します。`answered` (`value` 付き)・`unknown`・`unanswered`・`inactive` のいずれかです。 `inactive` はいまの回答では結果に効かない事実なので、隠すか薄く表示してください (入力済みの内容は 残っています)。基準の軸の幅を縮める `unanswered` の事実には `rank` が付き、`1` が一番聞く価値があります。 - **ヒアリングを始める前に並べる。** `GET /catalog/facts` と `GET /catalog/measures` で、設問と軸を 前もって取れます。ただし固定の順番で聞くと、一番効くことから聞くというこの機能の要点を 捨てることになります。できるだけ `questions` を使ってください。 ## ヒアリングを削除する `DELETE /sessions/{id}` で、そのキーで始めたヒアリングを削除できます。訪問者からデータの削除を 求められたときなどに使います。確定済みのヒアリングは API からは削除できない (`409 assessment_fixed`) ので、ワークスペースに削除を依頼してください。 ## エラー エラーはすべて `{ "error": "", "message": "…" }` です。`validation_failed` だけ `errors[]` が付きます。 **分岐は `error` で行い、`message` を訪問者に見せないでください。** 多言語化されていません。 | ステータス | `error` | いつ | |---|---|---| | 400 | `validation_failed` | 回答が設問に合わない。何も保存されていない | | 401 | `api_key_required` | `Authorization: Bearer` ヘッダが無い | | 401 | `invalid_api_key` | そのキーが存在しない。カタログを削除するとキーも消える | | 401 | `api_key_revoked` | キーが失効している | | 404 | `not_found` | セッションが無い、またはこのキーで始めたものではない | | 404 | `catalog_not_found` | カタログが使えなくなった、またはワークスペースでアセスメントが無効 | | 409 | `catalog_unpublished` | カタログが公開されていない | | 409 | `session_closed` | ヒアリングは閉じられている | | 409 | `assessment_fixed` | 人がヒアリングを確定した | | 409 | `conflict` | 同じヒアリングに同時に書き込まれた。読み直して 1 回だけ再試行する | | 429 | `rate_limited` | リクエストが多すぎる。`Retry-After` ヘッダは無いので、間隔を空ける | | 500 | `internal_error` | 時間をおいて再試行する | | 503 | `unsupported_engine` | カタログが今は使えない。ワークスペースに問い合わせる | ## レート制限 | 対象 | 上限 | |---|---| | すべてのリクエスト | キーごとに毎分 300 (キーが無ければ IP アドレスごと) | | `POST /sessions` | キーごとに毎分 60 | | 1 件のヒアリングへの書き込み (回答・取り消し・閉じる・削除) | ヒアリングごとに毎分 20 | 上限はおおよその値です。ポーリングはしないでください。サイトかワークスペースの人が変えない限り、 Minion 側では何も変わりません。 ## 実装例 ### API クライアント 下の 2 つの例で共通に使います。`lib/assessment.ts` (Next.js) または `src/lib/assessment.ts` (Astro) に置きます。 ```ts // lib/assessment.ts // // サーバー側専用。連携キーを読むので、ブラウザで動くコードから import してはいけない。 // クライアントコンポーネントから「型だけ」を import するのは問題ない (型は消える)。 const BASE = process.env.ASSESSMENT_API_BASE ?? 'https://minionworkspace.com/api/public/assessment' export type AnswerValue = boolean | number | string export interface Question { fact_id: string label: string description?: string type: 'boolean' | 'number' | 'select' /** number のみ */ unit?: string /** select のみ */ options?: { value: string; label: string }[] topic?: string } export interface MeasureValue { measure_id: string label: string group: string | null primary: boolean display: 'currency' | 'number' unit?: string value: { min: number; max: number } settled: boolean } export interface Result { measures: MeasureValue[] verdict: { level: 'pass' | 'borderline' | 'fail' | 'info'; label: string } | null pending: { fact_id: string; label: string }[] } export interface Progress { answered: number unknown: number } export interface Session { id: string title: string state: 'in_progress' | 'awaiting_review' | 'fixed' closed_at: string | null created_at: string } /** POST /sessions, GET /sessions/:id */ export interface SessionView { session: Session catalog: { key: string; name: string } questions: Question[] result: Result progress: Progress } /** POST /sessions/:id/answers, DELETE /sessions/:id/answers/:factId */ export interface AnswerView { questions: Question[] /** そのリクエストで 1 件以上回答したときだけ付く。 */ topic_changed?: boolean result: Result progress: Progress } export class AssessmentApiError extends Error { constructor( readonly status: number, readonly code: string, message: string, readonly errors: { fact_id: string; code: string; message: string }[] = [], ) { super(message) } } async function call(method: 'GET' | 'POST' | 'DELETE', path: string, body?: object): Promise { const key = process.env.ASSESSMENT_API_KEY if (!key) throw new Error('ASSESSMENT_API_KEY is not set') const res = await fetch(`${BASE}${path}`, { method, headers: { Authorization: `Bearer ${key}`, ...(body ? { 'Content-Type': 'application/json' } : {}), }, body: body ? JSON.stringify(body) : undefined, cache: 'no-store', }) const data = await res.json().catch(() => null) if (!res.ok) { throw new AssessmentApiError( res.status, data?.error ?? 'http_error', data?.message ?? `HTTP ${res.status}`, data?.errors, ) } return data as T } const session = (id: string) => `/sessions/${encodeURIComponent(id)}` export const assessment = { /** キーがどのカタログを指しているか。起動時に 1 回呼んで、キーの取り違えに気づく。 */ catalog: () => call<{ catalog: { key: string; name: string; description: string | null; fact_count: number; measure_count: number } }>('GET', '/catalog'), start: (title: string, limit = 1) => call('POST', '/sessions', { title, limit }), get: (id: string, limit = 1) => call('GET', `${session(id)}?limit=${limit}`), answer: (id: string, factId: string, value: AnswerValue, limit = 1) => call('POST', `${session(id)}/answers`, { answers: { [factId]: value }, limit }), unknown: (id: string, factId: string, limit = 1) => call('POST', `${session(id)}/answers`, { unknown: [factId], limit }), clear: (id: string, factId: string) => call('DELETE', `${session(id)}/answers/${encodeURIComponent(factId)}`), close: (id: string) => call<{ session: Session }>('POST', `${session(id)}/close`), remove: (id: string) => call<{ ok: true }>('DELETE', session(id)), } ``` ### Next.js (App Router) ルートハンドラがセッションの Cookie を持って API と話し、クライアントコンポーネントが 1 問ずつ 表示します。`@/*` はプロジェクトのルートを指す前提です。 ```ts // app/api/hearing/route.ts import { cookies } from 'next/headers' import { NextResponse } from 'next/server' import { assessment, AssessmentApiError, type AnswerValue } from '@/lib/assessment' // 訪問者のセッション ID は httpOnly Cookie に置く。このルートはその 1 件にしか // 触らないので、API への汎用プロキシにはならない。 const COOKIE = 'assessment_session' const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i type Action = | { action: 'start' } | { action: 'answer'; fact_id: string; value: AnswerValue } | { action: 'unknown'; fact_id: string } | { action: 'close' } async function currentSessionId(): Promise { const id = (await cookies()).get(COOKIE)?.value return id && UUID.test(id) ? id : null } /** 再読み込み後の再開。 */ export async function GET() { const id = await currentSessionId() if (!id) return NextResponse.json({ view: null }) try { const view = await assessment.get(id) // 閉じた、または Minion 側で確定された。止まったフォームを見せず、最初からにする。 if (view.session.state !== 'in_progress') { ;(await cookies()).delete(COOKIE) return NextResponse.json({ view: null }) } return NextResponse.json({ view }) } catch (err) { if (err instanceof AssessmentApiError && err.status === 404) { ;(await cookies()).delete(COOKIE) return NextResponse.json({ view: null }) } throw err } } export async function POST(request: Request) { const body = (await request.json()) as Action const jar = await cookies() try { if (body.action === 'start') { // 確認する人が見分けられるタイトルにする。後から変更できない。 const stamp = new Date().toISOString().slice(0, 16).replace('T', ' ') const view = await assessment.start(`Web サイト見積 ${stamp}`) jar.set(COOKIE, view.session.id, { httpOnly: true, secure: true, sameSite: 'lax', path: '/', maxAge: 60 * 60 * 24 * 7, }) return NextResponse.json({ view }) } const id = await currentSessionId() if (!id) return NextResponse.json({ error: 'no_session' }, { status: 409 }) switch (body.action) { case 'answer': return NextResponse.json({ view: await assessment.answer(id, body.fact_id, body.value) }) case 'unknown': return NextResponse.json({ view: await assessment.unknown(id, body.fact_id) }) case 'close': await assessment.close(id) jar.delete(COOKIE) return NextResponse.json({ view: null, closed: true }) default: return NextResponse.json({ error: 'unknown_action' }, { status: 400 }) } } catch (err) { if (err instanceof AssessmentApiError) { // ページに渡すのはエラーコードだけ。キーの誤り (401) や障害 (5xx) は訪問者ではなく // こちらの問題なので、ただの 502 にする。 console.error('[assessment]', err.status, err.code, err.message) const status = [400, 404, 409, 429].includes(err.status) ? err.status : 502 return NextResponse.json({ error: err.code, errors: err.errors }, { status }) } throw err } } ``` ```tsx // app/estimate/Hearing.tsx 'use client' import { useEffect, useState } from 'react' import type { AnswerValue, MeasureValue, Progress, Question, Result } from '@/lib/assessment' interface View { questions: Question[] result: Result progress: Progress } /** もう書き込めないことを表すコード。最初からやり直す。 */ const GONE = ['no_session', 'not_found', 'session_closed', 'assessment_fixed'] async function send(body: object): Promise<{ view: View | null; closed?: boolean }> { const res = await fetch('/api/hearing', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) const json = await res.json() if (!res.ok) throw new Error(json.error ?? `HTTP ${res.status}`) return json } const numberFormat = new Intl.NumberFormat('ja-JP') function formatRange(m: MeasureValue): string { // `currency` に通貨コードは付かない。Minion はこの金額を円として表示している。 const one = (n: number) => m.display === 'currency' ? `¥${numberFormat.format(Math.round(n))}` : `${numberFormat.format(Math.round(n))}${m.unit ? ` ${m.unit}` : ''}` return m.settled ? one(m.value.min) : `${one(m.value.min)} 〜 ${one(m.value.max)}` } /** * 金額を見せてよい段階か。序盤の幅はとても広く、そのまま出すと「見当がついていない」と * 読まれる。ここの基準は一例なので、サイトに合わせて調整する。 */ function worthShowing(view: View): boolean { if (view.questions.length === 0) return true const primary = view.result.measures.find((m) => m.primary) ?? view.result.measures[0] if (!primary) return false if (primary.settled) return true const { min, max } = primary.value return view.progress.answered >= 3 && min > 0 && max / min <= 2 } export default function Hearing() { const [view, setView] = useState(null) const [loading, setLoading] = useState(true) const [busy, setBusy] = useState(false) const [sent, setSent] = useState(false) const [error, setError] = useState(null) useEffect(() => { fetch('/api/hearing') .then((res) => res.json()) .then((json) => setView(json.view)) .catch(() => setError('load_failed')) .finally(() => setLoading(false)) }, []) async function run(body: object) { setBusy(true) setError(null) try { const next = await send(body) setView(next.view) if (next.closed) setSent(true) } catch (err) { const code = err instanceof Error ? err.message : String(err) if (GONE.includes(code)) setView(null) else setError(code) } finally { setBusy(false) } } if (loading) return

読み込み中…

if (sent) return

ありがとうございました。内容を確認のうえ、ご連絡します。

if (!view) { return ( ) } // limit=1 なので設問は多くても 1 件。空配列なら、もう聞くことは残っていない。 const question = view.questions[0] return (
{question ? ( run({ action: 'answer', fact_id: question.fact_id, value })} onUnknown={() => run({ action: 'unknown', fact_id: question.fact_id })} /> ) : (

お伺いしたいことは以上です。

)} {error &&

エラーが発生しました ({error})。もう一度お試しください。

} {worthShowing(view) && }
) } function QuestionForm({ question, busy, onAnswer, onUnknown, }: { question: Question busy: boolean onAnswer: (value: AnswerValue) => void onUnknown: () => void }) { const [raw, setRaw] = useState('') return (
{question.topic &&

{question.topic}

}
{question.label} {question.description &&

{question.description}

} {question.type === 'boolean' && ( <> )} {question.type === 'select' && question.options?.map((option) => ( // 送るのは選択肢の value。ラベルではない。 ))} {question.type === 'number' && (
{ e.preventDefault() const n = Number(raw) // Number('') は 0 になるので、空欄は明示的に弾く。 if (raw.trim() !== '' && Number.isFinite(n)) onAnswer(n) }} > setRaw(e.target.value)} /> {question.unit && {question.unit}}
)}
) } function ResultPanel({ result }: { result: Result }) { return (
{result.measures.map((m) => (

{m.group ? `${m.group} · ` : ''} {m.label}: {m.primary ? {formatRange(m)} : formatRange(m)}

))} {result.verdict &&

{result.verdict.label}

} {result.pending.length > 0 && ( <>

確認が必要な点

    {result.pending.map((p) => (
  • {p.label}
  • ))}
)}
) } ``` ```tsx // app/estimate/page.tsx import Hearing from './Hearing' export default function EstimatePage() { return (

お見積もり

) } ``` ### Astro (サーバー出力、クライアント側 JavaScript なし) ただの HTML フォームで、1 回の読み込みに 1 問です。回答はすべて POST のあとにリダイレクトするので、 再読み込みで回答が二重に送られることはありません。 ```js // astro.config.mjs import { defineConfig } from 'astro/config' import node from '@astrojs/node' export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone' }), // サイトを配信するホスト名を列挙する。これが無いと Astro はリクエストのホストを信用せず、 // Astro.url が localhost になり、フォームの POST がすべて // 403 "Cross-site POST form submissions are forbidden" で拒否される。 security: { allowedDomains: [{ hostname: 'www.example.com', protocol: 'https' }] }, }) ``` ```astro --- // src/pages/estimate.astro // サーバーアダプタ (@astrojs/node など) が必要。クライアント側の JavaScript は使わない。 import { assessment, AssessmentApiError, type AnswerValue, type MeasureValue, type SessionView, } from '../lib/assessment' export const prerender = false const COOKIE = 'assessment_session' const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i /** もう書き込めないことを表すコード。最初からやり直す。 */ const GONE = ['not_found', 'session_closed', 'assessment_fixed'] const cookie = Astro.cookies.get(COOKIE)?.value const id = cookie && UUID.test(cookie) ? cookie : null let error: string | null = null if (Astro.request.method === 'POST') { const form = await Astro.request.formData() const action = String(form.get('action') ?? '') try { if (action === 'start') { // 確認する人が見分けられるタイトルにする。後から変更できない。 const stamp = new Date().toISOString().slice(0, 16).replace('T', ' ') const started = await assessment.start(`Web サイト見積 ${stamp}`) Astro.cookies.set(COOKIE, started.session.id, { httpOnly: true, secure: true, sameSite: 'lax', path: '/', maxAge: 60 * 60 * 24 * 7, }) return Astro.redirect('/estimate', 303) } if (id && action === 'answer') { const factId = String(form.get('fact_id')) if (form.get('unknown') === '1') { await assessment.unknown(id, factId) return Astro.redirect('/estimate', 303) } // フォームの値は常に文字列。設問の型に変換してから送る。 const type = String(form.get('type')) const raw = String(form.get('value') ?? '') let value: AnswerValue | null = raw if (type === 'boolean') value = raw === 'true' if (type === 'number') value = raw.trim() !== '' && Number.isFinite(Number(raw)) ? Number(raw) : null if (value === null) { error = '数値を入力してください。' } else { await assessment.answer(id, factId, value) return Astro.redirect('/estimate', 303) } } if (id && action === 'close') { await assessment.close(id) Astro.cookies.delete(COOKIE, { path: '/' }) return Astro.redirect('/estimate?sent=1', 303) } } catch (err) { if (!(err instanceof AssessmentApiError)) throw err if (!GONE.includes(err.code)) { console.error('[assessment]', err.status, err.code, err.message) error = 'エラーが発生しました。もう一度お試しください。' } } } let view: SessionView | null = null if (id) { try { view = await assessment.get(id) // 閉じた、または Minion 側で確定された。止まったフォームを見せず、最初からにする。 if (view.session.state !== 'in_progress') view = null } catch (err) { if (!(err instanceof AssessmentApiError) || err.status !== 404) throw err } if (!view) Astro.cookies.delete(COOKIE, { path: '/' }) } // limit=1 なので設問は多くても 1 件。空配列なら、もう聞くことは残っていない。 const question = view?.questions[0] const sent = Astro.url.searchParams.has('sent') && !view const numberFormat = new Intl.NumberFormat('ja-JP') function formatRange(m: MeasureValue): string { // `currency` に通貨コードは付かない。Minion はこの金額を円として表示している。 const one = (n: number) => m.display === 'currency' ? `¥${numberFormat.format(Math.round(n))}` : `${numberFormat.format(Math.round(n))}${m.unit ? ` ${m.unit}` : ''}` return m.settled ? one(m.value.min) : `${one(m.value.min)} 〜 ${one(m.value.max)}` } /** 序盤の幅はとても広い。ここの基準は一例なので、サイトに合わせて調整する。 */ function worthShowing(v: SessionView): boolean { if (v.questions.length === 0) return true const primary = v.result.measures.find((m) => m.primary) ?? v.result.measures[0] if (!primary) return false if (primary.settled) return true const { min, max } = primary.value return v.progress.answered >= 3 && min > 0 && max / min <= 2 } ---

お見積もり

{sent &&

ありがとうございました。内容を確認のうえ、ご連絡します。

} {error &&

{error}

} {!view && !sent && (
)} {question && (
{question.topic &&

{question.topic}

}
{question.label} {question.description &&

{question.description}

} {question.type === 'boolean' && ( <> )} {question.type === 'select' && question.options?.map((option) => ( ))} {question.type === 'number' && ( <> {question.unit && {question.unit}} )}
)} {view && !question && (

お伺いしたいことは以上です。

)} {view && worthShowing(view) && (
{view.result.measures.map((m) => (

{m.group ? `${m.group} · ` : ''}{m.label}: {m.primary ? {formatRange(m)} : formatRange(m)}

))} {view.result.verdict &&

{view.result.verdict.label}

} {view.result.pending.length > 0 && ( <>

確認が必要な点

    {view.result.pending.map((p) =>
  • {p.label}
  • )}
)}
)}
``` ### 言語モデルを前に置く チャットボットでもループは同じです。言語モデルの仕事は 2 つだけで、設問を訪問者向けの言葉にすることと、 訪問者の返事を値に変換することです。順番は Minion が持ったままです。 訪問者と話すプロンプトに持たせる決まりは 3 つです。 - **渡された設問だけを聞く。** モデルが自分で質問を足さない。 - **1 回の発言で 1 問。** まとめて聞くと、返事を値に変換できなくなる。 - **訪問者が答えていないことを埋めない。** 推測は「未確定が無いように見えて間違っている結果」になる。 「不明」なら、確認する人から見える場所に空白が残る。 返事を値に変換するときは、設問をモデルに渡して JSON で答えさせます。 ```text 構造化されたヒアリングの回答を 1 件記録します。質問はしないでください。 設問: デザインの進め方 種類: select — 次の value のうちちょうど 1 つで答える: opt_new_design: 新規デザイン opt_reuse: 既存デザイン踏襲 訪問者の返事: """基本的には今のデザインのままでいいです""" 次のいずれかの JSON だけを返してください: {"value": <回答>} 返事が設問にはっきり答えている {"unknown": true} 分からない、または答えたくない {"unclear": true} 判断できない、または幅で答えている 推測はしないでください。 ``` `boolean` なら `true` か `false`、`number` なら 1 つの数 (`unit` があればその単位で) を求めます。 モデルの出力は、送る前に設問と突き合わせてください。モデルは value の代わりにラベルを、数値の 代わりに文字列を返すことがあり、API はどちらも拒否します。 ```ts // lib/check-reply.ts import type { AnswerValue, Question } from './assessment' /** * モデルが取り出した値を、送る前に設問と突き合わせる。 * * - `{ value }` — 設問の型に合う。回答として送る * - `{ unknown }` — 相手が分からない / 答えたくない。`unknown` として送る * - `null` — モデルが判断できなかった。推測せず、相手に聞き直す */ export function checkReply( question: Question, reply: unknown, ): { value: AnswerValue } | { unknown: true } | null { if (!reply || typeof reply !== 'object') return null if ('unknown' in reply && reply.unknown === true) return { unknown: true } if (!('value' in reply)) return null const { value } = reply switch (question.type) { case 'boolean': return typeof value === 'boolean' ? { value } : null case 'number': return typeof value === 'number' && Number.isFinite(value) ? { value } : null case 'select': // 選択肢の value のどれかでなければならない。ラベルや、それに近い文字列は不可。 return typeof value === 'string' && question.options?.some((o) => o.value === value) ? { value } : null } } ``` `checkReply` が `null` を返したら、言い方を変えて聞き直してください。2 回目もはっきりしなければ、 不明として送って次へ進みます。会話そのものを保存する場所は Minion にはありません。記録が必要なら サイト側で残してください。 ## やってはいけないこと | ❌ | ✅ | なぜ | |---|---|---| | ブラウザの JavaScript から API を呼ぶ、キーを `NEXT_PUBLIC_` / `PUBLIC_` の変数に置く | サーバーから呼ぶ | キーが誰にでも読める。そもそも CORS ヘッダが無いので呼べない | | キーを `?apiKey=` で送る | `Authorization: Bearer` | クエリ文字列のキーは受け付けず、ログにも残る | | ブラウザから来たパスやセッション ID をそのまま転送する | 訪問者の Cookie にあるセッション ID にだけ作用させる | ID さえ分かれば他人のヒアリングを読める・消せる | | ページを開いた時点でヒアリングを始める | 訪問者が「はじめる」を押してから始める | 1 件ごとにワークスペースに記録が残る | | 再読み込みで新しいヒアリングを始める、タイムアウト後に `POST /sessions` を再試行する | ID を保存して `GET /sessions/{id}` で再開する | 開始は冪等ではないので重複する | | 設問を並べ替える・飛ばす・足す、`/catalog/facts` を上から順に聞く | `questions[0]` を聞く | 返ってきた順に聞くことが聞き漏れを防ぐ | | 設問の数を数えて終わりを判断する | `questions` が `[]` になったら終わる | 設問の数は回答で変わる | | `"12"`、選択肢のラベル、`"はい"` を送る | `12`、選択肢の `value`、`true` | `400 validation_failed` | | `"unknown": "fct_x"` やフォーム形式の本文を送る | JSON の本文で `"unknown": ["fct_x"]` | 形の崩れた部分は無視され、`200` で何も記録されない | | はっきりしない返事をモデルに推測させる | 聞き直し、それでもだめなら不明にする | 推測は、空白の見えない間違った結果になる | | 1 問目から幅を見せる | 金額を見せる時点を決める | 序盤の幅は何倍にもなりうる | | `questions` が空になったら基準以外の軸も確定値として見せる | `/facts` で残りの事実を聞くか、基準の軸だけを見せる | 設問の順位は基準の軸だけで決まる | | `message` を訪問者に見せる | `error` を自分の文言に対応づける | `message` は多言語化されていない | | サイトから確定しようとする | 閉じて、人が確定する | 確定は人の判断で、そう設計してある | | 複数言語のサイトで 1 つのカタログを使う | 言語ごとにカタログとキーを分ける | カタログの文面は 1 言語 | --- # アセスメント Source: https://docs.minionworkspace.com/ja/guides/assessment/ Summary: ヒアリングで聞き取った事実から答えを出し、自社サイトやボットの訪問者にも設問に答えてもらう アセスメントは、ヒアリングで聞き取った事実から結果 (見積金額や適合度) を算出し、まだ答えの出ていない設問のうち**どれを聞くと結果が一番動くか**を示す機能です。 上から順に読み上げないヒアリングのために作られています。次に聞くべきことはそれまでに分かったことで変わるので、設問は固定の一覧ではありません。 - **1 問目から答えが出ます。** 未確定が残っているあいだ結果は幅で表され、事実が埋まるにつれて狭まります。 - **「不明」も答えのひとつです。** その設問は二度と聞かれず、確認事項として残ります。 - **設問の順番は自動で決まります。** 次に出るのは、答えると結果の幅が一番縮む設問です。それまでの回答で関係なくなった設問は消えます。 - **自社のノウハウは外に出ません。** 何がどれだけ結果に効くか、賃率はいくらか、といった知識はカタログの中にあり、外部連携の API を含めて Minion の外へは出ません。 アセスメントは experimental で、既定では無効です。サイドバーに出ていない場合はお問い合わせください。 :::tip[自社サイトやボットでヒアリングを回しますか?] [アセスメントの組み込み](https://docs.minionworkspace.com/ja/guides/assessment-integration/) は、API・JSON の形・Next.js / Astro の 実装例・言語モデルを前に置く方法・やってはいけないことを 1 ページに閉じたものです。 **そのページだけを開発者 (あるいはコーディング支援の AI) に渡せば組み込める**ように書いてあります。 言語モデル向けのプレーンテキスト版は `/ja/llms-assessment.txt` にあります。 ::: ## 構成要素 | 用語 | 説明 | |---|---| | **カタログ** | ひとつの種類の判断を表す、使い回せるモデル。事実・寄与項目・出力の軸と、必要なら判定を持ちます。**アセスメント → カタログ管理** にあります | | **事実** | 聞き取る内容。はい / いいえ、数値、選択のいずれか | | **寄与項目** | 結果に何がどれだけ入るか、いつ入るか。「多言語対応が必要なら、多言語ルーティングに 16 時間」のようなもの。自社のノウハウなので Minion の外へは出ません | | **出力の軸** | カタログが出す答え。初期費用、月額、適合度など。通貨または数値で表示します | | **判定** | 任意。軸の値から合格 / 要検討 / 不合格を出します。スクリーニングなどで使います | | **アセスメント** | 1 件の案件。ここまでの回答と、そこから出る結果 | いちばん早い始め方は **サンプルを追加** です。完成したカタログ (ロゴ制作の見積、Web サイト制作の見積、案件の受注判断、採用スクリーニング) がワークスペースに複製されるので、中身を読んでから自分用に直してください。 カタログは下書きとして編集し、**公開**してはじめて効きます。使われるのは公開済みの版だけで、Minion の中のアセスメントでも外部連携でも同じです。 **白紙から作成すると、名前と一緒に識別子** (`web-production` のような名札) **を聞かれます。** 英語の名前なら自動で埋まり、日本語の名前からは作れないので自分で決めることになります。ここは少し考える価値があります。識別子は**カタログを作った後は変えられません** — 外部連携が「話している相手が想定どおりのカタログか」を確かめるために焼き付ける値なので、動かせるようにすると名前を直しただけで誤警報になるためです。後から見るときは **連携** の画面か、カタログ一覧の名前の下にあります。サンプルは識別子を持って複製されます。 ## 自社サイトでヒアリングを回す カタログは Minion の外から使えます。Web サイトの見積フォーム、チャットボット、自社の業務システムなどです。訪問者はそこで設問に答え、ヒアリングは 1 件ずつアセスメントの一覧に入り、担当者が確認します。 ### 外部連携から見えるもの・見えないもの | Minion の外に出るもの | 外に出ないもの | |---|---| | 設問: 名称・説明・種類・単位・選択肢・話題 | 寄与項目と、それぞれがどれだけ効くか | | 提示する結果: 軸ごとの幅 | 寄与項目がいつ含まれるかの条件 | | 判定のラベルと水準 | 賃率・販管費率・目標粗利率 | | まだ確定していない事実 | 内訳と、価格に換算する前の工数 | ひとつ注意があります。**キーを持っている人はカタログの設問をすべて読めます。** 設問だけでも「何に値段を付けているか」は分かります。キーは制作したサイトのサーバーに置き、ブラウザには決して渡さないでください。 ### 連携キーを発行する 1. カタログを**公開**します。一度も公開していないカタログにはキーを発行できません。 2. カタログを開き、**連携** に切り替えます。 3. **外部連携キー** で、使う場所がわかる名前 (「コーポレートサイトの見積フォーム」など) を付けて **キーを発行** を押します。 4. **その場でキーを控えてください。** キーが表示されるのはこのときだけで、Minion にはハッシュしか残りません。 「代表値が未設定の数値の事実があります」という警告が出ても、キーはそのまま使えます。ただし設問の順番の精度が落ちるので、該当する事実に **代表値** を設定して公開し直してください。 キーの振る舞い: - **有効なキーが 1 本でもあるカタログは、外部から使えます。すべて失効させれば閉じます。** 別の公開スイッチはありません。 - **キーは使う場所ごとに発行してください。** キーが届くのは自分が始めたヒアリングだけなので、Web サイトとチャットボットが互いのヒアリングを見ることはありません。Minion の画面で作ったアセスメントにも、どのキーからも届きません。 - **キーを失効させると、その連携はすぐに止まります。** そのキーで始まったヒアリングは一覧に残ります。 - **カタログを削除すると、キーも削除されます。** 確認ダイアログに有効なキーの本数が出ます。 - **ワークスペースでアセスメントを無効にすると、すべてのキーが止まります。** カタログの判定が存在しない実装を指している間は、キーを発行できません。カタログの編集画面にその旨が表示されます。 ### 入ってきたヒアリングを確認する キーから始まったヒアリングは、一覧の中では通常のアセスメントと同じで、**外部** の印が付きます。カタログ・出所 (**画面から作成** / **外部連携から**)・状態で絞り込めます。回答が 1 件も無いアセスメント (多くは 1 問目で離脱した訪問者) は既定で隠れており、**未回答も表示** で出せます。 | 状態 | 意味 | 次に動くのは | |---|---|---| | **進行中** | 記入中 | 訪問者、または記入している人 | | **確認待ち** | サイトがヒアリングを終えた | 担当者: 内容を確かめて確定する | | **確定** | 人が確かめ、結果を凍結した | — | - **サイトがヒアリングを終えると通知が届きます** (回答が 1 件以上ある場合)。ワークスペースの全メンバーにアプリ内で届きます。メールは既定で無効で、通知設定で有効にできます。 - **確定するのは常に人です。** 外部連携からは確定できません。確定すると、サイトはそのヒアリングを読むことはできますが、変更も削除もできなくなります。 - サイトが終えたヒアリングの**確定を解除**すると、進行中ではなく **確認待ち** に戻ります。人の確認がまだ必要だからです。 - 確認待ちのアセスメントも中身は他と同じです。開いて回答を直し、訪問者が分からなかった点を埋めてから確定してください。 ### ヒアリング中にカタログを変更する 確定するまで、アセスメントはカタログの**公開済みの版**に追従します。変更を公開すると、進行中のヒアリングは次のリクエストからそれを反映します。追加した事実は聞かれるようになり、削除した事実は聞かれなくなり、すでに答えた内容はそのまま残ります。確定したアセスメントは確定時の版を使い続けます (API から読んだ場合も同じです)。未公開の下書きは何にも影響しません。 訪問者がすでに選んだ選択肢を削除しても、その回答はそのまま残ります。 ### 個人情報 回答は、はい / いいえ・数値・選択だけで、自由記述の回答はありません。外部連携が保存できるテキストは、ヒアリングを始めるときに付ける**タイトル**だけです。 サイトで氏名やメールアドレスも集める場合は、サイト側で保管してください。突き合わせたいときは、タイトルに会社名など見分けのつくものを入れられます。ワークスペースに保存されることを前提にしてください。 ヒアリングを消したいときは、外部連携から自分が始めたヒアリングを削除できます (確定済みを除く)。一覧からは、ワークスペースの誰でも削除できます。 ### 制約 - **カタログの文面は 1 言語です** (書いた言語のまま)。複数言語のサイトでは、言語ごとにカタログを作り、それぞれにキーを発行してください。 - **API はサーバーから呼ぶものです。** CORS ヘッダを返さないのでブラウザから直接は呼べず、キーをブラウザに渡してはいけません。 - **レート制限** (1 分あたり): キーごとに 300 リクエスト、キーごとに新しいヒアリング 60 件、1 件のヒアリングへの変更 20 回。