fix: nvr_snapshot이 위치 이름 대신 외운 채널 번호를 써서 엉뚱한 카메라를 가져오던 문제
실사고(papa): 대화 초반 "2번이 옥상 카메라"를 모델이 외웠는데, 이후 채널 라벨을 붙이면서 채널 2 = 대문이 됨. "옥상 확인"에 모델이 camera:"2" 전달 → 대문 프레임을 받고도 "옥상 카메라 확인했습니다"라고 답함. - 도구 설명/스키마: 채널 번호 말고 위치 이름(선룸/옥상/대문/뒷산/거실/마당)을 쓰라고 명시 - 번호로 호출 시 결과에 "[확인] 채널 번호 '2' = 대문 카메라입니다" 명시 — 모델이 스테일 매핑을 자각하고 이름으로 재호출하게 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
This commit is contained in:
@@ -1092,11 +1092,11 @@ export function createBuildTools(isOrchestrationSkillEnabled: () => boolean) {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'nvr_snapshot',
|
||||
description: '집 CCTV(감시 카메라)의 지금 화면을 한 장 캡처해 실제로 보고 확인한다. "마당에 누구 있어?", "택배 왔어?", "현관 확인해줘", "고양이 있나 봐줘" 같은 요청에 사용. 캡처한 이미지를 직접 보고 화면에 실제로 보이는 것만 설명하고, 어두우면 어둡다고 하고 추측은 하지 말 것.',
|
||||
description: '집 CCTV의 지금 화면을 한 장 캡처해 실제로 보고 확인한다. "마당에 누구 있어?", "택배 왔어?", "옥상 확인해줘" 같은 요청에 사용. 설치된 카메라(위치 이름): 선룸, 옥상, 대문, 뒷산, 거실, 마당. camera에는 이 위치 이름을 넣을 것 — 이전 대화에서 외운 채널 번호는 매핑이 바뀌었을 수 있으니 쓰지 말 것. 캡처한 이미지를 직접 보고 화면에 실제로 보이는 것만 설명, 어두우면 어둡다고 하고 추측 금지.',
|
||||
parameters: {
|
||||
type: 'object', required: ['camera'],
|
||||
properties: {
|
||||
camera: { type: 'string', description: '카메라 이름("현관","마당","주차장" 등) 또는 채널 번호("0"~"3","채널 2"). 이름을 모르면 아무 값이나 넣어 호출하면 사용 가능한 카메라 목록이 반환됨.' },
|
||||
camera: { type: 'string', description: '카메라 위치 이름: 선룸 / 옥상 / 대문 / 뒷산 / 거실 / 마당' },
|
||||
stream: { type: 'string', enum: ['main', 'sub'], description: '화질(기본 main). 신호 약한 무선 카메라가 안 잡히면 sub.' },
|
||||
},
|
||||
},
|
||||
|
||||
+18
-17
@@ -31,33 +31,29 @@ async function listCameras(): Promise<CamInfo[]> {
|
||||
return cams;
|
||||
}
|
||||
|
||||
type Resolved = { id: number; label: string; byNumber: boolean } | { error: string };
|
||||
|
||||
// "현관", "채널 2", "2", "머큐시스" 등 자유 입력을 채널 ID로 해석.
|
||||
async function resolveCameraId(input: unknown): Promise<{ id: number; label: string } | { error: string }> {
|
||||
async function resolveCameraId(input: unknown): Promise<Resolved> {
|
||||
const cams = await listCameras();
|
||||
const list = () => cams.map((c) => `${c.label}(${c.id})`).join(', ');
|
||||
if (input == null || String(input).trim() === '') {
|
||||
return { error: `어느 카메라인지 지정하세요. 사용 가능: ${list()}` };
|
||||
}
|
||||
const raw = String(input).trim();
|
||||
// 순수 숫자면 채널 ID로 직접
|
||||
if (/^\d+$/.test(raw)) {
|
||||
const id = Number(raw);
|
||||
// 순수 숫자 또는 "채널 N" — 채널 ID로 직접
|
||||
const numMatch = raw.match(/^(?:(?:채널|channel|ch|cam)\s*)?(\d+)$/i) || raw.match(/(?:채널|channel|ch|cam)\s*(\d+)/i);
|
||||
if (numMatch) {
|
||||
const id = Number(numMatch[1]);
|
||||
const hit = cams.find((c) => c.id === id);
|
||||
return hit ? { id: hit.id, label: hit.label } : { error: `채널 ${id}은 없습니다. 사용 가능: ${list()}` };
|
||||
}
|
||||
// "채널 N" / "channel N"
|
||||
const chMatch = raw.match(/(?:채널|channel|ch)\s*(\d+)/i);
|
||||
if (chMatch) {
|
||||
const id = Number(chMatch[1]);
|
||||
const hit = cams.find((c) => c.id === id);
|
||||
return hit ? { id: hit.id, label: hit.label } : { error: `채널 ${id}은 없습니다. 사용 가능: ${list()}` };
|
||||
return hit ? { id: hit.id, label: hit.label, byNumber: true } : { error: `채널 ${id}은 없습니다. 사용 가능: ${list()}` };
|
||||
}
|
||||
// 라벨 부분일치(대소문자·공백 무시)
|
||||
const norm = (s: string) => s.replace(/\s+/g, '').toLowerCase();
|
||||
const n = norm(raw);
|
||||
const hit = cams.find((c) => norm(c.label) === n) || cams.find((c) => norm(c.label).includes(n) || n.includes(norm(c.label)));
|
||||
if (hit) return { id: hit.id, label: hit.label };
|
||||
return { error: `"${raw}"에 해당하는 카메라를 찾지 못했습니다. 카메라에 이름이 아직 안 붙어 있을 수 있습니다. 사용 가능: ${list()}` };
|
||||
if (hit) return { id: hit.id, label: hit.label, byNumber: false };
|
||||
return { error: `"${raw}"에 해당하는 카메라를 찾지 못했습니다. 카메라 이름은 ${cams.map((c) => c.label).join(', ')} 입니다 — 이 중에서 골라 다시 부르세요.` };
|
||||
}
|
||||
|
||||
function grabFrame(rtspUrl: string, outPath: string, timeoutMs: number): Promise<{ ok: true } | { ok: false; detail: string }> {
|
||||
@@ -94,13 +90,13 @@ function grabFrame(rtspUrl: string, outPath: string, timeoutMs: number): Promise
|
||||
|
||||
export const nvrSnapshotTool = {
|
||||
name: 'nvr_snapshot',
|
||||
description: '집 CCTV(감시 카메라)의 지금 화면을 한 장 캡처해서 확인한다. "마당에 누구 있어?", "택배 왔나 봐줘", "현관 확인해줘" 같은 요청에 사용. camera에 카메라 이름("현관", "마당") 또는 채널 번호("0"~"3", "채널 2")를 넣는다. 이름을 모르면 아무 값이나 넣어 호출하면 사용 가능한 카메라 목록이 반환된다. 캡처된 이미지를 실제로 보고 답하되, 화면에 실제로 보이는 것만 설명하고 추측하지 말 것.',
|
||||
description: '집 CCTV(감시 카메라)의 지금 화면을 한 장 캡처해서 확인한다. "마당에 누구 있어?", "택배 왔나 봐줘", "옥상 확인해줘" 같은 요청에 사용. camera에는 **위치 이름**을 넣는다 — 설치된 카메라: 선룸, 옥상, 대문, 뒷산, 거실, 마당. 채널 번호도 받지만 이전 대화에서 외운 번호는 신뢰하지 말고 위치 이름을 쓸 것(번호↔위치 매핑이 바뀌었을 수 있음). 캡처된 이미지를 실제로 보고, 화면에 실제로 보이는 것만 설명하고 추측하지 말 것.',
|
||||
schema: { camera: 'string' },
|
||||
jsonSchema: {
|
||||
type: 'object',
|
||||
required: ['camera'],
|
||||
properties: {
|
||||
camera: { type: 'string', description: '카메라 이름("현관", "마당", "주차장" 등) 또는 채널 번호("0","1","2","3","채널 2"). 잘 모르면 아무 값이나 넣으면 목록이 나옴.' },
|
||||
camera: { type: 'string', description: '카메라 위치 이름: 선룸 / 옥상 / 대문 / 뒷산 / 거실 / 마당. (이전 대화에서 외운 채널 번호 말고 위치 이름을 쓸 것)' },
|
||||
stream: { type: 'string', enum: ['main', 'sub'], description: '화질. 기본 main(고화질). 신호 약한 무선 카메라가 안 잡히면 sub.' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
@@ -131,9 +127,14 @@ export const nvrSnapshotTool = {
|
||||
}
|
||||
|
||||
const now = new Date().toLocaleString('ko-KR', { timeZone: 'Asia/Seoul', hour12: false });
|
||||
// 번호로 불렀으면 그 채널이 실제로 어느 위치인지 못박아 알려준다 — 모델이 이전 대화에서
|
||||
// 외운 "N번=옥상" 같은 틀린 매핑으로 엉뚱한 카메라를 가져오는 사고를 막는다(2026-09-07).
|
||||
const numberNote = resolved.byNumber
|
||||
? `\n\n[확인] 채널 번호 '${args.camera}' = **${resolved.label}** 카메라입니다. 사용자가 다른 위치를 원했다면 위치 이름(선룸/옥상/대문/뒷산/거실/마당)으로 다시 불러야 합니다.`
|
||||
: '';
|
||||
return {
|
||||
success: true,
|
||||
stdout: `${resolved.label} 카메라 — ${now} 현재 화면:\n\n${buildImageMarkdown(outPath, workspacePath)}\n\n[중요] 위 이미지를 실제로 보고 답하세요. 이전 대화 내용이나 이 카메라가 어디를 비출 것이라는 짐작이 아니라, **지금 이 프레임에 보이는 것**만 구체적으로: 눈에 띄는 사물, 사람/동물/차량 유무, 문·조명·날씨 상태. 이미지가 어둡거나 흐리거나 나뭇가지 등에 가려 잘 안 보이면 그렇다고 말하세요. 프레임에 없는 것은 언급하지 마세요.`,
|
||||
stdout: `${resolved.label} 카메라 — ${now} 현재 화면:${numberNote}\n\n${buildImageMarkdown(outPath, workspacePath)}\n\n[중요] 위 이미지를 실제로 보고 답하세요. 이전 대화 내용이나 이 카메라가 어디를 비출 것이라는 짐작이 아니라, **지금 이 프레임에 보이는 것**만 구체적으로: 눈에 띄는 사물, 사람/동물/차량 유무, 문·조명·날씨 상태. 이미지가 어둡거나 흐리거나 나뭇가지 등에 가려 잘 안 보이면 그렇다고 말하세요. 프레임에 없는 것은 언급하지 마세요.`,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user