マニュアルがない部分を含めて自動化できる範囲を広げる
※現在のFAQのところに知識データを何某おけば判定処理で参照するようにしてます。
カテゴリーによってはその顧客の診察などの内容次第になるので、これで作る知識で回答すべきかどうかの分岐はzendesk側でやるのがいいかと思います。
知識はCSVにできればdifyに突っ込むだけなのでこの処理を一旦やってひろつさんにガーっとみてもらって知識基盤を拡充をしてバージョン1リリースするのがいいかと思います。
その後は月次くらいで更新すればいいかと思います。
GASだけでやれるのでGASで完結させてます。
※課金走ってしまうのでOpen AIのキーを付け替えてもらうとAI処理部分動きます。
※OpenAIキーは各自のものを設定してください。
/************ 認証変数 ************/
const ZD_SUBDOMAIN = 'emostyle'; // 例: greenspoonsupport
const ZD_EMAIL = '[email protected]';
const ZD_API_TOKEN = 'Xn6M1p5WcwO9TwSS9eDai9p11M6xxQAU0XsxqZaS'; // 必ず新しく発行したAPIトークンを使用
const SHEET_NAME = 'Comments_ALL'; // 出力先のシート名
const BACK_DAYS = 3; // 過去◯日分のチケットを対象(例:3日)
/***********************************************/
const AUTH_HEADER = {
'Authorization': 'Basic ' + Utilities.base64Encode(`${ZD_EMAIL}/token:${ZD_API_TOKEN}`)
};
// time-based incremental API は1分に1回しか叩けない制限 → 安全策で65秒空ける
const SLEEP_MS = 65 * 1000;
/**
* ユーザーの role をキャッシュするマップ
* key: user_id (number), value: role文字列 ("agent", "end-user" など)
*/
let userRoleMap = {}; // グローバル変数(Apps Script 上では実行ごとにリセットされます)
/**
* =============================
* メインエントリポイント
* =============================
*/
function exportDescriptionAndComments() {
const startTime = Math.floor(Date.now() / 1000 - BACK_DAYS * 86400);
Logger.log(`startTime=${startTime}, now=${Math.floor(Date.now()/1000)}, days=${BACK_DAYS}`);
const sheet = prepSheet_();
// ① チケット一覧を取得
// ticket_id → subject
// ticket_id → ticket_created_at
const { ticketIds, subjectMap, createdMap } = fetchTicketsAndSubjects_(startTime);
Logger.log(`Total tickets fetched: ${ticketIds.length}`);
// ② 監査ログで description + comments を取得
ticketIds.forEach((tid, idx) => {
Logger.log(`[${idx+1}/${ticketIds.length}] ticket #${tid}`);
fetchTicketAudits_(tid, sheet, subjectMap, createdMap);
// 1 秒ほど待つ(audits API は比較的レート制限緩め → 700/min 程度)
Utilities.sleep(100);
});
SpreadsheetApp.getUi().alert('完了しました!');
}
/**
* =============================
* 1) チケット一覧の取得
* - ticket_id / subject / created_at をマップ化
* =============================
*/
function fetchTicketsAndSubjects_(startTime) {
let url = `https://${ZD_SUBDOMAIN}.zendesk.com/api/v2/incremental/tickets.json?start_time=${startTime}`;
const ticketIds = [];
const subjectMap = {};
const createdMap = {}; // { ticketId: new Date(...) }
while (url) {
const { code, data } = callZendesk_(url);
if (code !== 200) {
throw new Error(`tickets API error: HTTP ${code}`);
}
(data.tickets || []).forEach(t => {
ticketIds.push(t.id);
subjectMap[t.id] = t.subject || '';
if (t.created_at) {
createdMap[t.id] = new Date(t.created_at); // Date型に変換
} else {
createdMap[t.id] = null; // ない場合は null
}
});
if (data.end_of_stream) break;
url = data.next_page;
// 1分待機
Utilities.sleep(SLEEP_MS);
}
return { ticketIds, subjectMap, createdMap };
}
/**
* =============================
* 2) 監査ログを取得し
* description / Comment を行ごとに組み立て→シート書込
* =============================
*
* @param {number} ticketId
* @param {Sheet} sheet - 出力シート
* @param {object} subjectMap - { ticketId: subject }
* @param {object} createdMap - { ticketId: Date(ticketCreatedAt) }
*/
function fetchTicketAudits_(ticketId, sheet, subjectMap, createdMap) {
let url = `https://${ZD_SUBDOMAIN}.zendesk.com/api/v2/tickets/${ticketId}/audits.json?sort_order=asc`;
while (url) {
try {
const { code, data } = callZendesk_(url);
if (code === 404) {
// チケット削除・アクセス不能 → スキップ
Logger.log(`Ticket ${ticketId} => 404 Not Found. Skipping.`);
break;
}
if (code !== 200) {
throw new Error(`audits API error: HTTP ${code} for ticket ${ticketId}`);
}
const rows = [];
(data.audits || []).forEach(audit => {
const commentDate = new Date(audit.created_at); // コメントの発生日時
const events = audit.events || [];
events.forEach(ev => {
if (ev.type === 'Comment') {
// 回答/追記コメントイベント
const text = ev.body || ev.plain_body || ev.html_body || '';
const role = getUserRole_(ev.author_id); // end-user or agent など
rows.push([
// [0] ticket_id
ticketId,
// [1] subject
subjectMap[ticketId] || '',
// [2] ticket_created_at
createdMap[ticketId],
// [3] comment_datetime
commentDate,
// [4] comment_author_role
role,
// [5] author_id
ev.author_id,
// [6] comment_body
text.replace(/\r?\n/g, '⏎')
]);
} else if (ev.type === 'Create' && ev.field_name === 'description') {
// チケット作成時の本文
const desc = ev.value || '';
const role = getUserRole_(audit.author_id); // 作成者もaudits.author_id
rows.push([
ticketId,
subjectMap[ticketId] || '',
createdMap[ticketId],
commentDate, // createイベントの発生日時
role,
audit.author_id,
desc.replace(/\r?\n/g, '⏎')
]);
}
});
});
if (rows.length) {
sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, rows[0].length).setValues(rows);
Logger.log(`Ticket ${ticketId}: wrote ${rows.length} rows`);
} else {
Logger.log(`Ticket ${ticketId}: no description or comments in this page.`);
}
if (!data.next_page) {
break;
}
url = data.next_page;
// 次ページへ 1秒待機
Utilities.sleep(1000);
} catch (err) {
Logger.log(`Error on ticket ${ticketId}: ${err.message}`);
// このチケットはスキップして次へ
break;
}
}
}
/**
* =============================
* ユーザーの role を取得(キャッシュあり)
* /api/v2/users/{id}.json を呼び出し role を取得する
* =============================
*/
function getUserRole_(userId) {
if (!userId) {
return 'unknown';
}
if (userRoleMap[userId]) {
return userRoleMap[userId];
}
// 初回だけ呼び出す
const url = `https://${ZD_SUBDOMAIN}.zendesk.com/api/v2/users/${userId}.json`;
const { code, data } = callZendesk_(url);
if (code === 200 && data.user) {
const role = data.user.role || 'unknown'; // "end-user", "agent", "admin" 等
userRoleMap[userId] = role;
return role;
} else {
userRoleMap[userId] = 'unknown';
return 'unknown';
}
}
/**
* =============================
* Zendesk API 共通コール
* =============================
*/
function callZendesk_(url) {
const response = UrlFetchApp.fetch(url, {
headers: AUTH_HEADER,
muteHttpExceptions: true
});
return {
code: response.getResponseCode(),
data: JSON.parse(response.getContentText() || '{}')
};
}
/**
* =============================
* 出力シート初期化
* カラム =
* [ ticket_id,
* subject,
* ticket_created_at,
* comment_datetime,
* comment_author_role,
* author_id,
* comment_body ]
* =============================
*/
function prepSheet_() {
const ss = SpreadsheetApp.getActive();
let sh = ss.getSheetByName(SHEET_NAME);
if (!sh) {
sh = ss.insertSheet(SHEET_NAME);
}
if (sh.getLastRow() === 0) {
sh.appendRow([
'ticket_id',
'subject',
'ticket_created_at',
'comment_datetime',
'comment_author_role',
'author_id',
'comment_body'
]);
}
return sh;
}