v2.2.15: Astra Office Refactor & Multi-Service Integration
This commit is contained in:
+279
@@ -3303,6 +3303,185 @@ export class AgentExecutor {
|
||||
} catch (err: any) { report.push(`❌ URL Read failed: ${err.message}`); }
|
||||
}
|
||||
|
||||
// Action 9: Create Calendar Event (OAuth) — agent 가 회의록·작업 분석 후 일정 자동 생성.
|
||||
// 형식: <create_calendar_event title="..." start="2026-05-21T14:00" duration="60" location="...">설명</create_calendar_event>
|
||||
// 속성: title (필수), start (필수, ISO 'YYYY-MM-DDTHH:MM' 또는 timezone 포함),
|
||||
// end | duration (분, default 60), location, all_day (true/false)
|
||||
const calRegex = /<create_calendar_event\b([^>]*)>([\s\S]*?)<\/create_calendar_event>/gi;
|
||||
while ((match = calRegex.exec(aiMessage)) !== null) {
|
||||
const attrs = _parseCalEventAttrs(match[1]);
|
||||
const desc = match[2].trim();
|
||||
if (!attrs.title || !attrs.start) {
|
||||
report.push(`❌ Calendar Event: title / start 누락`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const { createCalendarEvent } = await import('./features/calendar');
|
||||
const r = await createCalendarEvent(this.context, {
|
||||
title: attrs.title,
|
||||
start: attrs.start,
|
||||
end: attrs.end,
|
||||
durationMinutes: attrs.duration,
|
||||
location: attrs.location,
|
||||
description: desc || undefined,
|
||||
allDay: attrs.allDay,
|
||||
});
|
||||
if (r.ok) {
|
||||
report.push(`📅 Calendar Event Created: ${r.event.title} (${r.event.startIso})`);
|
||||
// chatHistory 에 결과 주입 — agent 가 다음 답변에서 link 인용 가능.
|
||||
this.chatHistory.push({
|
||||
role: 'system',
|
||||
content: `[Calendar event created] ${r.event.title} · ${r.event.startIso}\nLink: ${r.event.htmlLink}`,
|
||||
internal: true,
|
||||
});
|
||||
} else {
|
||||
report.push(`❌ Calendar Event Failed: ${r.error}`);
|
||||
}
|
||||
} catch (err: any) { report.push(`❌ Calendar Event Error: ${err?.message ?? String(err)}`); }
|
||||
}
|
||||
|
||||
// Action 10/11/12: Google Sheets read / write / append.
|
||||
// 모두 spreadsheet_id (속성) + range (속성) 필수. write/append 는 본문이 TSV.
|
||||
// <read_sheet spreadsheet_id="1abc..." range="Sheet1!A1:D20"/>
|
||||
// <write_sheet spreadsheet_id="1abc..." range="Sheet1!A1">
|
||||
// 이름\t나이\t직책
|
||||
// 민지\t29\t디자이너
|
||||
// </write_sheet>
|
||||
// <append_sheet spreadsheet_id="1abc..." range="Sheet1!A:C">
|
||||
// 2026-05-21\t새 항목\t완료
|
||||
// </append_sheet>
|
||||
const sheetReadRegex = /<read_sheet\b([^>/]*?)\s*\/>/gi;
|
||||
while ((match = sheetReadRegex.exec(aiMessage)) !== null) {
|
||||
const a = _parseSheetAttrs(match[1]);
|
||||
if (!a.spreadsheetId || !a.range) {
|
||||
report.push(`❌ Sheet Read: spreadsheet_id / range 누락`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const { readSheetRange, valuesToMarkdownTable } = await import('./features/sheets');
|
||||
const r = await readSheetRange(this.context, a.spreadsheetId, a.range);
|
||||
if (r.ok) {
|
||||
const md = valuesToMarkdownTable(r.values);
|
||||
report.push(`📊 Sheet Read: ${a.spreadsheetId.slice(0, 8)}…/${r.range} (${r.values.length} rows)`);
|
||||
this.chatHistory.push({
|
||||
role: 'system',
|
||||
content: `[Sheet read ${r.range}]\n${md}`,
|
||||
internal: true,
|
||||
});
|
||||
} else {
|
||||
report.push(`❌ Sheet Read Failed: ${r.error}`);
|
||||
}
|
||||
} catch (err: any) { report.push(`❌ Sheet Read Error: ${err?.message ?? String(err)}`); }
|
||||
}
|
||||
const sheetWriteRegex = /<write_sheet\b([^>]*)>([\s\S]*?)<\/write_sheet>/gi;
|
||||
while ((match = sheetWriteRegex.exec(aiMessage)) !== null) {
|
||||
const a = _parseSheetAttrs(match[1]);
|
||||
const body = match[2];
|
||||
if (!a.spreadsheetId || !a.range) {
|
||||
report.push(`❌ Sheet Write: spreadsheet_id / range 누락`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const { writeSheetRange, parseTsvBody } = await import('./features/sheets');
|
||||
const values = parseTsvBody(body);
|
||||
if (values.length === 0) {
|
||||
report.push(`❌ Sheet Write: 본문 비어있음`);
|
||||
continue;
|
||||
}
|
||||
const r = await writeSheetRange(this.context, a.spreadsheetId, a.range, values);
|
||||
if (r.ok) {
|
||||
report.push(`📊 Sheet Write: ${r.updatedRange} (${r.updatedCells} cells)`);
|
||||
} else {
|
||||
report.push(`❌ Sheet Write Failed: ${r.error}`);
|
||||
}
|
||||
} catch (err: any) { report.push(`❌ Sheet Write Error: ${err?.message ?? String(err)}`); }
|
||||
}
|
||||
const sheetAppendRegex = /<append_sheet\b([^>]*)>([\s\S]*?)<\/append_sheet>/gi;
|
||||
while ((match = sheetAppendRegex.exec(aiMessage)) !== null) {
|
||||
const a = _parseSheetAttrs(match[1]);
|
||||
const body = match[2];
|
||||
if (!a.spreadsheetId || !a.range) {
|
||||
report.push(`❌ Sheet Append: spreadsheet_id / range 누락`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const { appendSheetRows, parseTsvBody } = await import('./features/sheets');
|
||||
const values = parseTsvBody(body);
|
||||
if (values.length === 0) {
|
||||
report.push(`❌ Sheet Append: 본문 비어있음`);
|
||||
continue;
|
||||
}
|
||||
const r = await appendSheetRows(this.context, a.spreadsheetId, a.range, values);
|
||||
if (r.ok) {
|
||||
report.push(`📊 Sheet Append: ${r.appendedRange} (${r.updatedCells} cells)`);
|
||||
} else {
|
||||
report.push(`❌ Sheet Append Failed: ${r.error}`);
|
||||
}
|
||||
} catch (err: any) { report.push(`❌ Sheet Append Error: ${err?.message ?? String(err)}`); }
|
||||
}
|
||||
|
||||
// Action 13/14/15: Task tracker — _shared/tasks.md 에 누적.
|
||||
// 회의록·계획·작업 진척 추적의 단일 출처. status: open/in_progress/blocked/done.
|
||||
// <add_task title="..." owner="@me" due="2026-05-24T18:00" notes="..."/>
|
||||
// <update_task id="t_001" status="in_progress" notes="진행중"/>
|
||||
// <complete_task id="t_001"/>
|
||||
const addTaskRegex = /<add_task\b([^>/]*?)\s*\/>/gi;
|
||||
while ((match = addTaskRegex.exec(aiMessage)) !== null) {
|
||||
const a = _parseTaskAttrs(match[1]);
|
||||
if (!a.title) { report.push(`❌ Add Task: title 누락`); continue; }
|
||||
try {
|
||||
const { readTaskStore, writeTaskStore, addTask } = await import('./features/tasks');
|
||||
const store = readTaskStore(this.context);
|
||||
const created = addTask(store, {
|
||||
title: a.title,
|
||||
owner: a.owner,
|
||||
due: a.due,
|
||||
notes: a.notes,
|
||||
status: a.status,
|
||||
});
|
||||
writeTaskStore(this.context, store);
|
||||
report.push(`📋 Task Added: ${created.id} · ${created.title}${created.due ? ' (due ' + created.due + ')' : ''}`);
|
||||
} catch (err: any) { report.push(`❌ Add Task Error: ${err?.message ?? String(err)}`); }
|
||||
}
|
||||
const updTaskRegex = /<update_task\b([^>/]*?)\s*\/>/gi;
|
||||
while ((match = updTaskRegex.exec(aiMessage)) !== null) {
|
||||
const a = _parseTaskAttrs(match[1]);
|
||||
if (!a.id) { report.push(`❌ Update Task: id 누락`); continue; }
|
||||
try {
|
||||
const { readTaskStore, writeTaskStore, updateTask } = await import('./features/tasks');
|
||||
const store = readTaskStore(this.context);
|
||||
const patch: any = {};
|
||||
if (a.title) patch.title = a.title;
|
||||
if (a.owner) patch.owner = a.owner;
|
||||
if (a.due) patch.due = a.due;
|
||||
if (a.notes) patch.notes = a.notes;
|
||||
if (a.status) patch.status = a.status;
|
||||
const updated = updateTask(store, a.id, patch);
|
||||
if (!updated) {
|
||||
report.push(`❌ Update Task: ${a.id} 를 active 목록에서 못 찾음`);
|
||||
} else {
|
||||
writeTaskStore(this.context, store);
|
||||
report.push(`📋 Task Updated: ${updated.id} → ${updated.status}${updated.due ? ' (due ' + updated.due + ')' : ''}`);
|
||||
}
|
||||
} catch (err: any) { report.push(`❌ Update Task Error: ${err?.message ?? String(err)}`); }
|
||||
}
|
||||
const compTaskRegex = /<complete_task\b([^>/]*?)\s*\/>/gi;
|
||||
while ((match = compTaskRegex.exec(aiMessage)) !== null) {
|
||||
const a = _parseTaskAttrs(match[1]);
|
||||
if (!a.id) { report.push(`❌ Complete Task: id 누락`); continue; }
|
||||
try {
|
||||
const { readTaskStore, writeTaskStore, completeTask } = await import('./features/tasks');
|
||||
const store = readTaskStore(this.context);
|
||||
const closed = completeTask(store, a.id);
|
||||
if (!closed) {
|
||||
report.push(`❌ Complete Task: ${a.id} 못 찾음 (이미 done 이거나 존재 X)`);
|
||||
} else {
|
||||
writeTaskStore(this.context, store);
|
||||
report.push(`✅ Task Done: ${closed.id} · ${closed.title}`);
|
||||
}
|
||||
} catch (err: any) { report.push(`❌ Complete Task Error: ${err?.message ?? String(err)}`); }
|
||||
}
|
||||
|
||||
if (firstCreatedFile) {
|
||||
// Always open file results in the editor group (column 2) — the ConnectAI
|
||||
// sidebar lives in column 3 and we don't want freshly-written files to
|
||||
@@ -3369,3 +3548,103 @@ export class AgentExecutor {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <add_task> / <update_task> / <complete_task> 의 attribute 파서.
|
||||
* 모든 필드 optional 로 받고 caller 가 필수 체크. status 는 정규화 (in_progress, 등).
|
||||
*/
|
||||
export function _parseTaskAttrs(raw: string): {
|
||||
id?: string;
|
||||
title?: string;
|
||||
owner?: string;
|
||||
due?: string;
|
||||
notes?: string;
|
||||
status?: import('./features/tasks').TaskStatus;
|
||||
} {
|
||||
const out: any = {};
|
||||
const re = /([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/g;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = re.exec(raw)) !== null) {
|
||||
const key = m[1].toLowerCase();
|
||||
const val = (m[2] ?? m[3] ?? m[4] ?? '').trim();
|
||||
if (!val) continue;
|
||||
switch (key) {
|
||||
case 'id': out.id = val; break;
|
||||
case 'title': out.title = val; break;
|
||||
case 'owner': out.owner = val; break;
|
||||
case 'due': out.due = val; break;
|
||||
case 'notes': out.notes = val; break;
|
||||
case 'status': {
|
||||
const v = val.toLowerCase().replace(/\s+/g, '_');
|
||||
if (v === 'in_progress' || v === 'inprogress' || v === 'progress') out.status = 'in_progress';
|
||||
else if (v === 'blocked' || v === 'block') out.status = 'blocked';
|
||||
else if (v === 'done' || v === 'completed' || v === 'closed') out.status = 'done';
|
||||
else out.status = 'open';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* <read_sheet> / <write_sheet> / <append_sheet> 의 attribute 문자열을 객체로 파싱.
|
||||
* spreadsheet_id / spreadsheetId / sheetId 모두 받는 — LLM 의 변형 emission 흡수.
|
||||
*/
|
||||
export function _parseSheetAttrs(raw: string): { spreadsheetId?: string; range?: string } {
|
||||
const out: { spreadsheetId?: string; range?: string } = {};
|
||||
const re = /([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/g;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = re.exec(raw)) !== null) {
|
||||
const key = m[1].toLowerCase();
|
||||
const val = (m[2] ?? m[3] ?? m[4] ?? '').trim();
|
||||
if (!val) continue;
|
||||
if (key === 'spreadsheet_id' || key === 'spreadsheetid' || key === 'sheet_id' || key === 'sheetid') {
|
||||
out.spreadsheetId = val;
|
||||
} else if (key === 'range') {
|
||||
out.range = val;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* <create_calendar_event ...> 의 attribute 문자열을 객체로 파싱.
|
||||
* 큰따옴표 / 작은따옴표 / 따옴표 없이 (공백·`>` 으로 종료) 모두 허용 — LLM 이 어떤
|
||||
* 스타일로 emit 해도 통과시키기 위함. 단위테스트 가능하도록 export.
|
||||
*/
|
||||
export function _parseCalEventAttrs(raw: string): {
|
||||
title?: string;
|
||||
start?: string;
|
||||
end?: string;
|
||||
duration?: number;
|
||||
location?: string;
|
||||
allDay?: boolean;
|
||||
} {
|
||||
const out: any = {};
|
||||
// attr_name = "value" | 'value' | bare. `-` 포함 키 (all-day) 지원.
|
||||
const re = /([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/g;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = re.exec(raw)) !== null) {
|
||||
const key = m[1].toLowerCase();
|
||||
const val = (m[2] ?? m[3] ?? m[4] ?? '').trim();
|
||||
if (!val) continue;
|
||||
switch (key) {
|
||||
case 'title': out.title = val; break;
|
||||
case 'start': out.start = val; break;
|
||||
case 'end': out.end = val; break;
|
||||
case 'duration': {
|
||||
const n = parseInt(val, 10);
|
||||
if (!Number.isNaN(n) && n > 0) out.duration = n;
|
||||
break;
|
||||
}
|
||||
case 'location': out.location = val; break;
|
||||
case 'all_day':
|
||||
case 'allday':
|
||||
case 'all-day':
|
||||
out.allDay = val === 'true' || val === '1' || val === 'yes';
|
||||
break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user