Googleスプレッドシートを新規作成 →「拡張機能」→「Apps Script」 に下記を貼り付けて保存。「プロジェクトの設定」→「スクリプト プロパティ」で
API_TOKEN にお店の合言葉を設定
// ソナレア カルテOCR 中継 GAS v2(Claude / Anthropic)
// v2: 合言葉をコードに書かず、スクリプトプロパティ API_TOKEN と照合するようにした
var SHARED_PASS = PropertiesService.getScriptProperties().getProperty('API_TOKEN') || ''; // 合言葉はスクリプトプロパティに保管(コードには書かない)
var MODEL = 'claude-opus-4-8';
function json_(o){
return ContentService.createTextOutput(JSON.stringify(o))
.setMimeType(ContentService.MimeType.JSON);
}
function doPost(e){
var data;
try { data = JSON.parse(e.postData.contents); }
catch (err) { return json_({ ok:false, error:'bad json' }); }
if (!SHARED_PASS || data.pass !== SHARED_PASS) return json_({ ok:false, error:'unauthorized' });
if (data.action !== 'ocr') return json_({ ok:false, error:'unknown action' });
var key = PropertiesService.getScriptProperties().getProperty('ANTHROPIC_API_KEY');
if (!key) return json_({ ok:false, error:'APIキー未設定(スクリプトプロパティ ANTHROPIC_API_KEY)' });
var fieldNames = ['name','kana','dob','gender','phoneHome','phoneMobile',
'zip','address','rModel','rMaker','rDate','lModel','lMaker','lDate',
'serial','battery','avgRight','avgLeft','disability','referrer','welfareDate','note'];
var props = {};
for (var i = 0; i < fieldNames.length; i++) props[fieldNames[i]] = { type:'string' };
var schema = { type:'object', properties:props, required:fieldNames, additionalProperties:false };
var prompt =
'画像は日本の補聴器店の手書きカルテ(表・フィッティング記録)です。読み取れる項目だけを正確に抽出してください。\n' +
'・読み取れない/該当なしの項目は必ず空文字 "" にする(推測で埋めない)。\n' +
'・name=氏名、kana=フリガナ、dob=生年月日(可能ならYYYY-MM-DD、和暦は西暦に)、gender=「男」か「女」。\n' +
'・phoneHome=自宅電話、phoneMobile=携帯電話、zip=郵便番号、address=住所。\n' +
'・rModel/lModel=右/左の機種名、rMaker/lMaker=メーカー、rDate/lDate=右/左の測定日または購入日。\n' +
'・serial=製造番号(NO.)、battery=電池規格、avgRight/avgLeft=平均聴力(4分法)の数値dB(右/左)。\n' +
'・disability=身障手帳の有無や級、referrer=紹介者、welfareDate=福祉利用日、note=主訴/備考など自由記述。\n' +
'オージオグラムのグラフ自体は数値化しなくてよい。出力はスキーマに従う。';
var payload = {
model: MODEL,
max_tokens: 1500,
output_config: { format: { type:'json_schema', schema: schema } },
messages: [{ role:'user', content: [
{ type:'image', source:{ type:'base64', media_type: data.mediaType || 'image/jpeg', data: data.image } },
{ type:'text', text: prompt }
]}]
};
var res = UrlFetchApp.fetch('https://api.anthropic.com/v1/messages', {
method:'post', contentType:'application/json',
headers:{ 'x-api-key': key, 'anthropic-version':'2023-06-01' },
payload: JSON.stringify(payload), muteHttpExceptions:true
});
var codeNum = res.getResponseCode();
var body = res.getContentText();
if (codeNum !== 200) return json_({ ok:false, error:'API '+codeNum, detail: body.slice(0,400) });
var parsed;
try { parsed = JSON.parse(body); } catch (err) { return json_({ ok:false, error:'resp parse' }); }
if (parsed.stop_reason === 'refusal') return json_({ ok:false, error:'refusal' });
var txt = '';
for (var j = 0; j < parsed.content.length; j++) {
if (parsed.content[j].type === 'text') { txt = parsed.content[j].text; break; }
}
var fields;
try { fields = JSON.parse(txt); } catch (err) { return json_({ ok:false, error:'fields parse', raw: txt.slice(0,300) }); }
return json_({ ok:true, fields: fields });
}