1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GASでAI返信案を安全にプレビューする:HTMLエスケープと表示テスト

1
Posted at

AIに返信案を作らせても、顧客へ送る前に人が画面で確認する工程は残します。その確認画面で返信案をHTMLとして解釈すると、本文中の記号が表示崩れしたり、意図しないタグとして扱われたりします。

この記事では、Google Apps Script(GAS)のHTML Serviceで返信案をプレビューする最小構成を作ります。目的は送信の自動化ではなく、返信案を文字として表示し、表示結果をテストしてから人へ渡すことです。AI APIの呼び出し、顧客データの保存、送信承認の代行は扱いません。

作るもの

AI返信案を人間確認へ渡す表示ゲート

次の流れだけを実装します。

  1. Sheetsなどから返信案を読む
  2. HTML Serviceのテンプレートへ表示用データを渡す
  3. 本文をHTMLとして解釈させず、文字として表示する
  4. 記号・改行・空欄のテストを通す
  5. 表示確認後も、送信可否は人が決める

プレビュー画面の状態は、送信権限とは別に管理します。たとえば READY_FOR_REVIEW は「画面に表示できる」という意味であり、「顧客へ送信してよい」という意味にはしません。

まず表示契約を決める

返信案の表示前に、次の値を契約にします。

項目 許可する状態 それ以外
本文 文字列 HOLD
案件ID 空でない文字列 HOLD
表示状態 READY_FOR_REVIEW 送信処理へ進めない
HTML テンプレート側でエスケープ 未エスケープ挿入を禁止

GASのHTMLテンプレートで != value ?> を使うと、値をエスケープせずに挿入できます。AI出力や顧客入力のように信頼できない文字列には使いません。

GAS側の最小実装

まず、シートの1行を表示用モデルへ変換します。表示に必要な値だけを取り出し、送信処理は呼びません。

const PREVIEW_STATUS = 'READY_FOR_REVIEW';

function getReplyPreview(inquiryId) {
  const row = findReplyRow_(inquiryId);
  if (!row) throw new Error('reply_preview_not_found');

  const body = String(row.reply_body || '');
  const status = String(row.review_status || '');
  if (!body || status !== PREVIEW_STATUS) {
    throw new Error('reply_preview_hold');
  }

  return {
    inquiryId: String(row.inquiry_id),
    body,
    status,
    canSend: false,
  };
}

function showReplyPreview(inquiryId) {
  const template = HtmlService.createTemplateFromFile('reply-preview');
  template.model = getReplyPreview(inquiryId);
  return template.evaluate().setTitle('返信案の確認');
}

function doGet(event) {
  const inquiryId = String(event.parameter.inquiryId || '');
  return showReplyPreview(inquiryId);
}

function findReplyRow_(inquiryId) {
  // 実運用では列名で行を読み、案件IDの完全一致と重複を検査する。
  const fixtures = {
    'Q-1001': {
      inquiry_id: 'Q-1001',
      reply_body: 'ご連絡ありがとうございます。\\n確認して折り返します。',
      review_status: PREVIEW_STATUS,
    },
  };
  return fixtures[inquiryId] || null;
}

HTMLテンプレートでは通常の出力を使う

ファイル名を reply-preview.html として、本文をHTMLとして挿入しないテンプレートにします。

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <style>
      .reply-body { white-space: pre-wrap; overflow-wrap: anywhere; }
    </style>
  </head>
  <body>
    <h1>返信案の確認</h1>
    <p>案件ID: <?= model.inquiryId ?></p>
    <p class="reply-body"><?= model.body ?></p>
    <p>状態: <?= model.status ?></p>
    <p>この画面は表示確認用です。送信可否は別の人間確認で判断します。</p>
  </body>
</html>
  1. = model.body ?> はHTML Serviceの通常出力に任せる
  2. white-space: pre-wrap で改行を保持する
  3. overflow-wrap: anywhere で長いIDやURLの横はみ出しを抑える
  4. != model.body ?> へ置き換えない

テンプレートで文脈に応じたエスケープを使う場合、サーバー側で同じ値を二重にエスケープしません。手作業の変換とテンプレートの通常出力を混在させると、< や & のような文字が二重変換され、確認者が見た本文と元データの対応を追いにくくなります。

ブラウザ側で値を入れる場合

クライアント側JavaScriptで値を入れる場合は、innerHTML ではなく textContent を使います。

const preview = document.querySelector('#reply-body');
preview.textContent = serverModel.body;

// 使わない
// preview.innerHTML = serverModel.body;

HTMLを許可する仕様が別途ある場合でも、許可タグの一覧、除去処理、テスト、責任者を先に決めます。AI返信案のプレビューでは、HTMLを許可しない方が契約を小さくできます。

表示境界をテストする

きれいな文章だけでなく、記号を含む架空データを使います。顧客の実データやアクセストークンはテストへ入れません。

ケース 入力例 確認すること
通常文 確認して折り返します。 文章と改行が読める
HTML記号 重要 & "確認" タグにならず記号のまま
改行 一行目\n二行目 改行位置が保持される
長い値 連続したIDやURL 横幅からはみ出さない
空欄 空文字 表示せずHOLDになる
状態不一致 DRAFTED プレビューを返さずHOLDになる

簡単なテストを、表示ロジックの変更前後で実行します。

function assertEqual_(actual, expected, label) {
  if (actual !== expected) {
    throw new Error(label + ': expected=' + expected + ', actual=' + actual);
  }
}

function testReplyPreviewFixtures_() {
  const normal = getReplyPreview('Q-1001');
  assertEqual_(normal.status, 'READY_FOR_REVIEW', 'status');
  assertEqual_(normal.canSend, false, 'canSend');
  if (!normal.body.includes('\\n')) {
    throw new Error('newline must remain in the source text');
  }
}

function testReplyPreviewHold_() {
  let failed = false;
  try {
    getReplyPreview('missing');
  } catch (error) {
    failed = error.message === 'reply_preview_not_found';
  }
  if (!failed) throw new Error('missing inquiry must be held');
}

運用上の境界を分ける

HTMLエスケープを実装しても、返信内容が正しいとは限りません。表示の安全性と業務上の承認を別の判定にします。

  • 表示できる:本文を文字として確認画面に出せる
  • 確認待ち:担当者が根拠、宛先、期限、対応範囲を見る
  • 送信可能:必要な人間確認と承認記録がそろっている
  • HOLD:本文空欄、案件ID不一致、状態不正、根拠不足などで人へ戻す

GASに送信処理を追加する場合も、プレビュー関数の canSend: false を、送信ボタンの表示だけで true へ変えないでください。送信権限、承認者、本文の版、確認時刻を別の台帳で照合します。

公開前チェックリスト

  • AI出力・顧客入力を innerHTML や != ... ?> で直接挿入していない
  • HTML記号、アンパサンド、引用符、改行の架空テストを実行した
  • 空欄・案件ID不一致・状態不一致をHOLDにした
  • 表示確認と送信承認を別の状態として記録した
  • 顧客情報、トークン、Cookie、個人情報をテストデータに入れていない
  • 表示結果が正しくても、事実確認や宛先確認を省略していない

HTMLとして見せないという小さな契約を先に置くと、プレビュー画面のテスト範囲と人へ戻す条件を決めやすくなります。AI返信の便利さより先に、確認者が見た文字列を正本の本文と照合できることを優先します。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?