feat: robot_project 도구에 add_link/delete_link 액션 추가

부품 카드에 추가한 참고링크(구매처/대체품/업그레이드/레포 등, 076a53b)를
이제 채팅으로도 붙이고 뗄 수 있다 — "이거 어디서 팔아?" 하고 물어서
web_search로 찾은 뒤 이 액션으로 바로 그 부품에 링크를 붙이는 흐름을
위한 것. delete_link는 라벨 일부일치로 찾는다(다른 액션들과 동일 패턴).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
kim
2026-09-20 18:12:31 +09:00
co-authored by Claude Sonnet 5
parent 076a53b8de
commit 4911ab005a
2 changed files with 42 additions and 5 deletions
+4 -2
View File
@@ -1130,8 +1130,8 @@ export function createBuildTools(isOrchestrationSkillEnabled: () => boolean) {
parameters: {
type: 'object', required: ['action'],
properties: {
action: { type: 'string', enum: ['get', 'add_part', 'update_part', 'delete_part', 'add_task', 'set_task_done', 'add_phase', 'add_note'], description: 'get: 전체 조회(예산 요약 포함). add_part/update_part/delete_part: 부품 추가/수정/삭제. add_task: 특정 단계에 작업 추가. set_task_done: 작업 완료/미완료 표시. add_phase: 새 단계 추가. add_note: 메모에 한 줄 추가.' },
part_name: { type: 'string', description: '(update_part/delete_part 필수) 대상 부품 이름(일부만 일치해도 됨, 예: "메카넘휠")' },
action: { type: 'string', enum: ['get', 'add_part', 'update_part', 'delete_part', 'add_task', 'set_task_done', 'add_phase', 'add_note', 'add_link', 'delete_link'], description: 'get: 전체 조회(예산 요약 포함, 부품별 링크 포함). add_part/update_part/delete_part: 부품 추가/수정/삭제. add_task: 특정 단계에 작업 추가. set_task_done: 작업 완료/미완료 표시. add_phase: 새 단계 추가. add_note: 메모에 한 줄 추가. add_link/delete_link: 부품에 참고링크(구매처/대체품/업그레이드/관련 레포 등) 추가/삭제 — 사용자가 "이거 어디서 팔아?" 류로 물어서 web_search로 찾았으면 이 액션으로 그 부품에 바로 붙여줄 것.' },
part_name: { type: 'string', description: '(update_part/delete_part/add_link/delete_link 필수) 대상 부품 이름(일부만 일치해도 됨, 예: "메카넘휠")' },
name: { type: 'string', description: '(add_part 필수, update_part 선택) 부품 이름' },
qty: { type: 'number', description: '(add_part/update_part 선택) 수량. 기본 1' },
unit_price: { type: 'number', description: '(add_part/update_part 선택) 단가(원)' },
@@ -1142,6 +1142,8 @@ export function createBuildTools(isOrchestrationSkillEnabled: () => boolean) {
done: { type: 'boolean', description: '(set_task_done 필수) true=완료 표시, false=미완료로 되돌림' },
new_phase_name: { type: 'string', description: '(add_phase 필수) 새 단계 이름' },
note_text: { type: 'string', description: '(add_note 필수) 메모에 추가할 한 줄' },
link_label: { type: 'string', description: '(add_link 필수) 링크 라벨(예: "쿠팡", "대체품", "GitHub 레포"), (delete_link 필수) 삭제할 링크의 라벨(일부만 일치해도 됨)' },
link_url: { type: 'string', description: '(add_link 필수) 링크 URL' },
},
},
},
+38 -3
View File
@@ -5,6 +5,12 @@ import {
RobotProjectData, RobotPart, RobotPhase,
} from '../gateway/routes/robot-storage.js';
// links가 없는(2026-09-20 이전 저장분) 부품을 만나도 죽지 않게 항상 배열을 보장한다.
function ensureLinks(p: RobotPart): NonNullable<RobotPart['links']> {
if (!p.links) p.links = [];
return p.links;
}
// "로봇 프로젝트"(메카넘휠 이동로봇, robot-app.html 대시보드) 전용 채팅도구.
// 대시보드와 같은 저장소(robot-storage.ts)를 읽고 쓰므로 채팅에서 바꾼 내용이 대시보드에도,
// 대시보드에서 바꾼 내용이 채팅에도 곧바로 반영된다. 물리적 동작이 없는 순수 데이터
@@ -88,10 +94,10 @@ export const robotProjectTool = {
properties: {
action: {
type: 'string',
enum: ['get', 'add_part', 'update_part', 'delete_part', 'add_task', 'set_task_done', 'add_phase', 'add_note'],
description: 'get: 전체 조회(예산 요약 포함). add_part/update_part/delete_part: 부품 추가/수정/삭제. add_task: 특정 단계에 작업 추가. set_task_done: 작업 완료/미완료 표시. add_phase: 새 단계 추가. add_note: 메모에 한 줄 추가.',
enum: ['get', 'add_part', 'update_part', 'delete_part', 'add_task', 'set_task_done', 'add_phase', 'add_note', 'add_link', 'delete_link'],
description: 'get: 전체 조회(예산 요약 포함, 부품별 링크 포함). add_part/update_part/delete_part: 부품 추가/수정/삭제. add_task: 특정 단계에 작업 추가. set_task_done: 작업 완료/미완료 표시. add_phase: 새 단계 추가. add_note: 메모에 한 줄 추가. add_link/delete_link: 부품에 참고링크(구매처/대체품/업그레이드/관련 레포 등) 추가/삭제 — 사용자가 "이거 어디서 팔아?" 류로 물어서 web_search로 찾았으면 이 액션으로 그 부품에 바로 붙여줄 것.',
},
part_name: { type: 'string', description: '(update_part/delete_part 필수) 대상 부품 이름(일부만 일치해도 됨, 예: "메카넘휠")' },
part_name: { type: 'string', description: '(update_part/delete_part/add_link/delete_link 필수) 대상 부품 이름(일부만 일치해도 됨, 예: "메카넘휠")' },
name: { type: 'string', description: '(add_part 필수, update_part 선택) 부품 이름' },
qty: { type: 'number', description: '(add_part/update_part 선택) 수량. 기본 1' },
unit_price: { type: 'number', description: '(add_part/update_part 선택) 단가(원)' },
@@ -102,6 +108,8 @@ export const robotProjectTool = {
done: { type: 'boolean', description: '(set_task_done 필수) true=완료 표시, false=미완료로 되돌림' },
new_phase_name: { type: 'string', description: '(add_phase 필수) 새 단계 이름' },
note_text: { type: 'string', description: '(add_note 필수) 메모에 추가할 한 줄' },
link_label: { type: 'string', description: '(add_link 필수) 링크 라벨(예: "쿠팡", "대체품", "GitHub 레포"), (delete_link 필수) 삭제할 링크의 라벨(일부만 일치해도 됨)' },
link_url: { type: 'string', description: '(add_link 필수) 링크 URL' },
},
additionalProperties: false,
},
@@ -190,6 +198,33 @@ export const robotProjectTool = {
return { success: true, stdout: `메모 추가됨: ${text}` };
}
if (action === 'add_link') {
const found = findPartIndex(data, String(args?.part_name || ''));
if (!found.ok) return { success: false, error: found.error };
const label = String(args?.link_label || '').trim();
const url = String(args?.link_url || '').trim();
if (!label) return { success: false, error: 'link_label이 필요합니다.' };
if (!url) return { success: false, error: 'link_url이 필요합니다.' };
const p = data.parts[found.index];
ensureLinks(p).push({ id: genRobotId('lk'), label, url });
saveRobotProject(workspaceRoot, data);
return { success: true, stdout: `"${p.name}"에 링크 추가됨: ${label} → ${url}` };
}
if (action === 'delete_link') {
const found = findPartIndex(data, String(args?.part_name || ''));
if (!found.ok) return { success: false, error: found.error };
const p = data.parts[found.index];
const links = ensureLinks(p);
const q = String(args?.link_label || '').trim().toLowerCase();
const matches = links.filter(l => l.label.toLowerCase().includes(q));
if (matches.length === 0) return { success: false, error: `"${p.name}"에 "${args?.link_label}"와 일치하는 링크가 없습니다. 현재 링크: ${links.map(l => l.label).join(', ') || '없음'}` };
if (matches.length > 1) return { success: false, error: `여러 링크가 일치합니다: ${matches.map(l => l.label).join(', ')} — 더 구체적으로 지정하세요.` };
p.links = links.filter(l => l.id !== matches[0].id);
saveRobotProject(workspaceRoot, data);
return { success: true, stdout: `"${p.name}"의 링크 삭제됨: ${matches[0].label}` };
}
return { success: false, error: `알 수 없는 action: ${action}` };
},
};