@milcho0604/velog-mcp 0.4.1 → 0.4.2
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.ko.md +3 -3
- package/README.md +3 -3
- package/dist/auth.js +30 -2
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +54 -2
- package/dist/client.js +145 -11
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/me.d.ts +2 -2
- package/dist/me.js +7 -4
- package/dist/me.js.map +1 -1
- package/dist/render/index.js +21 -11
- package/dist/render/index.js.map +1 -1
- package/dist/serial.d.ts +60 -0
- package/dist/serial.js +68 -0
- package/dist/serial.js.map +1 -0
- package/dist/tools/drafts.js +11 -8
- package/dist/tools/drafts.js.map +1 -1
- package/dist/tools/export.js +66 -5
- package/dist/tools/export.js.map +1 -1
- package/dist/tools/images.js +49 -13
- package/dist/tools/images.js.map +1 -1
- package/dist/tools/profile-edit.js +26 -17
- package/dist/tools/profile-edit.js.map +1 -1
- package/dist/tools/publish.js +49 -16
- package/dist/tools/publish.js.map +1 -1
- package/dist/tools/stats.d.ts +3 -1
- package/dist/tools/stats.js +8 -5
- package/dist/tools/stats.js.map +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/dist/render/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { mkdtemp, readdir, rm, stat, writeFile } from 'node:fs/promises';
|
|
|
15
15
|
import { tmpdir } from 'node:os';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { pathToFileURL } from 'node:url';
|
|
18
|
+
import { makeSerializer } from "../serial.js";
|
|
18
19
|
import { dumpDom, screenshot } from "./chrome.js";
|
|
19
20
|
import { MAX_AREA, MAX_DIM, buildDiagramHtml, parseAudit, } from "./page.js";
|
|
20
21
|
import { buildCoverHtml } from "./cover.js";
|
|
@@ -26,16 +27,13 @@ import { buildCoverHtml } from "./cover.js";
|
|
|
26
27
|
* 모델이 그림 다섯 장을 한 번에 요청하면 크롬 45개·6GB 가 된다.
|
|
27
28
|
* 사용자 기기에서 도는 물건이 그러면 안 된다 — 다른 작업까지 같이 죽는다.
|
|
28
29
|
*
|
|
29
|
-
* 그림 한 장은 4초면 끝나므로 줄 세워도 체감 손해가 거의
|
|
30
|
-
*
|
|
30
|
+
* 그림 한 장은 4초면 끝나므로 줄 세워도 체감 손해가 거의 없다
|
|
31
|
+
* (실측 2026-08-07: 3.9초/장 → 60초에 15장). 얻는 것이 훨씬 크다.
|
|
32
|
+
*
|
|
33
|
+
* ★ 구현은 src/serial.ts 로 옮겼다. 업로드도 같은 이유로 줄이 필요해졌는데,
|
|
34
|
+
* 같은 코드를 두 벌 두면 한쪽만 고치게 된다.
|
|
31
35
|
*/
|
|
32
|
-
|
|
33
|
-
function serialize(task) {
|
|
34
|
-
// 앞 작업이 실패해도 줄은 계속 이어져야 한다.
|
|
35
|
-
const next = renderQueue.then(task, task);
|
|
36
|
-
renderQueue = next.then(() => undefined, () => undefined);
|
|
37
|
-
return next;
|
|
38
|
-
}
|
|
36
|
+
const serialize = makeSerializer();
|
|
39
37
|
const RENDER_PREFIX = 'velog-mcp-render-';
|
|
40
38
|
const PROFILE_PREFIX = 'velog-mcp-chrome-';
|
|
41
39
|
const KEEP_MS = 24 * 60 * 60 * 1000;
|
|
@@ -70,8 +68,16 @@ async function render(args, parse) {
|
|
|
70
68
|
// 프로필만 finally 에서 지운다. 렌더 결과(dir)는 남겨야 하므로 밖에 둘 이유가 없다.
|
|
71
69
|
let profileA = '';
|
|
72
70
|
let profileB = '';
|
|
71
|
+
// ★ 결과 디렉터리는 **성공했을 때만** 남긴다. 예전엔 실패해도 남겨서,
|
|
72
|
+
// 감사 파싱이나 스크린샷이 반복 실패하면 HTML 과 쓰다 만 PNG 가 든 폴더가
|
|
73
|
+
// 매번 하나씩 쌓였다. 청소(sweepOld)는 다음 렌더가 돌 때만, 그것도 24시간이
|
|
74
|
+
// 지난 것만 거둬가므로 다시 안 그리면 영영 남는다.
|
|
75
|
+
// 실패한 산출물은 사용자가 열어볼 이유도 없다 — 경로를 돌려주지 못하니까.
|
|
76
|
+
let resultDir = '';
|
|
77
|
+
let succeeded = false;
|
|
73
78
|
try {
|
|
74
79
|
const dir = await mkdtemp(join(tmpdir(), RENDER_PREFIX));
|
|
80
|
+
resultDir = dir;
|
|
75
81
|
// 크롬 두 번에 **서로 다른 프로필**을 준다. 산출물이 나오면 SIGKILL 하고 실제
|
|
76
82
|
// 종료를 기다리지 않으므로, 같은 프로필을 재사용하면 앞선 크롬이 잠금을 놓기
|
|
77
83
|
// 전에 다음 크롬이 같은 폴더를 열게 된다.
|
|
@@ -100,6 +106,7 @@ async function render(args, parse) {
|
|
|
100
106
|
throw new Error('크롬이 PNG 를 만들지 못했습니다. 화면 크기가 비정상이거나(0 이하) ' +
|
|
101
107
|
'크롬 실행이 중간에 끊겼을 수 있습니다.');
|
|
102
108
|
}
|
|
109
|
+
succeeded = true;
|
|
103
110
|
return {
|
|
104
111
|
pngPath,
|
|
105
112
|
htmlPath,
|
|
@@ -111,12 +118,15 @@ async function render(args, parse) {
|
|
|
111
118
|
};
|
|
112
119
|
}
|
|
113
120
|
finally {
|
|
114
|
-
// 크롬 프로필은 임시 산출물이라 항상 지운다. 렌더 결과(dir)는
|
|
115
|
-
// 사용자가 HTML 을 열어 손보거나 PNG 를 다시 쓸 수 있어야 한다.
|
|
121
|
+
// 크롬 프로필은 임시 산출물이라 항상 지운다. 렌더 결과(dir)는 **성공했을
|
|
122
|
+
// 때만** 남긴다 — 사용자가 HTML 을 열어 손보거나 PNG 를 다시 쓸 수 있어야 한다.
|
|
116
123
|
for (const profile of [profileA, profileB]) {
|
|
117
124
|
if (profile)
|
|
118
125
|
await rm(profile, { recursive: true, force: true }).catch(() => { });
|
|
119
126
|
}
|
|
127
|
+
if (!succeeded && resultDir) {
|
|
128
|
+
await rm(resultDir, { recursive: true, force: true }).catch(() => { });
|
|
129
|
+
}
|
|
120
130
|
}
|
|
121
131
|
}
|
|
122
132
|
export function renderDiagram(spec, scale = 2) {
|
package/dist/render/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/render/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAGN,QAAQ,EACR,OAAO,EACP,gBAAgB,EAChB,UAAU,GACV,MAAM,WAAW,CAAC;AACnB,OAAO,EAAmC,cAAc,EAAE,MAAM,YAAY,CAAC;AAoB7E
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/render/index.ts"],"names":[],"mappings":"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"}
|
package/dist/serial.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 줄 세우기.
|
|
3
|
+
*
|
|
4
|
+
* MCP 클라이언트는 도구를 **병렬로** 부른다. 이 저장소에는 그게 문제가 되는
|
|
5
|
+
* 자리가 두 종류 있다.
|
|
6
|
+
*
|
|
7
|
+
* ① 자원을 통째로 먹는 작업 — 렌더는 이미 이 방식으로 크롬 메모리를 1GB 에
|
|
8
|
+
* 묶어뒀다(render/index.ts 주석). 업로드도 같은 성격이다: 파일당 10MB 를
|
|
9
|
+
* 최대 60초간 들고 있는데 동시성 상한이 없으면 그대로 곱해진다.
|
|
10
|
+
*
|
|
11
|
+
* ② **읽고-합쳐-쓰는** 작업 — `velog_update_post` 는 기존 글을 읽어 생략 필드를
|
|
12
|
+
* 채운 뒤 전체를 교체한다. 벨로그에는 버전도 ETag 도 없다. 같은 글에
|
|
13
|
+
* '제목=A' 와 '본문=B' 가 동시에 오면 둘 다 옛 값을 읽고, 나중에 쓴 쪽이
|
|
14
|
+
* 앞선 변경을 지운다. 그런데 **둘 다 성공을 보고한다** — 사후 검증도 각자
|
|
15
|
+
* 자기가 보낸 값과 맞춰보므로 통과한다. 사용자는 제목이 사라진 걸 모른다.
|
|
16
|
+
*
|
|
17
|
+
* ②는 같은 대상(글 id·프로필)끼리만 줄을 세우면 되므로 키를 받는다.
|
|
18
|
+
* 다른 글끼리는 그대로 병렬로 돈다.
|
|
19
|
+
*
|
|
20
|
+
* ⚠️ 이건 **한 프로세스 안**에서만 유효하다. 사용자가 벨로그 웹에서 동시에
|
|
21
|
+
* 고치는 것은 막을 수 없다 — 그건 벨로그가 버전을 주지 않는 한 방법이 없다.
|
|
22
|
+
* rate limiter 와 같은 한계이고, 같은 이유로 여기에 적어둔다.
|
|
23
|
+
*/
|
|
24
|
+
type Task<T> = () => Promise<T>;
|
|
25
|
+
/** 앞 작업이 끝나야 다음이 시작한다. 앞이 실패해도 줄은 이어진다. */
|
|
26
|
+
export declare function makeSerializer(): <T>(task: Task<T>) => Promise<T>;
|
|
27
|
+
/**
|
|
28
|
+
* 키가 같은 작업끼리만 줄을 세운다.
|
|
29
|
+
*
|
|
30
|
+
* ★ 다 쓴 키는 지운다. 안 지우면 글을 고칠 때마다 항목이 하나씩 쌓여
|
|
31
|
+
* 오래 켜둔 서버에서 계속 자란다. 대기자 수를 세다가 0 이 되면 버린다.
|
|
32
|
+
*/
|
|
33
|
+
export interface KeyedSerializer {
|
|
34
|
+
<T>(key: string, task: Task<T>): Promise<T>;
|
|
35
|
+
/**
|
|
36
|
+
* 지금 살아 있는 줄 수. **테스트 전용.**
|
|
37
|
+
* 정리가 되는지 밖에서 볼 방법이 없으면 그 테스트는 거짓 초록이 된다 —
|
|
38
|
+
* 실제로 `lanes.delete()` 를 지워도 통과하는 테스트를 썼다가 코덱스에 잡혔다.
|
|
39
|
+
*/
|
|
40
|
+
laneCount(): number;
|
|
41
|
+
}
|
|
42
|
+
export declare function makeKeyedSerializer(): KeyedSerializer;
|
|
43
|
+
/**
|
|
44
|
+
* ★★ 쓰기 줄은 **저장소 전체에 하나뿐이어야 한다.**
|
|
45
|
+
*
|
|
46
|
+
* 처음엔 모듈마다 `makeKeyedSerializer()` 를 따로 만들었다. 그러면 키가 같아도
|
|
47
|
+
* 줄이 다르다 — `velog_update_draft` 와 `velog_publish_draft` 를 같은 글에 동시에
|
|
48
|
+
* 부르면 서로를 못 본다. 코덱스 교차검증에서 실제로 재현됐다: 같은 글의 사전
|
|
49
|
+
* 조회가 겹쳤고(2), 발행 mutation 뒤에 초안 수정이 덮어써 최종 상태가 다시
|
|
50
|
+
* 초안이 됐다. 도구 이름이 달라도 **대상이 같으면 같은 줄**이어야 한다.
|
|
51
|
+
*
|
|
52
|
+
* 키 규약: 글은 `post:<id>`, 새 글은 `post:new`, 프로필은 `profile`.
|
|
53
|
+
*/
|
|
54
|
+
export declare const serializeWrite: KeyedSerializer;
|
|
55
|
+
/** 테스트에서만 쓴다 — 줄이 실제로 비워지는지 본다. */
|
|
56
|
+
export declare const __testing: {
|
|
57
|
+
makeKeyedSerializer: typeof makeKeyedSerializer;
|
|
58
|
+
makeSerializer: typeof makeSerializer;
|
|
59
|
+
};
|
|
60
|
+
export {};
|
package/dist/serial.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 줄 세우기.
|
|
3
|
+
*
|
|
4
|
+
* MCP 클라이언트는 도구를 **병렬로** 부른다. 이 저장소에는 그게 문제가 되는
|
|
5
|
+
* 자리가 두 종류 있다.
|
|
6
|
+
*
|
|
7
|
+
* ① 자원을 통째로 먹는 작업 — 렌더는 이미 이 방식으로 크롬 메모리를 1GB 에
|
|
8
|
+
* 묶어뒀다(render/index.ts 주석). 업로드도 같은 성격이다: 파일당 10MB 를
|
|
9
|
+
* 최대 60초간 들고 있는데 동시성 상한이 없으면 그대로 곱해진다.
|
|
10
|
+
*
|
|
11
|
+
* ② **읽고-합쳐-쓰는** 작업 — `velog_update_post` 는 기존 글을 읽어 생략 필드를
|
|
12
|
+
* 채운 뒤 전체를 교체한다. 벨로그에는 버전도 ETag 도 없다. 같은 글에
|
|
13
|
+
* '제목=A' 와 '본문=B' 가 동시에 오면 둘 다 옛 값을 읽고, 나중에 쓴 쪽이
|
|
14
|
+
* 앞선 변경을 지운다. 그런데 **둘 다 성공을 보고한다** — 사후 검증도 각자
|
|
15
|
+
* 자기가 보낸 값과 맞춰보므로 통과한다. 사용자는 제목이 사라진 걸 모른다.
|
|
16
|
+
*
|
|
17
|
+
* ②는 같은 대상(글 id·프로필)끼리만 줄을 세우면 되므로 키를 받는다.
|
|
18
|
+
* 다른 글끼리는 그대로 병렬로 돈다.
|
|
19
|
+
*
|
|
20
|
+
* ⚠️ 이건 **한 프로세스 안**에서만 유효하다. 사용자가 벨로그 웹에서 동시에
|
|
21
|
+
* 고치는 것은 막을 수 없다 — 그건 벨로그가 버전을 주지 않는 한 방법이 없다.
|
|
22
|
+
* rate limiter 와 같은 한계이고, 같은 이유로 여기에 적어둔다.
|
|
23
|
+
*/
|
|
24
|
+
/** 앞 작업이 끝나야 다음이 시작한다. 앞이 실패해도 줄은 이어진다. */
|
|
25
|
+
export function makeSerializer() {
|
|
26
|
+
let queue = Promise.resolve();
|
|
27
|
+
return (task) => {
|
|
28
|
+
// 성공·실패 둘 다에 task 를 걸어 앞 작업의 실패가 줄을 끊지 않게 한다.
|
|
29
|
+
const next = queue.then(task, task);
|
|
30
|
+
queue = next.then(() => undefined, () => undefined);
|
|
31
|
+
return next;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function makeKeyedSerializer() {
|
|
35
|
+
const lanes = new Map();
|
|
36
|
+
const serialize = (key, task) => {
|
|
37
|
+
const lane = lanes.get(key) ?? { tail: Promise.resolve(), waiting: 0 };
|
|
38
|
+
lanes.set(key, lane);
|
|
39
|
+
lane.waiting++;
|
|
40
|
+
const next = lane.tail.then(task, task);
|
|
41
|
+
lane.tail = next.then(() => undefined, () => undefined);
|
|
42
|
+
// ★ 정리는 `next` 가 아니라 `lane.tail` 에 건다. next 는 호출자에게 그대로
|
|
43
|
+
// 돌려주므로, 여기에 .then 을 걸면 호출자가 잡지 않은 거절이 하나 더 생긴다.
|
|
44
|
+
void lane.tail.then(() => {
|
|
45
|
+
lane.waiting--;
|
|
46
|
+
if (lane.waiting === 0 && lanes.get(key) === lane)
|
|
47
|
+
lanes.delete(key);
|
|
48
|
+
});
|
|
49
|
+
return next;
|
|
50
|
+
};
|
|
51
|
+
serialize.laneCount = () => lanes.size;
|
|
52
|
+
return serialize;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* ★★ 쓰기 줄은 **저장소 전체에 하나뿐이어야 한다.**
|
|
56
|
+
*
|
|
57
|
+
* 처음엔 모듈마다 `makeKeyedSerializer()` 를 따로 만들었다. 그러면 키가 같아도
|
|
58
|
+
* 줄이 다르다 — `velog_update_draft` 와 `velog_publish_draft` 를 같은 글에 동시에
|
|
59
|
+
* 부르면 서로를 못 본다. 코덱스 교차검증에서 실제로 재현됐다: 같은 글의 사전
|
|
60
|
+
* 조회가 겹쳤고(2), 발행 mutation 뒤에 초안 수정이 덮어써 최종 상태가 다시
|
|
61
|
+
* 초안이 됐다. 도구 이름이 달라도 **대상이 같으면 같은 줄**이어야 한다.
|
|
62
|
+
*
|
|
63
|
+
* 키 규약: 글은 `post:<id>`, 새 글은 `post:new`, 프로필은 `profile`.
|
|
64
|
+
*/
|
|
65
|
+
export const serializeWrite = makeKeyedSerializer();
|
|
66
|
+
/** 테스트에서만 쓴다 — 줄이 실제로 비워지는지 본다. */
|
|
67
|
+
export const __testing = { makeKeyedSerializer, makeSerializer };
|
|
68
|
+
//# sourceMappingURL=serial.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serial.js","sourceRoot":"","sources":["../src/serial.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,2CAA2C;AAC3C,MAAM,UAAU,cAAc;IAC7B,IAAI,KAAK,GAAqB,OAAO,CAAC,OAAO,EAAE,CAAC;IAChD,OAAO,CAAI,IAAa,EAAc,EAAE;QACvC,8CAA8C;QAC9C,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACpC,KAAK,GAAG,IAAI,CAAC,IAAI,CAChB,GAAG,EAAE,CAAC,SAAS,EACf,GAAG,EAAE,CAAC,SAAS,CACf,CAAC;QACF,OAAO,IAAI,CAAC;IACb,CAAC,CAAC;AACH,CAAC;AAkBD,MAAM,UAAU,mBAAmB;IAClC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAuD,CAAC;IAE7E,MAAM,SAAS,GAAG,CAAI,GAAW,EAAE,IAAa,EAAc,EAAE;QAC/D,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;QACvE,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACrB,IAAI,CAAC,OAAO,EAAE,CAAC;QAEf,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACxC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CACpB,GAAG,EAAE,CAAC,SAAS,EACf,GAAG,EAAE,CAAC,SAAS,CACf,CAAC;QACF,wDAAwD;QACxD,mDAAmD;QACnD,KAAK,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE;YACxB,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,IAAI,IAAI,CAAC,OAAO,KAAK,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,IAAI;gBAAE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACtE,CAAC,CAAC,CAAC;QACH,OAAO,IAAI,CAAC;IACb,CAAC,CAAC;IACF,SAAS,CAAC,SAAS,GAAG,GAAW,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC;IAC/C,OAAO,SAAS,CAAC;AAClB,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,mBAAmB,EAAE,CAAC;AAEpD,mCAAmC;AACnC,MAAM,CAAC,MAAM,SAAS,GAAG,EAAE,mBAAmB,EAAE,cAAc,EAAE,CAAC"}
|
package/dist/tools/drafts.js
CHANGED
|
@@ -15,6 +15,7 @@ import { toUrlSlug, isSafeImageUrl } from "../slug.js";
|
|
|
15
15
|
import { resolveMyUsername } from "../me.js";
|
|
16
16
|
import { assertOwned, assertOwnsSeries } from "../ownership.js";
|
|
17
17
|
import { READ_ONLY } from "./posts.js";
|
|
18
|
+
import { serializeWrite } from "../serial.js";
|
|
18
19
|
/**
|
|
19
20
|
* ★ 발행 차단 지점. 이 값은 파라미터가 아니라 상수다.
|
|
20
21
|
* true = 임시저장(남에게 보이지 않음), false = 발행(되돌릴 수 없음).
|
|
@@ -83,7 +84,9 @@ export function registerDraftTools(server, client) {
|
|
|
83
84
|
},
|
|
84
85
|
// 되돌릴 수 있는 쓰기다 — 비공개 초안이므로 파괴적이지 않다.
|
|
85
86
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
86
|
-
}, async ({ title, body, tags, url_slug, thumbnail, series_id }) =>
|
|
87
|
+
}, async ({ title, body, tags, url_slug, thumbnail, series_id }, extra) =>
|
|
88
|
+
// ★ 같은 대상에 대한 쓰기는 줄을 세운다 — 이유는 src/serial.ts
|
|
89
|
+
serializeWrite('post:new', async () => {
|
|
87
90
|
client.requireAuth('velog_create_draft');
|
|
88
91
|
if (series_id)
|
|
89
92
|
await assertOwnsSeries(client, series_id, 'velog_create_draft');
|
|
@@ -102,7 +105,7 @@ export function registerDraftTools(server, client) {
|
|
|
102
105
|
if (series_id)
|
|
103
106
|
input['series_id'] = series_id;
|
|
104
107
|
// mutate 는 재시도하지 않는다 — 응답 유실 시 초안이 중복 생성된다.
|
|
105
|
-
const data = await client.mutate(MUTATION_WRITE_POST, { input });
|
|
108
|
+
const data = await client.mutate(MUTATION_WRITE_POST, { input }, { signal: extra.signal });
|
|
106
109
|
assertStayedDraft(data.writePost);
|
|
107
110
|
// ★ 벨로그는 임시저장 생성 시 series_id 를 조용히 버린다:
|
|
108
111
|
// // apps/server/src/services/PostApiService/index.mts
|
|
@@ -115,7 +118,7 @@ export function registerDraftTools(server, client) {
|
|
|
115
118
|
' 한 번 호출하면 그때 실제로 붙습니다.'
|
|
116
119
|
: '';
|
|
117
120
|
return textResult(draftResult(data.writePost, '저장') + seriesNote);
|
|
118
|
-
});
|
|
121
|
+
}));
|
|
119
122
|
server.registerTool('velog_update_draft', {
|
|
120
123
|
title: '벨로그 초안 수정',
|
|
121
124
|
description: '기존 초안을 **통째로 교체**한다. 부분 수정이 아니다. ' +
|
|
@@ -139,7 +142,9 @@ export function registerDraftTools(server, client) {
|
|
|
139
142
|
// ★ destructive 가 맞다. 생략 필드가 보존되지 않고 초기화된다 —
|
|
140
143
|
// MCP 명세상 destructiveHint:false 는 '추가만 한다'는 뜻이라 거짓이 된다.
|
|
141
144
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
142
|
-
}, async ({ id, title, body, tags, url_slug, thumbnail, series_id }) =>
|
|
145
|
+
}, async ({ id, title, body, tags, url_slug, thumbnail, series_id }, extra) =>
|
|
146
|
+
// ★ 같은 대상에 대한 쓰기는 줄을 세운다 — 이유는 src/serial.ts
|
|
147
|
+
serializeWrite(`post:${id}`, async () => {
|
|
143
148
|
client.requireAuth('velog_update_draft');
|
|
144
149
|
// ★ 두 가지를 확인한 뒤에야 수정한다.
|
|
145
150
|
// ① 내 글인가 — 벨로그 서버가 edit 에서 소유권을 안 본다(ownership.ts)
|
|
@@ -172,9 +177,7 @@ export function registerDraftTools(server, client) {
|
|
|
172
177
|
input['thumbnail'] = thumbnail;
|
|
173
178
|
if (series_id)
|
|
174
179
|
input['series_id'] = series_id;
|
|
175
|
-
const data = await client.mutate(MUTATION_EDIT_POST, {
|
|
176
|
-
input,
|
|
177
|
-
});
|
|
180
|
+
const data = await client.mutate(MUTATION_EDIT_POST, { input }, { signal: extra.signal });
|
|
178
181
|
// ★ editPost 의 응답으로는 사후 상태를 알 수 없다. 공식 구현이
|
|
179
182
|
// `return { ...post, url_slug: data.url_slug }` 로 **갱신 전에 읽은**
|
|
180
183
|
// post 를 돌려주고 url_slug 만 덮기 때문이다
|
|
@@ -196,7 +199,7 @@ export function registerDraftTools(server, client) {
|
|
|
196
199
|
// 그걸로 문구를 만들면 제목을 바꿔도 옛 제목이 표시된다.
|
|
197
200
|
// 이미 재조회했으므로 그 결과를 얹는다.
|
|
198
201
|
return textResult(draftResult({ ...data.editPost, title: after.post.title ?? data.editPost.title ?? null }, '수정'));
|
|
199
|
-
});
|
|
202
|
+
}));
|
|
200
203
|
server.registerTool('velog_list_drafts', {
|
|
201
204
|
title: '내 초안 목록',
|
|
202
205
|
description: '내 임시저장 글 목록. 초안을 이어 쓰거나 수정하기 전에 id 를 여기서 확인한다.',
|
package/dist/tools/drafts.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drafts.js","sourceRoot":"","sources":["../../src/tools/drafts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEhE,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"drafts.js","sourceRoot":"","sources":["../../src/tools/drafts.ts"],"names":[],"mappings":"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"}
|
package/dist/tools/export.js
CHANGED
|
@@ -15,6 +15,31 @@ import { slugify } from "../slug.js";
|
|
|
15
15
|
import { fetchAllPosts } from "./stats.js";
|
|
16
16
|
import { READ_ONLY } from "./posts.js";
|
|
17
17
|
import { resolveMyUsername } from "../me.js";
|
|
18
|
+
/** 벨로그 커넥션 풀(limit 5)을 배려하는 요청 간격. */
|
|
19
|
+
const GAP_MS = 250;
|
|
20
|
+
/**
|
|
21
|
+
* 한 호출에 쓸 수 있는 시간.
|
|
22
|
+
*
|
|
23
|
+
* ★ 왜 필요한가 — MCP SDK 의 클라이언트측 기본 요청 타임아웃은 60초다
|
|
24
|
+
* (shared/protocol.js: DEFAULT_REQUEST_TIMEOUT_MSEC = 60000).
|
|
25
|
+
* 그런데 이 도구는 글 한 편마다 상세를 따로 받는다. 실측(2026-08-07, 벨로그
|
|
26
|
+
* 공개 글 3편): 본문 왕복 평균 1,313ms. 간격 250ms 를 더하면 편당 약 1.5초다.
|
|
27
|
+
* **기본값 limit=50 이면 약 78초** — 60초를 넘는다. 즉 기본 인자로 부르면
|
|
28
|
+
* 클라이언트가 먼저 포기했고, 그런데도 루프는 계속 돌며 파일을 썼다.
|
|
29
|
+
* 화면에는 '실패'가 뜨는데 디스크에는 파일이 생기는 상태였다.
|
|
30
|
+
*
|
|
31
|
+
* 그래서 60초 안쪽에서 **우리가 먼저 멈추고 무엇을 했는지 보고한다.**
|
|
32
|
+
* 파일 이름이 글 순서로 정해지므로 같은 out_dir 로 다시 부르면 그대로 이어진다.
|
|
33
|
+
*/
|
|
34
|
+
const TIME_BUDGET_MS = 50_000;
|
|
35
|
+
/**
|
|
36
|
+
* 한 편을 더 시작하기 전에 남겨둬야 할 여유.
|
|
37
|
+
*
|
|
38
|
+
* 데드라인 직전에 루프로 들어가면 간격 250ms 에 상세 요청 한 번이 통째로 붙는다.
|
|
39
|
+
* 실측 왕복이 1.3초대이므로 3초면 정상 한 편은 넉넉히 들어가고, 안 들어갈 만하면
|
|
40
|
+
* 아예 시작하지 않아 예산을 지킨다.
|
|
41
|
+
*/
|
|
42
|
+
const PER_POST_RESERVE_MS = 3_000;
|
|
18
43
|
/** YAML 문자열 값. 따옴표·백슬래시를 이스케이프해 프론트매터가 깨지지 않게 한다. */
|
|
19
44
|
function yamlString(value) {
|
|
20
45
|
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
@@ -80,28 +105,48 @@ export function registerExportTools(server, client) {
|
|
|
80
105
|
// 벨로그 쪽 상태는 안 바뀌지만 로컬 파일은 '덮어쓴다'. MCP 명세상
|
|
81
106
|
// destructiveHint:false 는 '추가만 한다'는 뜻이라 거짓이 된다.
|
|
82
107
|
annotations: { ...READ_ONLY, readOnlyHint: false, destructiveHint: true },
|
|
83
|
-
}, async ({ username, out_dir, limit }) => {
|
|
84
|
-
|
|
108
|
+
}, async ({ username, out_dir, limit }, extra) => {
|
|
109
|
+
// ★ 예산 시계는 **핸들러가 시작하자마자** 켠다. 예전엔 목록 조회가 끝난
|
|
110
|
+
// 뒤에 켰는데, 그 앞의 계정 조회·목록 4페이지가 이미 몇 초를 먹는다.
|
|
111
|
+
// '한 호출에 쓸 수 있는 시간'을 재는데 그 앞을 빼면 예산이 아니다.
|
|
112
|
+
const deadline = Date.now() + TIME_BUDGET_MS;
|
|
113
|
+
const target = username ?? (await resolveMyUsername(client, extra.signal));
|
|
85
114
|
const dir = resolve(out_dir);
|
|
86
115
|
await mkdir(dir, { recursive: true });
|
|
87
116
|
const maxPages = Math.ceil(limit / 50);
|
|
88
|
-
const { posts } = await fetchAllPosts(client, target, maxPages);
|
|
117
|
+
const { posts } = await fetchAllPosts(client, target, maxPages, extra.signal);
|
|
89
118
|
const targets = posts.slice(0, limit);
|
|
90
119
|
if (targets.length === 0)
|
|
91
120
|
return textResult(`@${target} 의 공개 글이 없습니다.`);
|
|
92
121
|
const written = [];
|
|
93
122
|
const failed = [];
|
|
123
|
+
/** 다 못 돈 이유. 없으면 끝까지 돈 것이다. */
|
|
124
|
+
let stoppedBy = null;
|
|
94
125
|
for (const [index, summary] of targets.entries()) {
|
|
126
|
+
// ★ 클라이언트가 취소했으면 즉시 멈춘다. 여기서 안 보면 사용자는 이미
|
|
127
|
+
// 포기했는데 파일은 계속 쌓인다 — 화면과 디스크가 어긋난다.
|
|
128
|
+
if (extra.signal.aborted) {
|
|
129
|
+
stoppedBy = '취소';
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
// ★ 시간 예산. 이유는 TIME_BUDGET_MS 주석 참고.
|
|
133
|
+
// ★ 남은 시간이 '한 편 분량'도 안 되면 시작하지 않는다. 데드라인
|
|
134
|
+
// 직전에 들어가면 간격 250ms + 상세 요청(최대 재시도 예산)이
|
|
135
|
+
// 그대로 붙어 예산을 훌쩍 넘긴다.
|
|
136
|
+
if (Date.now() + PER_POST_RESERVE_MS >= deadline) {
|
|
137
|
+
stoppedBy = '시간';
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
95
140
|
// 벨로그 커넥션 풀(limit 5)을 배려한다. 몰아치면 상대가 먼저 죽는다.
|
|
96
141
|
if (index > 0)
|
|
97
|
-
await new Promise((r) => setTimeout(r,
|
|
142
|
+
await new Promise((r) => setTimeout(r, GAP_MS));
|
|
98
143
|
try {
|
|
99
144
|
// 목록에는 body 가 없으므로 한 편씩 상세를 받는다.
|
|
100
145
|
const data = await client.request(QUERY_POST,
|
|
101
146
|
// url_slug 가 없으면 id 로 조회한다.
|
|
102
147
|
summary.url_slug
|
|
103
148
|
? { input: { username: target, url_slug: summary.url_slug } }
|
|
104
|
-
: { input: { id: summary.id } });
|
|
149
|
+
: { input: { id: summary.id } }, { signal: extra.signal });
|
|
105
150
|
if (!data.post) {
|
|
106
151
|
failed.push(`${summary.title} (본문 조회 실패)`);
|
|
107
152
|
continue;
|
|
@@ -111,16 +156,32 @@ export function registerExportTools(server, client) {
|
|
|
111
156
|
written.push(name);
|
|
112
157
|
}
|
|
113
158
|
catch (error) {
|
|
159
|
+
if (extra.signal.aborted) {
|
|
160
|
+
stoppedBy = '취소';
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
114
163
|
const reason = error instanceof Error ? error.message : String(error);
|
|
115
164
|
failed.push(`${summary.title} — ${reason}`);
|
|
116
165
|
}
|
|
117
166
|
}
|
|
167
|
+
const done = written.length + failed.length;
|
|
118
168
|
const lines = [
|
|
119
169
|
`✅ ${written.length}편을 저장했습니다.`,
|
|
120
170
|
'',
|
|
121
171
|
`- 위치: ${dir}`,
|
|
122
172
|
`- 형식: YAML 프론트매터 + 마크다운 본문`,
|
|
123
173
|
];
|
|
174
|
+
if (stoppedBy !== null) {
|
|
175
|
+
const left = targets.length - done;
|
|
176
|
+
lines.push('', stoppedBy === '취소'
|
|
177
|
+
? `⏹️ 요청이 취소되어 ${done}/${targets.length}편에서 멈췄습니다.`
|
|
178
|
+
: `⏱️ ${TIME_BUDGET_MS / 1000}초 예산에 도달해 ${done}/${targets.length}편에서 멈췄습니다.`, done === 0
|
|
179
|
+
? '준비 조회(계정·글 목록)만으로 예산을 다 썼습니다. 벨로그가 느린 ' +
|
|
180
|
+
'상태일 수 있으니 잠시 뒤 다시 시도하세요 — limit 을 줄여도 소용없습니다.'
|
|
181
|
+
: `남은 ${left}편은 저장되지 않았습니다. **같은 out_dir 로 다시 부르면** ` +
|
|
182
|
+
'이미 받은 파일을 덮어쓰며 이어집니다(파일 이름이 글 순서로 정해집니다). ' +
|
|
183
|
+
'한 번에 끝내려면 limit 을 줄여 나눠 부르세요.');
|
|
184
|
+
}
|
|
124
185
|
if (failed.length > 0) {
|
|
125
186
|
lines.push('', `⚠️ ${failed.length}편 실패:`, ...failed.slice(0, 10).map((f) => ` - ${f}`));
|
|
126
187
|
if (failed.length > 10)
|
package/dist/tools/export.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"export.js","sourceRoot":"","sources":["../../src/tools/export.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"export.js","sourceRoot":"","sources":["../../src/tools/export.ts"],"names":[],"mappings":"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"}
|
package/dist/tools/images.js
CHANGED
|
@@ -25,6 +25,7 @@ import { z } from 'zod';
|
|
|
25
25
|
import { textResult } from "../format.js";
|
|
26
26
|
import { ICON_NAMES, TONE_NAMES, formatAudit, formatCoverAudit, renderCover, renderDiagram, } from "../render/index.js";
|
|
27
27
|
import { isHexColor } from "../render/tones.js";
|
|
28
|
+
import { makeSerializer } from "../serial.js";
|
|
28
29
|
/** 우리가 올리는 것 — 서버 상한(30MB)보다 낮게 잡는다. */
|
|
29
30
|
const MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
|
|
30
31
|
/**
|
|
@@ -321,6 +322,17 @@ const planeSchema = z.object({
|
|
|
321
322
|
function markdown(alt, url) {
|
|
322
323
|
return ``;
|
|
323
324
|
}
|
|
325
|
+
/**
|
|
326
|
+
* 업로드도 줄을 세운다.
|
|
327
|
+
*
|
|
328
|
+
* ★ 파일당 10MB 상한은 있는데 **동시성 상한이 없었다.** 한 요청이 10MB 버퍼와
|
|
329
|
+
* 그 multipart 사본을 최대 60초 들고 있으므로, 병렬 호출 수만큼 그대로 곱해진다.
|
|
330
|
+
* 렌더는 같은 이유로 이미 줄을 세워 크롬 메모리를 1GB 에 묶어뒀는데
|
|
331
|
+
* 업로드만 빠져 있었다.
|
|
332
|
+
* ★ 벨로그 쪽 사정도 같은 방향이다 — 1분 20건을 넘기면 계정을 막는다
|
|
333
|
+
* (ImageService.detectAbuse). 몰아치지 않는 편이 안전하다.
|
|
334
|
+
*/
|
|
335
|
+
const serializeUpload = makeSerializer();
|
|
324
336
|
export function registerImageTools(server, client) {
|
|
325
337
|
/** 렌더 결과를 올리고 결과 문구를 만든다. 감사에 걸린 게 있으면 올리지 않는다. */
|
|
326
338
|
async function finish(args) {
|
|
@@ -360,11 +372,19 @@ export function registerImageTools(server, client) {
|
|
|
360
372
|
return lines;
|
|
361
373
|
}
|
|
362
374
|
if (args.upload) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
375
|
+
// ★ 파일 읽기도 **줄 안에서** 한다. 밖에서 읽으면 대기 중인 요청마다
|
|
376
|
+
// 10MB 버퍼가 메모리에 올라간 채 순서를 기다린다 — 줄을 세운 이유
|
|
377
|
+
// (메모리 상한)가 그대로 사라진다. 코덱스 교차검증에서 잡았다.
|
|
378
|
+
const url = await serializeUpload(async () => {
|
|
379
|
+
// ★ 줄에서 기다리는 동안 취소됐을 수 있다. 읽기 전에 확인한다 —
|
|
380
|
+
// 안 그러면 이미 포기한 요청이 파일부터 읽고 버퍼를 잡는다.
|
|
381
|
+
args.signal.throwIfAborted();
|
|
382
|
+
const bytes = new Uint8Array(await readFile(args.pngPath));
|
|
383
|
+
const uploadOptions = args.postId
|
|
384
|
+
? { type: 'post', contentType: 'image/png', refId: args.postId, signal: args.signal }
|
|
385
|
+
: { type: 'post', contentType: 'image/png', signal: args.signal };
|
|
386
|
+
return client.uploadImage(bytes, `${basename(args.pngPath, '.png')}.png`, uploadOptions);
|
|
387
|
+
});
|
|
368
388
|
lines.push('본문에 붙여넣을 마크다운:');
|
|
369
389
|
lines.push('');
|
|
370
390
|
lines.push(markdown(args.alt, url));
|
|
@@ -429,7 +449,7 @@ export function registerImageTools(server, client) {
|
|
|
429
449
|
.describe('붙일 글 id. 주면 벨로그가 내 글인지 확인한 뒤 받는다'),
|
|
430
450
|
},
|
|
431
451
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
432
|
-
}, async (args) => {
|
|
452
|
+
}, async (args, extra) => {
|
|
433
453
|
if (args.upload)
|
|
434
454
|
client.requireAuth('velog_render_diagram');
|
|
435
455
|
const spec = {
|
|
@@ -466,6 +486,7 @@ export function registerImageTools(server, client) {
|
|
|
466
486
|
alt: args.alt ?? args.title,
|
|
467
487
|
...(args.post_id ? { postId: args.post_id } : {}),
|
|
468
488
|
what: '다이어그램',
|
|
489
|
+
signal: extra.signal,
|
|
469
490
|
})).join('\n'));
|
|
470
491
|
});
|
|
471
492
|
server.registerTool('velog_render_cover', {
|
|
@@ -484,7 +505,7 @@ export function registerImageTools(server, client) {
|
|
|
484
505
|
post_id: z.string().max(64).optional(),
|
|
485
506
|
},
|
|
486
507
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
487
|
-
}, async (args) => {
|
|
508
|
+
}, async (args, extra) => {
|
|
488
509
|
if (args.upload)
|
|
489
510
|
client.requireAuth('velog_render_cover');
|
|
490
511
|
const spec = { title: args.title };
|
|
@@ -512,6 +533,7 @@ export function registerImageTools(server, client) {
|
|
|
512
533
|
alt: args.title,
|
|
513
534
|
...(args.post_id ? { postId: args.post_id } : {}),
|
|
514
535
|
what: '표지',
|
|
536
|
+
signal: extra.signal,
|
|
515
537
|
})).join('\n'));
|
|
516
538
|
});
|
|
517
539
|
server.registerTool('velog_upload_image', {
|
|
@@ -530,7 +552,7 @@ export function registerImageTools(server, client) {
|
|
|
530
552
|
post_id: z.string().max(64).optional().describe('붙일 글 id (서버가 소유권을 확인한다)'),
|
|
531
553
|
},
|
|
532
554
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
533
|
-
}, async (args) => {
|
|
555
|
+
}, async (args, extra) => {
|
|
534
556
|
client.requireAuth('velog_upload_image');
|
|
535
557
|
// ★ 문자열 그대로 비교하면 `/tmp/x/./a.png` 하나로 빠져나간다.
|
|
536
558
|
// 경로를 정규화하고, 심볼릭 링크까지 푼 형태로도 본다.
|
|
@@ -538,12 +560,26 @@ export function registerImageTools(server, client) {
|
|
|
538
560
|
throw new Error('이 PNG 는 이 서버의 자가감사에서 떨어진 산출물입니다 — 올릴 수 없습니다.\n' +
|
|
539
561
|
'그림을 고쳐 다시 그리세요 (velog_render_diagram).');
|
|
540
562
|
}
|
|
541
|
-
const { bytes, kind } = await readImageFile(args.path);
|
|
542
563
|
const name = basename(args.path);
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
564
|
+
// ★ 읽기부터 줄 안이다 — 이유는 위 finish() 의 주석과 같다.
|
|
565
|
+
// 대기 중인 요청이 10MB 씩 들고 서 있으면 상한을 둔 의미가 없다.
|
|
566
|
+
const { url, kind, bytes } = await serializeUpload(async () => {
|
|
567
|
+
extra.signal.throwIfAborted();
|
|
568
|
+
// ★ 거부 목록은 줄 밖에서 한 번 봤지만, 기다리는 사이에 이 PNG 가
|
|
569
|
+
// 감사에서 떨어져 목록에 오를 수 있다. 읽기 직전에 다시 본다.
|
|
570
|
+
if (await isRejected(args.path)) {
|
|
571
|
+
throw new Error('이 PNG 는 이 서버의 자가감사에서 떨어진 산출물입니다 — 올릴 수 없습니다.');
|
|
572
|
+
}
|
|
573
|
+
const read = await readImageFile(args.path);
|
|
574
|
+
const uploadOptions = args.post_id
|
|
575
|
+
? { type: args.type, contentType: read.kind.mime, refId: args.post_id, signal: extra.signal }
|
|
576
|
+
: { type: args.type, contentType: read.kind.mime, signal: extra.signal };
|
|
577
|
+
return {
|
|
578
|
+
url: await client.uploadImage(read.bytes, name, uploadOptions),
|
|
579
|
+
kind: read.kind,
|
|
580
|
+
bytes: read.bytes,
|
|
581
|
+
};
|
|
582
|
+
});
|
|
547
583
|
return textResult([
|
|
548
584
|
`✅ 올렸습니다 — ${kind.mime} · ${(bytes.length / 1024).toFixed(0)}KB`,
|
|
549
585
|
'',
|