claude-code-hwp-mcp 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +409 -0
- package/dist/hwp-bridge.d.ts +67 -0
- package/dist/hwp-bridge.js +320 -0
- package/dist/hwpx-engine.d.ts +39 -0
- package/dist/hwpx-engine.js +187 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +54 -0
- package/dist/prompts/hwp-prompts.d.ts +2 -0
- package/dist/prompts/hwp-prompts.js +368 -0
- package/dist/resources/document-resources.d.ts +3 -0
- package/dist/resources/document-resources.js +109 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.js +29 -0
- package/dist/tools/analysis-tools.d.ts +4 -0
- package/dist/tools/analysis-tools.js +414 -0
- package/dist/tools/composite-tools.d.ts +3 -0
- package/dist/tools/composite-tools.js +664 -0
- package/dist/tools/document-tools.d.ts +3 -0
- package/dist/tools/document-tools.js +264 -0
- package/dist/tools/editing-tools.d.ts +4 -0
- package/dist/tools/editing-tools.js +916 -0
- package/package.json +31 -0
- package/python/__pycache__/hwp_analyzer.cpython-313.pyc +0 -0
- package/python/__pycache__/hwp_editor.cpython-313.pyc +0 -0
- package/python/__pycache__/hwp_service.cpython-313.pyc +0 -0
- package/python/__pycache__/privacy_scanner.cpython-313.pyc +0 -0
- package/python/__pycache__/ref_reader.cpython-313.pyc +0 -0
- package/python/__pycache__/test_integration.cpython-313.pyc +0 -0
- package/python/hwp_analyzer.py +544 -0
- package/python/hwp_editor.py +933 -0
- package/python/hwp_service.py +1291 -0
- package/python/privacy_scanner.py +115 -0
- package/python/ref_reader.py +115 -0
- package/python/requirements.txt +2 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analysis tools: analyze, get text, get tables, get fields
|
|
3
|
+
*/
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
const HWP_EXTENSIONS = new Set(['.hwp', '.hwpx']);
|
|
8
|
+
const ANALYSIS_TIMEOUT = 60000;
|
|
9
|
+
async function ensureAnalysis(bridge, filePath) {
|
|
10
|
+
await bridge.ensureRunning();
|
|
11
|
+
if (filePath) {
|
|
12
|
+
const resolved = path.resolve(filePath);
|
|
13
|
+
if (!fs.existsSync(resolved)) {
|
|
14
|
+
throw new Error(`파일을 찾을 수 없습니다: ${resolved}`);
|
|
15
|
+
}
|
|
16
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
17
|
+
if (!HWP_EXTENSIONS.has(ext)) {
|
|
18
|
+
throw new Error('HWP 또는 HWPX 파일만 지원합니다.');
|
|
19
|
+
}
|
|
20
|
+
const response = await bridge.send('analyze_document', { file_path: resolved }, ANALYSIS_TIMEOUT);
|
|
21
|
+
if (!response.success)
|
|
22
|
+
throw new Error(response.error ?? '분석 실패');
|
|
23
|
+
bridge.setCachedAnalysis(response.data);
|
|
24
|
+
bridge.setCurrentDocument(resolved);
|
|
25
|
+
return response.data;
|
|
26
|
+
}
|
|
27
|
+
const cached = bridge.getCachedAnalysis();
|
|
28
|
+
const current = bridge.getCurrentDocument();
|
|
29
|
+
// P1 #8: 캐시가 현재 열린 문서와 일치하는 경우에만 반환
|
|
30
|
+
if (cached && current && cached.file_path === current)
|
|
31
|
+
return cached;
|
|
32
|
+
if (!current) {
|
|
33
|
+
throw new Error('열린 문서가 없습니다. hwp_open_document로 문서를 열거나 file_path를 지정하세요. Python 프로세스 재시작 시 열린 문서 상태가 초기화됩니다.');
|
|
34
|
+
}
|
|
35
|
+
const response = await bridge.send('analyze_document', { file_path: current }, ANALYSIS_TIMEOUT);
|
|
36
|
+
if (!response.success)
|
|
37
|
+
throw new Error(response.error ?? '분석 실패');
|
|
38
|
+
bridge.setCachedAnalysis(response.data);
|
|
39
|
+
return response.data;
|
|
40
|
+
}
|
|
41
|
+
export function registerAnalysisTools(server, bridge, toolset = 'standard') {
|
|
42
|
+
server.tool('hwp_analyze_document', 'HWP/HWPX 문서의 전체 구조를 분석합니다. 페이지 수, 표(데이터 포함), 필드(양식), 본문 텍스트를 반환합니다. 문서를 처음 다룰 때 반드시 이 도구를 먼저 호출하세요.', {
|
|
43
|
+
file_path: z.string().describe('HWP/HWPX 파일 경로'),
|
|
44
|
+
}, async ({ file_path }) => {
|
|
45
|
+
try {
|
|
46
|
+
const result = await ensureAnalysis(bridge, file_path);
|
|
47
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
server.tool('hwp_get_document_text', '현재 열린 문서 또는 지정 파일의 본문 텍스트를 추출합니다. 문서 내용을 읽거나 검색할 때 사용하세요.', {
|
|
54
|
+
file_path: z.string().optional().describe('HWP/HWPX 파일 경로 (생략 시 현재 문서)'),
|
|
55
|
+
max_chars: z.number().optional().describe('최대 문자 수 (기본: 15000)'),
|
|
56
|
+
}, async ({ file_path, max_chars }) => {
|
|
57
|
+
try {
|
|
58
|
+
const analysis = await ensureAnalysis(bridge, file_path);
|
|
59
|
+
const limit = max_chars ?? 15000;
|
|
60
|
+
const text = analysis.full_text || '';
|
|
61
|
+
const truncated = text.length > limit;
|
|
62
|
+
return {
|
|
63
|
+
content: [{
|
|
64
|
+
type: 'text',
|
|
65
|
+
text: JSON.stringify({
|
|
66
|
+
text: truncated ? text.slice(0, limit) : text,
|
|
67
|
+
char_count: text.length,
|
|
68
|
+
truncated,
|
|
69
|
+
}),
|
|
70
|
+
}],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
server.tool('hwp_get_tables', '문서의 표 데이터를 조회합니다. 특정 표 인덱스를 지정하거나 전체 표를 반환합니다. 표 셀을 채우기 전에 구조를 확인할 때 사용하세요.', {
|
|
78
|
+
file_path: z.string().optional().describe('HWP/HWPX 파일 경로 (생략 시 현재 문서)'),
|
|
79
|
+
table_index: z.number().optional().describe('특정 표 인덱스 (생략 시 전체)'),
|
|
80
|
+
}, async ({ file_path, table_index }) => {
|
|
81
|
+
try {
|
|
82
|
+
const analysis = await ensureAnalysis(bridge, file_path);
|
|
83
|
+
const tables = analysis.tables || [];
|
|
84
|
+
if (table_index !== undefined) {
|
|
85
|
+
if (table_index < 0 || table_index >= tables.length) {
|
|
86
|
+
return {
|
|
87
|
+
content: [{ type: 'text', text: JSON.stringify({ error: `표 인덱스 ${table_index}이 범위를 벗어났습니다. (총 ${tables.length}개)` }) }],
|
|
88
|
+
isError: true,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
content: [{ type: 'text', text: JSON.stringify({ table: tables[table_index], total_count: tables.length }) }],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
content: [{ type: 'text', text: JSON.stringify({ tables, total_count: tables.length }) }],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
server.tool('hwp_map_table_cells', '표의 셀을 Tab 순서로 순회하여 각 셀의 Tab 인덱스와 내용을 매핑합니다. 병합 셀이 있는 표에서 hwp_fill_table_cells의 tab 파라미터에 사용할 인덱스를 확인할 때 사용하세요.', {
|
|
104
|
+
table_index: z.number().int().min(0).describe('표 인덱스 (0부터 시작)'),
|
|
105
|
+
}, async ({ table_index }) => {
|
|
106
|
+
if (!bridge.getCurrentDocument()) {
|
|
107
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
108
|
+
error: '열린 문서가 없습니다. hwp_open_document로 문서를 열어주세요.',
|
|
109
|
+
}) }], isError: true };
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
await bridge.ensureRunning();
|
|
113
|
+
const response = await bridge.send('map_table_cells', { table_index }, ANALYSIS_TIMEOUT);
|
|
114
|
+
if (!response.success) {
|
|
115
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
|
|
116
|
+
}
|
|
117
|
+
return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
// --- minimal에 포함: get_document_info, text_search ---
|
|
124
|
+
server.tool('hwp_get_document_info', '현재 열린 문서의 경량 메타데이터(페이지 수, 파일 경로)를 빠르게 반환합니다. analyze_document보다 훨씬 빠릅니다. 문서가 열려 있는지, 몇 페이지인지만 빠르게 확인할 때 사용하세요.', {}, async () => {
|
|
125
|
+
if (!bridge.getCurrentDocument()) {
|
|
126
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
await bridge.ensureRunning();
|
|
130
|
+
const response = await bridge.send('get_document_info', {}, 10000);
|
|
131
|
+
if (!response.success) {
|
|
132
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
|
|
133
|
+
}
|
|
134
|
+
return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
server.tool('hwp_text_search', '문서에서 텍스트를 검색하고 발견된 위치와 횟수를 반환합니다. 치환 없이 검색만 합니다. 특정 텍스트가 문서에 있는지 확인하거나, 몇 번 등장하는지 파악할 때 사용하세요.', {
|
|
141
|
+
search: z.string().describe('검색할 텍스트'),
|
|
142
|
+
max_results: z.number().int().min(1).optional().describe('최대 검색 결과 수 (기본 50)'),
|
|
143
|
+
}, async ({ search, max_results }) => {
|
|
144
|
+
if (!bridge.getCurrentDocument()) {
|
|
145
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다. hwp_open_document로 문서를 열어주세요.' }) }], isError: true };
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
await bridge.ensureRunning();
|
|
149
|
+
const params = { search };
|
|
150
|
+
if (max_results)
|
|
151
|
+
params.max_results = max_results;
|
|
152
|
+
const response = await bridge.send('text_search', params, ANALYSIS_TIMEOUT);
|
|
153
|
+
if (!response.success) {
|
|
154
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
|
|
155
|
+
}
|
|
156
|
+
return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
// --- standard 이상에서만 등록되는 도구 ---
|
|
163
|
+
if (toolset !== 'minimal') {
|
|
164
|
+
server.tool('hwp_get_cell_format', '특정 표 셀의 글자 서식(글꼴, 크기, 자간, 장평, 굵기 등)과 단락 서식(정렬, 줄간격 등)을 조회합니다. 표 셀에 내용을 채우기 전에 해당 셀의 서식을 파악하여 동일한 서식으로 입력할 때 사용하세요.', {
|
|
165
|
+
table_index: z.number().int().min(0).describe('표 인덱스'),
|
|
166
|
+
cell_tab: z.number().int().min(0).describe('셀 Tab 인덱스 (hwp_map_table_cells로 확인)'),
|
|
167
|
+
}, async ({ table_index, cell_tab }) => {
|
|
168
|
+
if (!bridge.getCurrentDocument()) {
|
|
169
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다. hwp_open_document로 문서를 열어주세요.' }) }], isError: true };
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
await bridge.ensureRunning();
|
|
173
|
+
const response = await bridge.send('get_cell_format', { table_index, cell_tab }, ANALYSIS_TIMEOUT);
|
|
174
|
+
if (!response.success) {
|
|
175
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
|
|
176
|
+
}
|
|
177
|
+
return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
server.tool('hwp_get_table_format_summary', '표 전체의 서식 요약을 반환합니다. 샘플 셀들의 글꼴/크기/자간/장평/줄간격을 한번에 파악합니다. 표에 내용을 채우기 전에 서식 패턴을 파악할 때 사용하세요.', {
|
|
184
|
+
table_index: z.number().int().min(0).describe('표 인덱스'),
|
|
185
|
+
sample_tabs: z.array(z.number().int().min(0)).optional().describe('조회할 Tab 인덱스 목록 (생략 시 첫 5개+마지막)'),
|
|
186
|
+
}, async ({ table_index, sample_tabs }) => {
|
|
187
|
+
if (!bridge.getCurrentDocument()) {
|
|
188
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다. hwp_open_document로 문서를 열어주세요.' }) }], isError: true };
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
await bridge.ensureRunning();
|
|
192
|
+
const params = { table_index };
|
|
193
|
+
if (sample_tabs)
|
|
194
|
+
params.sample_tabs = sample_tabs;
|
|
195
|
+
const response = await bridge.send('get_table_format_summary', params, ANALYSIS_TIMEOUT);
|
|
196
|
+
if (!response.success) {
|
|
197
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
|
|
198
|
+
}
|
|
199
|
+
return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
server.tool('hwp_read_reference', '참고자료 파일(txt, csv, xlsx, json, md)의 내용을 추출합니다. 사업계획서 작성 시 참고 데이터를 가져올 때 사용하세요. HWP 파일은 hwp_analyze_document를 사용하세요.', {
|
|
206
|
+
file_path: z.string().describe('참고자료 파일 경로'),
|
|
207
|
+
max_chars: z.number().optional().describe('최대 문자 수 (기본 30000)'),
|
|
208
|
+
}, async ({ file_path, max_chars }) => {
|
|
209
|
+
const resolved = path.resolve(file_path);
|
|
210
|
+
if (!fs.existsSync(resolved)) {
|
|
211
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: `파일을 찾을 수 없습니다: ${resolved}` }) }], isError: true };
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
await bridge.ensureRunning();
|
|
215
|
+
const params = { file_path: resolved };
|
|
216
|
+
if (max_chars)
|
|
217
|
+
params.max_chars = max_chars;
|
|
218
|
+
const response = await bridge.send('read_reference', params, ANALYSIS_TIMEOUT);
|
|
219
|
+
if (!response.success) {
|
|
220
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
|
|
221
|
+
}
|
|
222
|
+
return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
server.tool('hwp_get_fields', '문서의 필드(양식) 목록과 현재 값을 조회합니다. 필드를 채우기 전에 어떤 필드가 있는지 확인할 때 사용하세요.', {
|
|
229
|
+
file_path: z.string().optional().describe('HWP/HWPX 파일 경로 (생략 시 현재 문서)'),
|
|
230
|
+
}, async ({ file_path }) => {
|
|
231
|
+
try {
|
|
232
|
+
const analysis = await ensureAnalysis(bridge, file_path);
|
|
233
|
+
const fields = analysis.fields || [];
|
|
234
|
+
const emptyCount = fields.filter(f => !f.value || f.value.trim() === '').length;
|
|
235
|
+
return {
|
|
236
|
+
content: [{
|
|
237
|
+
type: 'text',
|
|
238
|
+
text: JSON.stringify({
|
|
239
|
+
fields,
|
|
240
|
+
total_count: fields.length,
|
|
241
|
+
empty_count: emptyCount,
|
|
242
|
+
}),
|
|
243
|
+
}],
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
server.tool('hwp_get_as_markdown', '문서 내용을 마크다운 형식으로 변환하여 반환합니다. 표는 마크다운 테이블로, 텍스트는 구조화됩니다. AI가 문서 내용을 이해하기 가장 좋은 형식입니다.', {
|
|
251
|
+
file_path: z.string().optional().describe('HWP/HWPX 파일 경로 (생략 시 현재 문서)'),
|
|
252
|
+
}, async ({ file_path }) => {
|
|
253
|
+
try {
|
|
254
|
+
const analysis = await ensureAnalysis(bridge, file_path);
|
|
255
|
+
const parts = [];
|
|
256
|
+
// 제목
|
|
257
|
+
parts.push(`# ${analysis.file_name}`);
|
|
258
|
+
parts.push(`> ${analysis.file_format} | ${analysis.pages}페이지 | 표 ${analysis.tables?.length ?? 0}개 | 필드 ${analysis.fields?.length ?? 0}개\n`);
|
|
259
|
+
// 본문 텍스트
|
|
260
|
+
if (analysis.full_text) {
|
|
261
|
+
parts.push('## 본문');
|
|
262
|
+
parts.push(analysis.full_text);
|
|
263
|
+
parts.push('');
|
|
264
|
+
}
|
|
265
|
+
// 표 데이터를 마크다운 테이블로
|
|
266
|
+
const tables = analysis.tables || [];
|
|
267
|
+
for (const table of tables) {
|
|
268
|
+
parts.push(`## 표 ${table.index}`);
|
|
269
|
+
if (table.headers && table.headers.length > 0) {
|
|
270
|
+
// 헤더에서 빈 문자열을 '-'로 대체
|
|
271
|
+
const headers = table.headers.map((h) => h.replace(/\r?\n/g, ' ').trim() || '-');
|
|
272
|
+
parts.push('| ' + headers.join(' | ') + ' |');
|
|
273
|
+
parts.push('| ' + headers.map(() => '---').join(' | ') + ' |');
|
|
274
|
+
}
|
|
275
|
+
if (table.data) {
|
|
276
|
+
for (const row of table.data) {
|
|
277
|
+
const cells = row.map((c) => (c ?? '').replace(/\r?\n/g, ' ').trim() || '-');
|
|
278
|
+
parts.push('| ' + cells.join(' | ') + ' |');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
parts.push('');
|
|
282
|
+
}
|
|
283
|
+
// 필드
|
|
284
|
+
if (analysis.fields && analysis.fields.length > 0) {
|
|
285
|
+
parts.push('## 필드');
|
|
286
|
+
for (const field of analysis.fields) {
|
|
287
|
+
parts.push(`- **${field.name}**: ${field.value || '(비어있음)'}`);
|
|
288
|
+
}
|
|
289
|
+
parts.push('');
|
|
290
|
+
}
|
|
291
|
+
const markdown = parts.join('\n');
|
|
292
|
+
return {
|
|
293
|
+
content: [{
|
|
294
|
+
type: 'text',
|
|
295
|
+
text: JSON.stringify({
|
|
296
|
+
markdown,
|
|
297
|
+
char_count: markdown.length,
|
|
298
|
+
tables: tables.length,
|
|
299
|
+
fields: analysis.fields?.length ?? 0,
|
|
300
|
+
}),
|
|
301
|
+
}],
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
server.tool('hwp_get_page_text', '특정 페이지의 텍스트만 추출합니다. 전체 문서가 아닌 특정 페이지 내용만 필요할 때 사용하세요.', {
|
|
309
|
+
page: z.number().int().min(1).describe('페이지 번호 (1부터 시작)'),
|
|
310
|
+
}, async ({ page }) => {
|
|
311
|
+
if (!bridge.getCurrentDocument()) {
|
|
312
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
await bridge.ensureRunning();
|
|
316
|
+
// 전체 텍스트를 가져온 뒤 페이지별로 분리하는 방식 (COM API에 페이지별 추출 없음)
|
|
317
|
+
const response = await bridge.send('analyze_document', {
|
|
318
|
+
file_path: bridge.getCurrentDocument(),
|
|
319
|
+
}, ANALYSIS_TIMEOUT);
|
|
320
|
+
if (!response.success) {
|
|
321
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
|
|
322
|
+
}
|
|
323
|
+
const analysis = response.data;
|
|
324
|
+
const totalPages = analysis.pages || 1;
|
|
325
|
+
if (page > totalPages) {
|
|
326
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
327
|
+
error: `페이지 ${page}은 범위를 벗어났습니다 (총 ${totalPages}페이지)`,
|
|
328
|
+
}) }], isError: true };
|
|
329
|
+
}
|
|
330
|
+
// 전체 텍스트를 페이지 수로 균등 분할 (근사치)
|
|
331
|
+
const text = analysis.full_text || '';
|
|
332
|
+
const charsPerPage = Math.ceil(text.length / totalPages);
|
|
333
|
+
const start = (page - 1) * charsPerPage;
|
|
334
|
+
const pageText = text.slice(start, start + charsPerPage);
|
|
335
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
336
|
+
page,
|
|
337
|
+
total_pages: totalPages,
|
|
338
|
+
text: pageText,
|
|
339
|
+
char_count: pageText.length,
|
|
340
|
+
note: '페이지 분할은 텍스트 길이 기반 근사치입니다',
|
|
341
|
+
}) }] };
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
server.tool('hwp_image_extract', '문서의 모든 이미지를 지정 디렉토리에 추출합니다. 문서 내 이미지를 파일로 가져올 때 사용하세요.', {
|
|
348
|
+
output_dir: z.string().describe('이미지 저장 디렉토리 경로'),
|
|
349
|
+
}, async ({ output_dir }) => {
|
|
350
|
+
if (!bridge.getCurrentDocument())
|
|
351
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
352
|
+
try {
|
|
353
|
+
await bridge.ensureRunning();
|
|
354
|
+
const resolved = path.resolve(output_dir);
|
|
355
|
+
const r = await bridge.send('image_extract', { output_dir: resolved }, ANALYSIS_TIMEOUT);
|
|
356
|
+
if (!r.success)
|
|
357
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
|
|
358
|
+
return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
server.tool('hwp_document_split', '문서를 페이지 단위로 분할하여 별도 파일로 저장합니다. 긴 문서를 나눌 때 사용하세요.', {
|
|
365
|
+
output_dir: z.string().describe('분할 파일 저장 디렉토리'),
|
|
366
|
+
pages_per_split: z.number().int().min(1).optional().describe('분할 단위 페이지 수 (기본: 1)'),
|
|
367
|
+
}, async ({ output_dir, pages_per_split }) => {
|
|
368
|
+
if (!bridge.getCurrentDocument())
|
|
369
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
370
|
+
try {
|
|
371
|
+
await bridge.ensureRunning();
|
|
372
|
+
const resolved = path.resolve(output_dir);
|
|
373
|
+
const params = { output_dir: resolved };
|
|
374
|
+
if (pages_per_split)
|
|
375
|
+
params.pages_per_split = pages_per_split;
|
|
376
|
+
const r = await bridge.send('document_split', params, ANALYSIS_TIMEOUT * 2);
|
|
377
|
+
if (!r.success)
|
|
378
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
|
|
379
|
+
return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
|
|
380
|
+
}
|
|
381
|
+
catch (err) {
|
|
382
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
server.tool('hwp_form_detect', '문서에서 양식 필드(빈 괄호, 체크박스, 밑줄 등)를 자동 감지합니다. 양식을 채우기 전에 어떤 필드가 있는지 파악할 때 사용하세요.', {}, async () => {
|
|
386
|
+
if (!bridge.getCurrentDocument())
|
|
387
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
388
|
+
try {
|
|
389
|
+
await bridge.ensureRunning();
|
|
390
|
+
const r = await bridge.send('form_detect', {}, ANALYSIS_TIMEOUT);
|
|
391
|
+
if (!r.success)
|
|
392
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
|
|
393
|
+
return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
|
|
394
|
+
}
|
|
395
|
+
catch (err) {
|
|
396
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
server.tool('hwp_extract_style_profile', '양식 문서에서 서식 프로파일(글꼴/크기/자간/장평/줄간격/들여쓰기/여백)을 추출합니다. 양식 파일을 제공받았을 때 서식을 파악하여 동일하게 적용할 때 사용하세요.', {}, async () => {
|
|
400
|
+
if (!bridge.getCurrentDocument())
|
|
401
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
402
|
+
try {
|
|
403
|
+
await bridge.ensureRunning();
|
|
404
|
+
const r = await bridge.send('extract_style_profile', {}, ANALYSIS_TIMEOUT);
|
|
405
|
+
if (!r.success)
|
|
406
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
|
|
407
|
+
return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
} // end toolset !== 'minimal'
|
|
414
|
+
}
|