daiso 1.2.1 → 1.2.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.md +7 -5
- package/dist/api/configStatus.js +20 -0
- package/dist/services/cgv/errors.js +5 -2
- package/dist/services/cgv/transport.js +1 -1
- package/dist/services/cu/client.js +0 -4
- package/dist/services/gs25/productSearch.js +9 -19
- package/dist/services/oliveyoung/transport.js +19 -13
- package/dist/utils/zyteJsonFallback.js +4 -24
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -85,17 +85,15 @@
|
|
|
85
85
|
**[실시간 서비스 상태 보기](https://aka-page.betteruptime.com/)**
|
|
86
86
|
|
|
87
87
|
<!-- WORKERS_INVOCATIONS_CHART:START -->
|
|
88
|
-
<h3>Cloudflare 요청 수 (2026-08-
|
|
88
|
+
<h3>Cloudflare 요청 수 (2026-08-20 ~ 2026-09-18, 30일)</h3>
|
|
89
89
|
|
|
90
|
-
<img src="./assets/analytics/workers-invocations.png?v=2026-09-
|
|
90
|
+
<img src="./assets/analytics/workers-invocations.png?v=2026-09-18T18:31:45.449Z" alt="Cloudflare 요청 수 그래프 (2026-08-20 ~ 2026-09-18)" width="100%">
|
|
91
91
|
|
|
92
|
-
<sub>기준 워커: <code>daiso-mcp</code> · 마지막 갱신: 2026-09-
|
|
92
|
+
<sub>기준 워커: <code>daiso-mcp</code> · 마지막 갱신: 2026-09-19 03:31 KST</sub>
|
|
93
93
|
<br><sub>집계: Worker 실행 + 루트 GET 리디렉션 요청 · 사용자 수와 다릅니다.</sub>
|
|
94
94
|
|
|
95
95
|
</div>
|
|
96
96
|
|
|
97
|
-
> Zyte 유료 호출을 중지했습니다. 무료 원본 조회를 우선하며 일부 서비스는 별도 실행 환경이 필요합니다. [비용 정책과 서비스별 상태](docs/zyte-free-access.md)를 확인하세요.
|
|
98
|
-
|
|
99
97
|
> [!IMPORTANT]
|
|
100
98
|
> 최근 공개 서버 사용량이 크게 증가하여 2026년 7월 18일부터 올리브영·CGV·CU·GS25·롯데마트의 검색을 포함한 공개 GET API에 IP당 하루 합산 3,000회(KST 기준)의 호출 제한을 적용합니다. 한도를 초과하는 사용이 필요하다면 Daiso MCP는 오픈 소스이므로 이 저장소를 직접 배포해 이용해 주세요.
|
|
101
99
|
|
|
@@ -462,6 +460,10 @@ curl -H "Authorization: Bearer $HEALTH_CHECK_SECRET" \
|
|
|
462
460
|
- `timeoutMs=20000`: 체크별 요청 제한 시간
|
|
463
461
|
- `slowThresholdMs=9000`: 지정 시간보다 느린 성공 응답을 degraded로 표시
|
|
464
462
|
|
|
463
|
+
Health Checks 알림은 모든 실패·저하 서비스 ID를 상세 오류보다 먼저 표시합니다. 푸시 알림은 3,500자로 제한되며 전체 오류와 샘플은 해당 실행의 GitHub Actions 요약에 남습니다. 올리브영 릴레이의 설정 완전성은 `config.oliveyoungRelay`로 확인할 수 있습니다. 이 값은 연결 성공을 뜻하지 않으며, [설정 진단 문서](./docs/oliveyoung-free-relay.md#설정-진단)를 참고하세요.
|
|
464
|
+
|
|
465
|
+
Zyte 유료 요청은 키 설정 여부와 관계없이 비활성화되어 있습니다. 직접 JSON 요청이 실패하면 원래 HTTP 상태와 오류를 유지합니다.
|
|
466
|
+
|
|
465
467
|
상태 기준:
|
|
466
468
|
|
|
467
469
|
- `ok`: 필수 응답 구조와 최소 결과가 정상입니다.
|
package/dist/api/configStatus.js
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
|
+
import { isValidOliveyoungRelayUrl } from '../services/oliveyoung/transport.js';
|
|
1
2
|
function isConfigured(value) {
|
|
2
3
|
return typeof value === 'string' && value.trim().length > 0;
|
|
3
4
|
}
|
|
4
5
|
export function buildConfigStatus(bindings) {
|
|
6
|
+
const urlValid = isValidOliveyoungRelayUrl(bindings?.OY_RELAY_URL);
|
|
7
|
+
const tokenConfigured = isConfigured(bindings?.OY_RELAY_TOKEN);
|
|
8
|
+
const accessClientIdConfigured = isConfigured(bindings?.OY_ACCESS_CLIENT_ID);
|
|
9
|
+
const accessClientSecretConfigured = isConfigured(bindings?.OY_ACCESS_CLIENT_SECRET);
|
|
10
|
+
const accessConfigured = accessClientIdConfigured && accessClientSecretConfigured;
|
|
11
|
+
const accessPairValid = (bindings?.OY_ACCESS_CLIENT_ID === undefined &&
|
|
12
|
+
bindings?.OY_ACCESS_CLIENT_SECRET === undefined) ||
|
|
13
|
+
accessConfigured;
|
|
5
14
|
return {
|
|
15
|
+
oliveyoungRelay: {
|
|
16
|
+
configured: urlValid && tokenConfigured && accessPairValid,
|
|
17
|
+
urlConfigured: isConfigured(bindings?.OY_RELAY_URL),
|
|
18
|
+
urlValid,
|
|
19
|
+
tokenConfigured,
|
|
20
|
+
accessClientIdConfigured,
|
|
21
|
+
accessClientSecretConfigured,
|
|
22
|
+
accessConfigured,
|
|
23
|
+
accessPairValid,
|
|
24
|
+
usedBy: ['oliveyoung'],
|
|
25
|
+
},
|
|
6
26
|
googleMapsApiKey: {
|
|
7
27
|
configured: isConfigured(bindings?.GOOGLE_MAPS_API_KEY),
|
|
8
28
|
usedBy: ['gs25', 'cu', 'lottemart', 'megabox', 'lottecinema', 'cgv'],
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { ZYTE_COST_POLICY_MESSAGE } from '../../core/errors.js';
|
|
5
5
|
export class CgvUpstreamUnavailableError extends Error {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
upstreamStatus;
|
|
7
|
+
constructor(upstreamStatus) {
|
|
8
|
+
const statusSuffix = upstreamStatus === undefined ? '' : ` (HTTP ${upstreamStatus})`;
|
|
9
|
+
super(`CGV 원본 서비스에 연결할 수 없습니다. ${ZYTE_COST_POLICY_MESSAGE}${statusSuffix}`);
|
|
10
|
+
this.upstreamStatus = upstreamStatus;
|
|
8
11
|
this.name = 'CgvUpstreamUnavailableError';
|
|
9
12
|
}
|
|
10
13
|
}
|
|
@@ -55,7 +55,7 @@ export async function requestCgv(path, searchParams, timeout = 15000, _zyteApiKe
|
|
|
55
55
|
return await parseJsonResponse(response);
|
|
56
56
|
}
|
|
57
57
|
if (response.status === 401 || response.status === 403) {
|
|
58
|
-
throw new CgvUpstreamUnavailableError();
|
|
58
|
+
throw new CgvUpstreamUnavailableError(response.status);
|
|
59
59
|
}
|
|
60
60
|
throw new Error(`CGV API 호출 실패: ${response.status}`);
|
|
61
61
|
}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* CU API 클라이언트
|
|
3
3
|
*/
|
|
4
4
|
import { fetchJson } from '../../utils/http.js';
|
|
5
|
-
import { ZYTE_COST_POLICY_MESSAGE } from '../../core/errors.js';
|
|
6
5
|
import { fetchJsonWithZyteFallback } from '../../utils/zyteJsonFallback.js';
|
|
7
6
|
import { CU_API } from './api.js';
|
|
8
7
|
import { cuStockUnavailableReason } from './upstreamError.js';
|
|
@@ -70,9 +69,6 @@ async function requestCuWebHtml(path, body, _apiKey, timeout = 15000) {
|
|
|
70
69
|
if (response.ok) {
|
|
71
70
|
return response.text();
|
|
72
71
|
}
|
|
73
|
-
if (response.status === 400 || response.status === 403 || response.status === 429) {
|
|
74
|
-
throw new Error(ZYTE_COST_POLICY_MESSAGE);
|
|
75
|
-
}
|
|
76
72
|
throw new Error(`API 요청 실패: ${response.status} ${response.statusText}`);
|
|
77
73
|
}
|
|
78
74
|
export async function geocodeCuAddress(address, options = {}) {
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* GS25 상품 검색 API
|
|
3
3
|
*/
|
|
4
4
|
/* c8 ignore start */
|
|
5
|
-
import { fetchJson
|
|
6
|
-
import { requestByZyte } from '../../utils/zyte.js';
|
|
5
|
+
import { fetchJson } from '../../utils/http.js';
|
|
7
6
|
import { GS25_API } from './api.js';
|
|
8
7
|
import { toNumber } from './storeUtils.js';
|
|
9
8
|
const GS25_TOTAL_SEARCH_HEADERS = {
|
|
@@ -22,23 +21,14 @@ async function fetchGs25TotalSearchResponse(query, options) {
|
|
|
22
21
|
const { timeout = 20000 } = options;
|
|
23
22
|
const endpoint = new URL(GS25_API.TOTAL_SEARCH_PATH, GS25_API.APIGW_BASE_URL);
|
|
24
23
|
const bodyText = JSON.stringify({ query });
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
catch (error) {
|
|
36
|
-
const zyteApiKey = options.zyteApiKey?.trim();
|
|
37
|
-
if (!(error instanceof HttpError) || error.status !== 403 || !zyteApiKey) {
|
|
38
|
-
throw error;
|
|
39
|
-
}
|
|
40
|
-
return requestByZyte({ apiKey: zyteApiKey, url: endpoint.toString() });
|
|
41
|
-
}
|
|
24
|
+
return fetchJson(endpoint.toString(), {
|
|
25
|
+
...GS25_DEFAULT_FETCH_OPTIONS,
|
|
26
|
+
method: 'POST',
|
|
27
|
+
retryUnsafeMethods: true,
|
|
28
|
+
timeout,
|
|
29
|
+
headers: GS25_TOTAL_SEARCH_HEADERS,
|
|
30
|
+
body: bodyText,
|
|
31
|
+
});
|
|
42
32
|
}
|
|
43
33
|
export async function fetchGs25NormalizedKeyword(keyword, options = {}) {
|
|
44
34
|
const query = keyword.trim();
|
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
/** 올리브영 무료 직접 요청 및 운영자가 설정한 브라우저 릴레이 전송. */
|
|
2
2
|
import { fetchJson } from '../../utils/http.js';
|
|
3
3
|
import { OLIVEYOUNG_API } from './api.js';
|
|
4
|
+
/** 설정 진단과 실제 요청에 같은 릴레이 URL 규칙을 적용합니다. */
|
|
5
|
+
export function isValidOliveyoungRelayUrl(value) {
|
|
6
|
+
if (!value?.trim())
|
|
7
|
+
return false;
|
|
8
|
+
let relay;
|
|
9
|
+
try {
|
|
10
|
+
relay = new URL(value);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return !(relay.username ||
|
|
16
|
+
relay.password ||
|
|
17
|
+
relay.search ||
|
|
18
|
+
relay.hash ||
|
|
19
|
+
(relay.protocol !== 'https:' &&
|
|
20
|
+
!(relay.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(relay.hostname))));
|
|
21
|
+
}
|
|
4
22
|
export async function requestOliveyoung(path, body, options = {}) {
|
|
5
23
|
const { timeout = 15000, relayUrl, relayToken } = options;
|
|
6
24
|
const headers = {
|
|
@@ -10,19 +28,7 @@ export async function requestOliveyoung(path, body, options = {}) {
|
|
|
10
28
|
};
|
|
11
29
|
let url = `${OLIVEYOUNG_API.BASE_URL}${path}`;
|
|
12
30
|
if (relayUrl) {
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
relay = new URL(relayUrl);
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
throw new Error('올리브영 릴레이 URL은 HTTPS 또는 로컬 HTTP 주소여야 합니다.');
|
|
19
|
-
}
|
|
20
|
-
if (relay.username ||
|
|
21
|
-
relay.password ||
|
|
22
|
-
relay.search ||
|
|
23
|
-
relay.hash ||
|
|
24
|
-
(relay.protocol !== 'https:' &&
|
|
25
|
-
!(relay.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(relay.hostname)))) {
|
|
31
|
+
if (!isValidOliveyoungRelayUrl(relayUrl)) {
|
|
26
32
|
throw new Error('올리브영 릴레이 URL은 HTTPS 또는 로컬 HTTP 주소여야 합니다.');
|
|
27
33
|
}
|
|
28
34
|
if (!relayToken?.trim())
|
|
@@ -1,28 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 레거시 Zyte 옵션을 제거하고 무료 직접 JSON 조회만 수행합니다.
|
|
3
3
|
*/
|
|
4
|
-
import { fetchJson
|
|
5
|
-
import { requestByZyte } from './zyte.js';
|
|
6
|
-
const ZYTE_FALLBACK_STATUSES = new Set([400, 403, 429]);
|
|
7
|
-
function hasZyteApiKey(apiKey) {
|
|
8
|
-
if (apiKey?.trim()) {
|
|
9
|
-
return true;
|
|
10
|
-
}
|
|
11
|
-
/* c8 ignore next */
|
|
12
|
-
return typeof process !== 'undefined' && Boolean(process.env?.ZYTE_API_KEY?.trim());
|
|
13
|
-
}
|
|
4
|
+
import { fetchJson } from './http.js';
|
|
14
5
|
export async function fetchJsonWithZyteFallback(url, options = {}) {
|
|
15
|
-
const { zyteApiKey, zyteTags, ...directOptions } = options;
|
|
16
|
-
|
|
17
|
-
return await fetchJson(url, directOptions);
|
|
18
|
-
}
|
|
19
|
-
catch (error) {
|
|
20
|
-
if (!(error instanceof HttpError) || !ZYTE_FALLBACK_STATUSES.has(error.status)) {
|
|
21
|
-
throw error;
|
|
22
|
-
}
|
|
23
|
-
if (!hasZyteApiKey(zyteApiKey)) {
|
|
24
|
-
throw error;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return requestByZyte({ apiKey: zyteApiKey, url, tags: zyteTags });
|
|
6
|
+
const { zyteApiKey: _zyteApiKey, zyteTags: _zyteTags, ...directOptions } = options;
|
|
7
|
+
return fetchJson(url, directOptions);
|
|
28
8
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "daiso",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Korean retail and cinema MCP server, CLI, and Codex Skill for Daiso, convenience stores, marts, Olive Young, and theaters",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"@cloudflare/workers-types": "^5.20260914.1",
|
|
85
85
|
"@eslint/js": "^10.0.1",
|
|
86
86
|
"@types/node": "^26.5.1",
|
|
87
|
-
"@vitest/coverage-v8": "^
|
|
87
|
+
"@vitest/coverage-v8": "^5.0.1",
|
|
88
88
|
"eslint": "^10.8.0",
|
|
89
89
|
"eslint-config-prettier": "^10.1.8",
|
|
90
90
|
"globals": "^17.12.0",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"tsx": "^4.23.1",
|
|
96
96
|
"typescript": "6.0.3",
|
|
97
97
|
"typescript-eslint": "^8.65.0",
|
|
98
|
-
"vitest": "^
|
|
98
|
+
"vitest": "^5.0.1",
|
|
99
99
|
"wrangler": "^4.131.2"
|
|
100
100
|
},
|
|
101
101
|
"overrides": {
|