deel-local-cli 1.2.0 → 1.4.1
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.en.md +301 -1537
- package/README.md +272 -1582
- package/bin/deel.js +73 -4
- package/package.json +8 -3
- package/src/acp/jsonrpc.js +230 -0
- package/src/acp/map.js +219 -0
- package/src/acp/serve.js +556 -0
- package/src/agent/card.js +129 -0
- package/src/agent/compact.js +10 -2
- package/src/agent/effort.js +5 -0
- package/src/agent/evidence.js +186 -0
- package/src/agent/grade.js +20 -0
- package/src/agent/loop.js +134 -13
- package/src/agent/models.js +169 -0
- package/src/agent/modes.js +47 -1
- package/src/agent/pins.js +140 -0
- package/src/agent/preset.js +113 -0
- package/src/agent/project.js +10 -4
- package/src/agent/session.js +233 -13
- package/src/agent/store.js +31 -0
- package/src/backend/adapter.js +14 -0
- package/src/commands.js +616 -52
- package/src/i18n/en.js +265 -0
- package/src/i18n/index.js +126 -0
- package/src/i18n/ko.js +252 -0
- package/src/lsp/client.js +459 -0
- package/src/lsp/diag.js +112 -0
- package/src/lsp/rpc.js +84 -0
- package/src/lsp/servers.js +218 -0
- package/src/oneshot.js +19 -0
- package/src/pack/sbom.js +218 -0
- package/src/pack/selfpack.js +19 -3
- package/src/preview/serve.js +19 -2
- package/src/repl.js +193 -8
- package/src/safety/secrets.js +205 -0
- package/src/safety/undo.js +10 -3
- package/src/tools/desc.en.js +221 -0
- package/src/tools/docs.js +252 -0
- package/src/tools/index.js +190 -6
- package/src/tools/lsp.js +327 -0
- package/src/tools/task.js +30 -2
- package/src/ui/ansi.js +45 -0
- package/src/ui/approve.js +25 -21
- package/src/ui/banner.js +245 -0
- package/src/ui/export.js +217 -0
- package/src/ui/inputbox.js +37 -7
- package/src/ui/intro.js +206 -0
- package/src/ui/level.js +11 -5
- package/src/ui/notify.js +101 -0
- package/src/ui/status.js +181 -35
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 도구 설명 영어판 — 모델이 읽는 글이다.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 여기 따로 두나 ───────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* index.js 의 한글 설명은 손대지 않는다. 저 글은 오래 겪으면서 한 문장씩
|
|
7
|
+
* 눌러 온 것이라(“앞부분을 다시 보내지 마라”, “파일 목록만 봐서는 안 보인다”),
|
|
8
|
+
* 그 파일을 두 언어로 갈라 놓으면 다음에 한쪽만 고치게 된다. 여기 표에
|
|
9
|
+
* 없는 것은 한글 설명이 그대로 나간다 — 화면 말과 같은 규칙이다.
|
|
10
|
+
*
|
|
11
|
+
* ── 왜 옮기나 ───────────────────────────────────────────────────────────
|
|
12
|
+
*
|
|
13
|
+
* 두 가지다. 하나는 영어로 켠 사람이 한국어 도구 설명을 받으면, 모델이
|
|
14
|
+
* 무엇을 골라야 할지를 못 읽는다는 것. 다른 하나는 **토큰**이다.
|
|
15
|
+
* 도구 정의는 매 요청에 통째로 실리는 고정 몫인데, 한글은 글자당 약 1토큰이고
|
|
16
|
+
* 영문은 약 3.6자당 1토큰이다. 32k 창에서 이 몫이 10% 를 넘게 먹고 있었다.
|
|
17
|
+
*
|
|
18
|
+
* ── 옮길 때 지킨 것 ─────────────────────────────────────────────────────
|
|
19
|
+
*
|
|
20
|
+
* 규칙을 한 줄도 안 뺐다. 특히 이런 줄들은 글자 그대로 옮겼다 —
|
|
21
|
+
* 빠지면 그 모델만 조용히 다르게 굴고, 그 차이는 몇 걸음 뒤에야 드러난다.
|
|
22
|
+
*
|
|
23
|
+
* Edit “먼저 Read 로 읽어야 한다” · 안 읽고 고치는 것을 막는 자리
|
|
24
|
+
* Append“앞부분을 다시 보내지 마라” · 같은 자리에서 또 잘리는 것을 막는 자리
|
|
25
|
+
* Bash “끝나지 않는 것은 background” · 시간 초과로 죽는 것을 막는 자리
|
|
26
|
+
* Verify“확인 못 한 것은 못 했다고” · 이 프로그램이 거짓말을 안 하게 하는 자리
|
|
27
|
+
*
|
|
28
|
+
* 도구 이름과 인자 이름은 **안 옮긴다.** 그건 식별자다. Task 의 목적·할일처럼
|
|
29
|
+
* 한글로 된 인자 이름도 그대로 둔다 — 이름을 바꾸면 그 도구가 아예 안 불린다.
|
|
30
|
+
*/
|
|
31
|
+
export const 도구설명EN = {
|
|
32
|
+
Read: {
|
|
33
|
+
desc: 'Read one file. Line numbers come back with it. You must read a file before editing it.'
|
|
34
|
+
+ ' Excel files (.xlsx/.xlsm/.xls) can be read directly too — they come back as CSV per sheet.'
|
|
35
|
+
+ ' Korean HWP, Word and PowerPoint documents (.hwpx/.docx/.pptx) read directly as well —'
|
|
36
|
+
+ ' they come back as plain text. There is no need to ask the user to export anything.'
|
|
37
|
+
+ ' All of these are read-only here, though.',
|
|
38
|
+
params: {
|
|
39
|
+
file_path: 'path of the file to read',
|
|
40
|
+
offset: 'first line (1-based). Only for large files',
|
|
41
|
+
limit: 'how many lines to read',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
Write: {
|
|
45
|
+
desc: 'Create a file, or overwrite one completely. Use Edit to change part of a file.'
|
|
46
|
+
+ ' **You can create several files in one call** — pass them as an array in files.'
|
|
47
|
+
+ ' Do that when you are laying out a folder structure. One call per file means one model'
|
|
48
|
+
+ ' round trip per file, and an eight-file skeleton loses minutes to that.',
|
|
49
|
+
params: {
|
|
50
|
+
file_path: 'path to write (single file)',
|
|
51
|
+
content: 'the whole file content (single file)',
|
|
52
|
+
files: 'several files at once. When you use this, leave file_path and content out.',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
Append: {
|
|
56
|
+
desc: 'Append to the end of a file. This is how you build a large file — Write the first part,'
|
|
57
|
+
+ ' then call Append repeatedly until it is complete. Splitting it and landing it for certain'
|
|
58
|
+
+ ' beats trying to fit it in one call and getting cut off. No Read needed first — you are only'
|
|
59
|
+
+ ' adding to the end, so there is nothing to read.',
|
|
60
|
+
params: {
|
|
61
|
+
file_path: 'path of the file to append to',
|
|
62
|
+
content: 'what to add at the end',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
Edit: {
|
|
66
|
+
desc: 'Replace an exactly matching string in a file. You must Read it first.'
|
|
67
|
+
+ ' **If there are several places to change, send them in one call as the edits array** —'
|
|
68
|
+
+ ' they may even be in different files. One call per place means one model round trip per'
|
|
69
|
+
+ ' place, and a six-place cleanup loses minutes to that.',
|
|
70
|
+
params: {
|
|
71
|
+
file_path: 'path of the file to edit (single edit)',
|
|
72
|
+
old_string: 'what to replace. Must be unique within the file',
|
|
73
|
+
new_string: 'what to replace it with',
|
|
74
|
+
replace_all: 'true to replace every occurrence',
|
|
75
|
+
edits: 'several places at once, applied in the order given. When you use this, leave the arguments above out.',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
Glob: {
|
|
79
|
+
desc: 'Find files by name pattern. e.g. **/*.js, src/**/*.{ts,tsx}',
|
|
80
|
+
params: {
|
|
81
|
+
pattern: 'glob pattern',
|
|
82
|
+
path: 'folder to start from. Defaults to the whole working folder',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
Grep: {
|
|
86
|
+
desc: 'Search file contents with a regular expression.',
|
|
87
|
+
params: {
|
|
88
|
+
pattern: 'regular expression',
|
|
89
|
+
path: 'folder or file to search',
|
|
90
|
+
glob: 'restrict which files. e.g. **/*.js',
|
|
91
|
+
output_mode: 'defaults to files_with_matches',
|
|
92
|
+
'-i': 'ignore case',
|
|
93
|
+
'-n': 'show line numbers',
|
|
94
|
+
head_limit: 'cap the number of results',
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
Skill: {
|
|
98
|
+
desc: 'Open one skill and read it. The list carries only names and descriptions,'
|
|
99
|
+
+ ' so pick the one you need and pull its body with this.',
|
|
100
|
+
params: { name: 'skill name, exactly as listed' },
|
|
101
|
+
},
|
|
102
|
+
Bash: {
|
|
103
|
+
desc: 'Run a command. Commands that cannot be undone are blocked.'
|
|
104
|
+
+ ' **Anything that never ends (dev servers, watch) must be started with background: true** —'
|
|
105
|
+
+ ' called plainly it dies on timeout. After starting one, read its output with Jobs.',
|
|
106
|
+
params: {
|
|
107
|
+
command: 'the command to run',
|
|
108
|
+
description: 'one line on what this command does',
|
|
109
|
+
timeout: 'time limit in ms. Default 120000',
|
|
110
|
+
background: 'true for a command that never ends. Returns immediately; read it with Jobs',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
WebFetch: {
|
|
114
|
+
desc: 'Read a web page. Read-only — nothing is sent. Use it to check documentation, an error'
|
|
115
|
+
+ ' message, or how a library is used. Addresses on this machine or an internal network are'
|
|
116
|
+
+ ' not read. Several calls to the same site go out one after another, so they cost that much'
|
|
117
|
+
+ ' more time — fetch only what you need. Truncated JSON cannot be read, so if it comes back'
|
|
118
|
+
+ ' cut, narrow the request or raise max_chars and call again.',
|
|
119
|
+
params: {
|
|
120
|
+
url: 'address to read (http/https)',
|
|
121
|
+
max_chars: 'maximum characters to pull. Left out, it is sized to the model. Raise it if the'
|
|
122
|
+
+ ' material comes back cut (max 120000)',
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
Recall: {
|
|
126
|
+
desc: 'Search past sessions in this folder. When the user points back ("last time"), use this'
|
|
127
|
+
+ ' instead of asking again. This does not search file contents — that is Grep.',
|
|
128
|
+
params: {
|
|
129
|
+
query: 'what to look for. Two or three words (e.g. "CP949 encoding")',
|
|
130
|
+
limit: 'how many to bring back (default 8)',
|
|
131
|
+
tools: 'also dig through tool results (default false)',
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
Remember: {
|
|
135
|
+
desc: 'Write one line that outlives this session. Rules the user set, promises made, mistakes'
|
|
136
|
+
+ ' not to repeat. Do not record anything that only applies to this job, or anything a file'
|
|
137
|
+
+ ' would tell you. This line rides on every later request — keep it to one sentence.',
|
|
138
|
+
params: { text: 'one line (e.g. "internal documents are read as CP949 and written back as CP949")' },
|
|
139
|
+
},
|
|
140
|
+
TodoWrite: {
|
|
141
|
+
desc: 'Create and update the todo list. For anything that takes several steps, build the list'
|
|
142
|
+
+ ' first and update it the moment each step finishes. Always send the whole list. state is'
|
|
143
|
+
+ ' one of todo / doing / done, and only one item may be doing at a time. The number of steps'
|
|
144
|
+
+ ' is set by the size of the job — there is no fixed count and no cap. Do not squeeze unrelated'
|
|
145
|
+
+ ' work into one line to hit a number. Write each step small enough to check on its own.',
|
|
146
|
+
params: { todos: 'the whole todo list. No cap — as many as the job needs' },
|
|
147
|
+
},
|
|
148
|
+
Verify: {
|
|
149
|
+
desc: 'Check that what you made actually works. Call this **before** you finish, without fail.'
|
|
150
|
+
+ ' A file existing and a file working are different things — an unclosed tag, a src pointing'
|
|
151
|
+
+ ' at a file that is not there, one missing bracket in JS: none of that shows in a file listing.'
|
|
152
|
+
+ ' What can be run gets run (node --check, py_compile); what cannot gets read (HTML tag pairs,'
|
|
153
|
+
+ ' missing references, CSS braces, JSON). Whatever it could not check, it tells you it could not.'
|
|
154
|
+
+ ' Running tests or a build is Bash — that goes through the user for approval.',
|
|
155
|
+
params: { paths: 'files to check. Left out, it checks everything checkable in the working folder.' },
|
|
156
|
+
},
|
|
157
|
+
Outline: {
|
|
158
|
+
desc: 'See only the **skeleton** of a folder or file — per file, the names and line numbers of'
|
|
159
|
+
+ ' functions, classes, types, and headings. Call this before touching code you did not write.'
|
|
160
|
+
+ ' It tells you what is where for a fraction of what reading whole files costs. Pick the places'
|
|
161
|
+
+ ' to change here, then Read **only those files**. Reads js/ts, py, java/kotlin, go, rust, c#,'
|
|
162
|
+
+ ' md, html, css, sh, json.',
|
|
163
|
+
params: {
|
|
164
|
+
path: 'folder or file path. Defaults to the whole working folder',
|
|
165
|
+
pattern: 'narrow by name (e.g. **/*.js). Left out, everything',
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
Task: {
|
|
169
|
+
desc: 'Split a chunk of a large job off as a subtask and **run it separately.** The subtask works'
|
|
170
|
+
+ ' start to finish in its own conversation and returns only a summary — the files it read do'
|
|
171
|
+
+ ' not pile up in your window. That is why work that creates or edits several files has to be'
|
|
172
|
+
+ ' divided this way to get to the end. One chunk must be finishable on its own (e.g. "create'
|
|
173
|
+
+ ' index.html and style.css"). The subtask cannot see your conversation — put everything it'
|
|
174
|
+
+ ' needs into 할일. Do not use this for one short job. Doing it yourself is faster.',
|
|
175
|
+
params: {
|
|
176
|
+
목적: 'this chunk in one line (e.g. "build the dashboard page skeleton")',
|
|
177
|
+
할일: 'everything the subtask has to do. It cannot see this conversation, so put the background,'
|
|
178
|
+
+ ' the decisions, and the file paths here. Say what counts as done, too.',
|
|
179
|
+
모드: 'how the subtask works: code (builds and edits) · debug (finds causes) · ask (reads and'
|
|
180
|
+
+ ' answers only). Defaults to code.',
|
|
181
|
+
모델: 'hand this chunk to a **different model**. Only profile names the user has configured'
|
|
182
|
+
+ ' work (do not invent an address — it will not be accepted). Left out, it stays on the model'
|
|
183
|
+
+ ' you are using. Handing routine work (formatting, repetitive edits, short summaries) to a'
|
|
184
|
+
+ ' small model keeps your window from filling. Do the work that needs judgement yourself.',
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
Def: {
|
|
188
|
+
desc: 'Ask the language server **where a name is defined.** You get the location without reading'
|
|
189
|
+
+ ' the file. Unlike Grep it does not hand you the wrong places — not the same name in a comment,'
|
|
190
|
+
+ ' not the same name in a third-party library, not the same name inside a string.'
|
|
191
|
+
+ ' Call this before touching code you did not write. Once you know where it is, Read only that file.'
|
|
192
|
+
+ ' If the name exists in several places you get the list, and file_path picks one.',
|
|
193
|
+
params: {
|
|
194
|
+
name: 'the name to find (function, class, variable)',
|
|
195
|
+
file_path: 'the file the name is used in. Use it when the same name exists in several places',
|
|
196
|
+
line: 'line number inside file_path where the name appears (1-based)',
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
Refs: {
|
|
200
|
+
desc: 'Ask the language server for **every place a name is used.** Call it before you rename'
|
|
201
|
+
+ ' something or change a function — this is what tells you how many places have to change together.'
|
|
202
|
+
+ ' Unlike the hundreds of lines Grep gives you, only the places that really use it come back.'
|
|
203
|
+
+ ' Grep still finds comments, config and docs, though: use this for the code and Grep for the rest'
|
|
204
|
+
+ ' when you rename something outright.',
|
|
205
|
+
params: {
|
|
206
|
+
name: 'the name to find (function, class, variable)',
|
|
207
|
+
file_path: 'the file the name is defined in. Use it when the same name exists in several places',
|
|
208
|
+
line: 'line number inside file_path where the name appears (1-based)',
|
|
209
|
+
include_declaration: 'include the definition itself. Default false',
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
Jobs: {
|
|
213
|
+
desc: 'List, read, and end background commands (Bash with background). Called with no number,'
|
|
214
|
+
+ ' you get the list. Given a number, you get whatever output arrived since last time.'
|
|
215
|
+
+ ' If you started a server, you must end it when the job is done.',
|
|
216
|
+
params: {
|
|
217
|
+
번호: 'job number to look at. Left out, the list',
|
|
218
|
+
끝내기: 'true to end that job (stop)',
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 사내 문서 읽기 — hwpx · docx · pptx → 글.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 필요한가 ─────────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* 사내망의 스펙·공문·회의록이 이 세 형식이다. 지금까지는 Read 가 "바이너리라
|
|
7
|
+
* 못 읽음" 으로 끝났고, 그 끝이 사고를 낳았다 — hwp 를 정리해 달라고 했더니
|
|
8
|
+
* Read 가 실패하자 모델이 Write 로 새로 써서 원본이 죽었다. 덮어쓰기는 이제
|
|
9
|
+
* 막혀 있지만, **읽기가 되면 그 길 자체가 없어진다.** 문서를 주고 "이대로
|
|
10
|
+
* 만들어" 라고 시키는 일이 비로소 된다.
|
|
11
|
+
*
|
|
12
|
+
* 해외 CLI 어느 것도 hwp 계열을 못 읽는다. 이 파일이 있는 자리(한국 사내망)가
|
|
13
|
+
* 이 프로그램이 도는 자리다.
|
|
14
|
+
*
|
|
15
|
+
* ── 왜 의존성 없이 되나 ─────────────────────────────────────────────────
|
|
16
|
+
*
|
|
17
|
+
* 셋 다 속은 ZIP + XML 이다(hwpx 는 한컴의 공개 규격 OWPML).
|
|
18
|
+
* ZIP 은 우리 zip 읽개(pack/zip.js)로, XML 은 xlsx 가 쓰는 작은 읽기(tags)로
|
|
19
|
+
* 푼다. 새로 들이는 것이 없다.
|
|
20
|
+
*
|
|
21
|
+
* hwpx Contents/section*.xml 글은 <hp:t>, 문단은 <hp:p>, 표는 <hp:tbl>
|
|
22
|
+
* docx word/document.xml 글은 <w:t>, 문단은 <w:p>, 표는 <w:tbl>
|
|
23
|
+
* pptx ppt/slides/slide*.xml 글은 <a:t>, 문단은 <a:p>, 장이 구획
|
|
24
|
+
*
|
|
25
|
+
* ── 읽기만 한다 ─────────────────────────────────────────────────────────
|
|
26
|
+
*
|
|
27
|
+
* 엑셀과 같은 이유로 고치기는 안 한다. 서식·그림·양식이 든 문서를 글로
|
|
28
|
+
* 왕복시키면 반드시 뭔가 잃는다. 잃은 채로 저장된 문서는 겉보기에 멀쩡해서,
|
|
29
|
+
* 잃은 것을 알아차렸을 때는 원본이 없다.
|
|
30
|
+
*
|
|
31
|
+
* 구형 hwp(OLE 복합문서)는 아예 다른 물건이라 여기서 안 다룬다. 대신 못
|
|
32
|
+
* 읽는다고 말할 때 **어떻게 하면 되는지**(한글에서 hwpx 로 저장)를 같이 준다 —
|
|
33
|
+
* 길 없는 거절은 모델을 우회로(새로 쓰기)로 몬다.
|
|
34
|
+
*/
|
|
35
|
+
import { readFileSync } from 'node:fs';
|
|
36
|
+
import { extname, basename } from 'node:path';
|
|
37
|
+
import { readZip, looksZip } from '../pack/zip.js';
|
|
38
|
+
import { tags, unescapeXml } from './xlsx.js';
|
|
39
|
+
|
|
40
|
+
const 확장자 = { '.hwpx': 'hwpx', '.docx': 'docx', '.pptx': 'pptx' };
|
|
41
|
+
|
|
42
|
+
/** 이 길로 읽는 파일인가. 구형 .hwp 는 아니다 — 그건 별도 안내 대상이다. */
|
|
43
|
+
export function isDocPath(p) {
|
|
44
|
+
if (typeof p !== 'string' || !p) return false;
|
|
45
|
+
return extname(p).toLowerCase() in 확장자;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 어떤 문서인가. */
|
|
49
|
+
export function 종류(p) {
|
|
50
|
+
return 확장자[extname(String(p ?? '')).toLowerCase()] ?? null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 구형 hwp 인가 (OLE 복합문서 서명). Read 가 안내문을 고를 때 쓴다. */
|
|
54
|
+
export function looksOldHwp(경로, buf) {
|
|
55
|
+
if (extname(String(경로 ?? '')).toLowerCase() !== '.hwp') return false;
|
|
56
|
+
return buf.length >= 8 && buf.readUInt32LE(0) === 0xe011cfd0 && buf.readUInt32LE(4) === 0xe11ab1a1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 구형 hwp 를 만났을 때 하는 말. 길을 같이 준다. */
|
|
60
|
+
export function 옛hwp안내(보인이름) {
|
|
61
|
+
return `구형 hwp 형식이라 읽지 못합니다: ${보인이름}\n`
|
|
62
|
+
+ ' 한글(한컴오피스)에서 이 파일을 열어 **hwpx 로 저장**하면 그대로 읽을 수 있습니다.\n'
|
|
63
|
+
+ ' (다른 이름으로 저장 → 파일 형식에서 hwpx 선택)\n'
|
|
64
|
+
+ ' 이 파일을 Write 로 새로 만들면 안 됩니다 — 원본이 사라집니다.';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/*
|
|
68
|
+
* ── XML → 문단들 ────────────────────────────────────────────────────────
|
|
69
|
+
*
|
|
70
|
+
* 형식마다 태그 이름만 다르고 뼈대는 같다: 문단 태그 사이의 글 태그를 모으고,
|
|
71
|
+
* 표 칸은 | 로 잇는다. 그래서 이름표만 형식별로 두고 걷는 것은 하나다.
|
|
72
|
+
*
|
|
73
|
+
* 이름 앞머리(hp: · w: · a:)는 문서마다 접두어가 다를 수 있어 **끝 이름**으로
|
|
74
|
+
* 견준다. 접두어는 XML 선언부가 정하는 별명일 뿐이라 값이 고정이 아니다.
|
|
75
|
+
*/
|
|
76
|
+
const 이름표 = {
|
|
77
|
+
hwpx: { 글: 't', 문단: 'p', 표: 'tbl', 행: 'tr', 칸: 'tc', 줄바꿈: null, 탭: null },
|
|
78
|
+
docx: { 글: 't', 문단: 'p', 표: 'tbl', 행: 'tr', 칸: 'tc', 줄바꿈: 'br', 탭: 'tab' },
|
|
79
|
+
pptx: { 글: 't', 문단: 'p', 표: 'tbl', 행: 'tr', 칸: 'tc', 줄바꿈: 'br', 탭: null },
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const 끝이름 = (name) => {
|
|
83
|
+
const i = name.indexOf(':');
|
|
84
|
+
return i < 0 ? name : name.slice(i + 1);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* XML 한 장을 문단 목록으로.
|
|
89
|
+
*
|
|
90
|
+
* 표는 행마다 한 줄로 펴고 칸을 ` | ` 로 잇는다. 칸을 그냥 이어 붙이면
|
|
91
|
+
* "이름값가1" 같은 덩어리가 되는데, 그건 뜻이 사라진 글이라 안 읽은 것보다
|
|
92
|
+
* 나쁘다 — 모델이 그걸 근거로 답한다.
|
|
93
|
+
*/
|
|
94
|
+
export function 문단뽑기(xml, 갈래) {
|
|
95
|
+
const 표기 = 이름표[갈래];
|
|
96
|
+
const 문단들 = [];
|
|
97
|
+
|
|
98
|
+
let 글모음 = []; // 지금 문단의 글 조각들
|
|
99
|
+
let 표깊이 = 0;
|
|
100
|
+
let 행칸들 = null; // 표 행 안일 때: 칸 글들
|
|
101
|
+
let 칸글 = null; // 표 칸 안일 때: 그 칸의 글 조각들
|
|
102
|
+
let 글안 = false; // <t> 안인가 — 글 태그 밖의 지시문·수식 글을 안 줍기 위해
|
|
103
|
+
|
|
104
|
+
const 문단닫기 = () => {
|
|
105
|
+
const 글 = 글모음.join('').trimEnd();
|
|
106
|
+
글모음 = [];
|
|
107
|
+
if (글.trim()) 문단들.push(글);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
for (const t of tags(xml)) {
|
|
111
|
+
if (t.text !== undefined) {
|
|
112
|
+
if (!글안 || t.blank) continue;
|
|
113
|
+
const 글 = unescapeXml(t.text);
|
|
114
|
+
if (칸글) 칸글.push(글);
|
|
115
|
+
else 글모음.push(글);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const 이름 = 끝이름(t.name);
|
|
119
|
+
|
|
120
|
+
if (이름 === 표기.글) { 글안 = !t.closing && !t.selfClosing; continue; }
|
|
121
|
+
if (표기.탭 && 이름 === 표기.탭 && !t.closing) { (칸글 ?? 글모음).push('\t'); continue; }
|
|
122
|
+
if (표기.줄바꿈 && 이름 === 표기.줄바꿈 && !t.closing) { (칸글 ?? 글모음).push('\n'); continue; }
|
|
123
|
+
|
|
124
|
+
if (이름 === 표기.표) { 표깊이 += t.closing ? -1 : (t.selfClosing ? 0 : 1); continue; }
|
|
125
|
+
if (표깊이 > 0 && 이름 === 표기.행) {
|
|
126
|
+
if (t.closing) {
|
|
127
|
+
if (행칸들) 문단들.push(행칸들.map((x) => x.trim()).join(' | '));
|
|
128
|
+
행칸들 = null;
|
|
129
|
+
} else {
|
|
130
|
+
행칸들 = [];
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (표깊이 > 0 && 이름 === 표기.칸) {
|
|
135
|
+
if (t.closing) {
|
|
136
|
+
if (행칸들 && 칸글) 행칸들.push(칸글.join(''));
|
|
137
|
+
칸글 = null;
|
|
138
|
+
} else {
|
|
139
|
+
칸글 = [];
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (이름 === 표기.문단 && t.closing && !칸글) 문단닫기();
|
|
145
|
+
}
|
|
146
|
+
문단닫기(); // 안 닫힌 채 끝나는 문서도 있다. 마지막 글을 버리지 않는다.
|
|
147
|
+
return 문단들;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/*
|
|
151
|
+
* 형식별로 어느 파일이 알맹이인가.
|
|
152
|
+
*
|
|
153
|
+
* 이름을 **숫자로** 세운다. 글자로 세우면 slide10 이 slide2 앞에 온다 —
|
|
154
|
+
* 장 차례가 뒤섞인 발표 자료는 안 읽은 것보다 나쁘다.
|
|
155
|
+
*/
|
|
156
|
+
const 알맹이 = {
|
|
157
|
+
hwpx: { 골라 : /^Contents\/section(\d+)\.xml$/i, 구획이름: (n) => `구획 ${n + 1}` },
|
|
158
|
+
docx: { 골라 : /^word\/document\.xml$/i, 구획이름: () => '본문' },
|
|
159
|
+
pptx: { 골라 : /^ppt\/slides\/slide(\d+)\.xml$/i, 구획이름: (n, 번호) => `${번호}장` },
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 문서 하나를 읽는다.
|
|
164
|
+
*
|
|
165
|
+
* @returns {{ok:true, 갈래, 덩이들:[{이름, 문단들}]} | {ok:false, error}}
|
|
166
|
+
* 던지지 않는다 — 깨진 파일은 도구 실행 한가운데서 만나는 것이라,
|
|
167
|
+
* 예외가 나면 "문서가 깨졌다" 가 "도구가 터졌다" 로 보고된다.
|
|
168
|
+
*/
|
|
169
|
+
export function readDoc(경로또는버퍼) {
|
|
170
|
+
const 갈래 = Buffer.isBuffer(경로또는버퍼) ? null : 종류(경로또는버퍼);
|
|
171
|
+
let buf;
|
|
172
|
+
try {
|
|
173
|
+
buf = Buffer.isBuffer(경로또는버퍼) ? 경로또는버퍼 : readFileSync(경로또는버퍼);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
return { ok: false, error: `못 읽었습니다: ${err.message}` };
|
|
176
|
+
}
|
|
177
|
+
if (!갈래) return { ok: false, error: '어떤 문서인지 모르는 경로입니다' };
|
|
178
|
+
if (!looksZip(buf)) {
|
|
179
|
+
return { ok: false, error: `${갈래} 모양이 아닙니다 — 겉은 ${갈래} 인데 속이 zip 꾸러미가 아닙니다. 깨졌거나 다른 형식입니다.` };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let 꾸러미;
|
|
183
|
+
try {
|
|
184
|
+
꾸러미 = readZip(buf);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
return { ok: false, error: `꾸러미를 풀지 못했습니다 — ${err.message}` };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const { 골라, 구획이름 } = 알맹이[갈래];
|
|
190
|
+
const 찾은 = [];
|
|
191
|
+
for (const [이름, 몸] of 꾸러미.files) {
|
|
192
|
+
const m = 골라.exec(이름.replace(/\\/g, '/'));
|
|
193
|
+
if (m) 찾은.push({ 번호: m[1] ? Number(m[1]) : 0, 몸 });
|
|
194
|
+
}
|
|
195
|
+
if (!찾은.length) {
|
|
196
|
+
return { ok: false, error: `${갈래} 꾸러미인데 본문을 찾지 못했습니다. 깨졌거나 비정상 파일입니다.` };
|
|
197
|
+
}
|
|
198
|
+
찾은.sort((a, b) => a.번호 - b.번호);
|
|
199
|
+
|
|
200
|
+
const 덩이들 = 찾은.map((s, i) => ({
|
|
201
|
+
이름: 구획이름(i, s.번호),
|
|
202
|
+
문단들: 문단뽑기(s.몸.toString('utf8'), 갈래),
|
|
203
|
+
}));
|
|
204
|
+
return { ok: true, 갈래, 덩이들 };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 다 실어 봐야 창만 찬다. 엑셀(toText)과 같은 상한을 쓴다.
|
|
208
|
+
const 최대글자 = 60000;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 덩이들을 한 장의 글로.
|
|
212
|
+
*
|
|
213
|
+
* 자르면 잘랐다고 말한다 — 조용히 자르면 모델은 그게 전부인 줄 알고
|
|
214
|
+
* "문서에 그런 내용 없다" 고 답한다.
|
|
215
|
+
*/
|
|
216
|
+
export function toText(덩이들, { maxChars = 최대글자 } = {}) {
|
|
217
|
+
const 잘림 = [];
|
|
218
|
+
const 조각 = [];
|
|
219
|
+
let 셈 = 0;
|
|
220
|
+
let 여럿 = (덩이들?.length ?? 0) > 1;
|
|
221
|
+
|
|
222
|
+
for (const d of 덩이들 ?? []) {
|
|
223
|
+
if (여럿) 조각.push(`--- ${d.이름} ---`);
|
|
224
|
+
for (const 문단 of d.문단들) {
|
|
225
|
+
if (셈 + 문단.length > maxChars) {
|
|
226
|
+
잘림.push(`${maxChars.toLocaleString()}자에서 잘랐습니다 — 뒷부분은 안 실렸습니다`);
|
|
227
|
+
return { text: 조각.join('\n'), 잘림 };
|
|
228
|
+
}
|
|
229
|
+
조각.push(문단);
|
|
230
|
+
셈 += 문단.length + 1;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return { text: 조각.join('\n'), 잘림 };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** 한 줄 요약. Read 의 summary 자리로 간다. */
|
|
237
|
+
export function summarize(r) {
|
|
238
|
+
if (!r?.ok) return '';
|
|
239
|
+
const 문단수 = r.덩이들.reduce((n, d) => n + d.문단들.length, 0);
|
|
240
|
+
const 구획 = r.덩이들.length > 1 ? ` · ${r.갈래 === 'pptx' ? `${r.덩이들.length}장` : `구획 ${r.덩이들.length}개`}` : '';
|
|
241
|
+
return `${r.갈래}${구획} · ${문단수}문단`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** 고치려 들 때 하는 말. 엑셀과 같은 꼴 — 왜 안 되는지와 그럼 어떻게 하는지. */
|
|
245
|
+
export function 문서는못고침(보인이름) {
|
|
246
|
+
const 갈 = 종류(보인이름) ?? '문서';
|
|
247
|
+
return `${갈} 문서는 이 도구로 고칠 수 없습니다: ${보인이름}\n`
|
|
248
|
+
+ ' 읽기만 됩니다 (글로 바꿔서 보여줍니다). 서식·그림·양식이 든 문서를\n'
|
|
249
|
+
+ ' 글로 왕복시키면 반드시 뭔가 잃기 때문입니다.\n'
|
|
250
|
+
+ ` 내용을 바꿔야 한다면 ${basename(String(보인이름))} 은 그대로 두고, 바뀐 내용을\n`
|
|
251
|
+
+ ' 글 파일(.md 등)로 따로 만들어 사용자에게 건네세요.';
|
|
252
|
+
}
|