Add accountant/investor skills, Excel editor, email attachment fixes

Skills:
- Add accountant (CPA) and investor (CFA) skills with site: hints
- Add site: hints to lawyer, psychiatrist, musician skills
- Allow multiple skills active simultaneously (remove exclusive mode)

Tools:
- Add excel_read / excel_write tools (openpyxl-based)
- Fix python_eval packages: retry with --break-system-packages on PEP 668 failure
- Fix email_read: spaces→underscores in filenames, return /api/files/ links,
  remove absolute savedPaths from data, send SSE 'files' event for attachments

Server:
- FILE_OP v2: fall through to primary when secondary model unavailable
- Add PUT /api/files/{*filePath} endpoint for xlsx editor save

UI:
- xlsx viewer: Luckysheet modal (lazy CDN load, ~3MB on first use)
  with edit mode, 💾 save button, Nanum Gothic font
- Email attachment preview: SSE 'files' event appends file links to final reply
- ✏️ 편집 button (blue) for xlsx cards

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kim
2026-05-19 16:38:42 +09:00
co-authored by Claude Sonnet 4.6
parent bac5604d5e
commit 2be3edc314
13 changed files with 920 additions and 89 deletions
+7 -5
View File
@@ -364,7 +364,7 @@ export const emailReadTool = {
fs.mkdirSync(saveDir, { recursive: true });
for (const att of parsed.attachments) {
const filename = att.filename || `attachment-${savedPaths.length + 1}`;
const safeName = filename.replace(/[/\\:*?"<>|]/g, '_');
const safeName = filename.replace(/[/\\:*?"<>|]/g, '_').replace(/\s+/g, '_');
const dest = path.join(saveDir, safeName);
fs.writeFileSync(dest, att.content);
savedPaths.push(dest);
@@ -410,13 +410,14 @@ export const emailReadTool = {
`날짜: ${r.date}`,
r.attachments.length ? `첨부파일: ${r.attachments.join(', ')}` : '',
r.savedRelPaths?.length
? `✅ 첨부파일 저장 완료 — pdf_read/read 등에서 아래 경로를 그대로 사용하세요:\n${r.savedRelPaths.map((p: string) => ` "${p}"`).join('\n')}`
? `✅ 첨부파일 저장 완료 — pdf_read 등에서 아래 상대경로를 그대로 사용하세요:\n${r.savedRelPaths.map((p: string) => ` - [${path.basename(p)}](/api/files/${p.replace(/\\/g, '/')})`).join('\n')}`
: '',
'',
'─── 본문 ───',
r.body,
].filter((l: string) => l !== '');
return { success: true, stdout: lines.join('\n'), data: { ...r, account: found.account } };
const { savedPaths: _sp, ...safeData } = { ...r, account: found.account };
return { success: true, stdout: lines.join('\n'), data: safeData };
}
// No account specified — try all accounts
@@ -438,13 +439,14 @@ export const emailReadTool = {
`날짜: ${r.date}`,
r.attachments.length ? `첨부파일: ${r.attachments.join(', ')}` : '',
r.savedRelPaths?.length
? `✅ 첨부파일 저장 완료 — pdf_read/read 등에서 아래 경로를 그대로 사용하세요:\n${r.savedRelPaths.map((p: string) => ` "${p}"`).join('\n')}`
? `✅ 첨부파일 저장 완료 — pdf_read 등에서 아래 상대경로를 그대로 사용하세요:\n${r.savedRelPaths.map((p: string) => ` - [${path.basename(p)}](/api/files/${p.replace(/\\/g, '/')})`).join('\n')}`
: '',
'',
'─── 본문 ───',
r.body,
].filter((l: string) => l !== '');
return { success: true, stdout: lines.join('\n'), data: { ...r, account: found.account } };
const { savedPaths: _sp, ...safeData } = { ...r, account: found.account };
return { success: true, stdout: lines.join('\n'), data: safeData };
}
errors.push(acct.id);
}