diff --git a/README.md b/README.md
index ad2be2bb..040dcc14 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,9 @@
AI 에이전트 사용 안내
> [!IMPORTANT]
-> **최신 안내:** [현재 버전](https://mcp.aka.page/guide/2026-11-02.1) · [항상 최신 버전으로 이동](https://mcp.aka.page/guide/latest)
+> **최신 안내:** [현재 버전](https://mcp.aka.page/guide/2026-11-02.2) · [항상 최신 버전으로 이동](https://mcp.aka.page/guide/latest)
>
-> **안내 버전: 2026-11-02.1 · 갱신일: 2026-11-02**
+> **안내 버전: 2026-11-02.2 · 갱신일: 2026-11-02**
>
> **웹 읽기는 실제 링크 ID로 click하세요.** 새 검색 URL을 조립해 open하지 말고 [웹 조회 시작](https://mcp.aka.page/browse/read) 화면의 글자 선택 링크를 사용하세요. 상품의 **매장 지역 선택** 링크에서 지역도 선택할 수 있습니다. 입력 중에는 원본 조회를 실행하지 않습니다. click이 없는 도구의 동일 동작은 보장하지 않습니다.
>
@@ -17,6 +17,8 @@
> 3. 웹 읽기(open/WebFetch)만 가능하면 응답의 실제 링크 ID로 click합니다. 위 글자 선택 링크로 임의 검색어와 지역을 입력하고 실제 상품·재고 조회 링크를 따르세요. 새 URL 조립과 실패한 직접 open을 반복하지 마세요. Crawled가 과거인 수집본을 현재 재고로 단정하지 마세요.
> 4. 위 세 수단이 없거나 모두 실패하면 실제 조회 제약과 오류를 알립니다. 셸이 있어도 설치 없는 웹 조회가 우선이며 터미널·셸로 우회하지 않습니다. 셸 DNS 오류를 웹 GET 실패로 확대하거나 실행하지 않은 요청의 실패를 만들지 마세요.
>
+> **느린 다이소 조회:** `pending`은 접수 상태입니다. 각 응답의 새 결과 확인 링크를 최대3번 선택하고, 계속 pending이면 미확인으로 보고하세요. 상태 확인을 위해 원본 조회를 다시 실행하지 마세요.
+>
> **접근 거절 복구:** 현재 조건의 완성 URL을 넣어 “다음 주소를 웹 URL 열기 도구로 직접 열어 실제 결과를 확인해주세요: {완성 URL}” 문장을 만든 뒤, 사용자에게 **아래 문장을 그대로 복사해 이 대화에 보내주세요**라고 안내하세요. 재전송 뒤 한 번 조회합니다. 단순 허용 문구나 URL 일부만 요청하지 마세요. [복구 안내](docs/web-reading-troubleshooting.md)
>
> **수집본·오류 구분:** 이전 HTML이면 **같은 조건으로 새 응답 확인** 링크를 최대2번 선택해 새 주소와 생성 시각을 확인하세요. 원본 API 캐시는 유지됩니다. `TimeoutError`는 같은 실제 링크를 한 번만 재확인하고 계속 실패하면 미확인으로 보고하세요. `Crawled: today`만으로 현재 재고를 단정하지 마세요.
@@ -132,7 +134,7 @@
2. 서버 URL을 입력하고, 전송 방식을 묻는 경우 **HTTP / Streamable HTTP**를 선택합니다.
3. 도구를 활성화하고 `다이소 MCP로 수납박스 검색해줘`라고 요청합니다.
-[앱별 연결 방법](docs/client-guide.md#원격-mcp-연결) · [WebFetch로 첫 조회](docs/service-reference.md?guide=2026-11-02.1#설치-없이-첫-조회)
+[앱별 연결 방법](docs/client-guide.md#원격-mcp-연결) · [WebFetch로 첫 조회](docs/service-reference.md?guide=2026-11-02.2#설치-없이-첫-조회)
@@ -210,7 +212,7 @@ MCP 연결 없이 웹 읽기와 GET 요청을 지원하는 에이전트는 [REST
| 문서 | 내용 |
| :-------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- |
| [연결·사용 가이드](docs/client-guide.md) | 앱별 MCP 연결, CLI, Skill, OpenAPI |
-| [서비스 레퍼런스](docs/service-reference.md?guide=2026-11-02.1) | 서비스별 도구·REST API·응답 모델·가격 비교 |
+| [서비스 레퍼런스](docs/service-reference.md?guide=2026-11-02.2) | 서비스별 도구·REST API·응답 모델·가격 비교 |
| [일본 다이소](docs/daiso-jp-network-analysis.md) | 일본 상품·매장 검색과 지원 범위 |
| [독립·예술영화관](docs/dtryx.md) | 디트릭스 지원 극장과 조회 방법 |
| [AI 조회 가이드](docs/ai-instruction.md) | 도구 선택과 상품·매장·재고 조회 순서 |
diff --git a/docs/ai-instruction.md b/docs/ai-instruction.md
index d068ae80..bceecbd4 100644
--- a/docs/ai-instruction.md
+++ b/docs/ai-instruction.md
@@ -1,11 +1,14 @@
# DAISO MCP 에이전트 지침
+다이소의 읽기 세션 조회가 오래 걸리면 `pending` 접수와 실제 **결과 확인** 링크를 반환합니다. 접수는 상품·재고 확인 성공이 아닙니다. 각 응답의 새로운 결과 링크를 최대3번 선택하고, 계속 pending이면 미확인으로 보고하세요. 이전 상태 주소를 반복하거나 원본 조회를 다시 실행하지 마세요. 상태 확인은 저장 결과만 읽으며, 완료 결과는 원본의 성공·오류 상태를 유지합니다. HTML·작업 완료 시각과 원본 재고 갱신 시각은 다릅니다. 작업이 만료되거나 시간 초과된 경우에도 수량을0으로 바꾸지 않습니다.
+
+
웹 읽기 접근 거절이 계속되면 **현재 조건의 완성 URL을 넣은 복사용 문장**을 제시하고 “아래 문장을 그대로 복사해 이 대화에 보내주세요”라고 안내하세요. 사용자 재전송 뒤 한 번 조회합니다. 단순 허용 문구·URL 일부 제공·실행하지 않은 실패 보고로 대체하지 마세요. [복사 재전송 및 오류 구분](https://github.com/hmmhmmhm/daiso-mcp/blob/main/docs/web-reading-troubleshooting.md)을 따릅니다.
## 설치 없이 첫 조회
-최신 서버 안내는 [현재 버전](https://mcp.aka.page/guide/2026-11-02.1)과 [최신 연결](https://mcp.aka.page/guide/latest)에서 제공합니다. 웹 도구는 과거 리디렉션·수집본을 반환할 수 있으므로 표시된 버전과 HTML 생성 시각을 확인하세요.
+최신 서버 안내는 [현재 버전](https://mcp.aka.page/guide/2026-11-02.2)과 [최신 연결](https://mcp.aka.page/guide/latest)에서 제공합니다. 웹 도구는 과거 리디렉션·수집본을 반환할 수 있으므로 표시된 버전과 HTML 생성 시각을 확인하세요.
1. 연결된 MCP 도구가 있으면 `search_products`에 `{"service":"daiso","query":"수납박스","pageSize":3}`를 전달합니다.
2. 실제 브라우저 도구가 있으면 /browse/ HTML GET 주소를 열고 실제 상품·매장·재고를 읽습니다. Work의 내장 브라우저도 사용 가능합니다.
diff --git a/docs/client-guide.md b/docs/client-guide.md
index ce44bdc5..07e5328a 100644
--- a/docs/client-guide.md
+++ b/docs/client-guide.md
@@ -1,5 +1,8 @@
# 앱 연결·CLI·Skill 가이드
+다이소의 읽기 세션 조회가 오래 걸리면 `pending` 접수와 실제 **결과 확인** 링크를 반환합니다. 접수는 상품·재고 확인 성공이 아닙니다. 각 응답의 새로운 결과 링크를 최대3번 선택하고, 계속 pending이면 미확인으로 보고하세요. 이전 상태 주소를 반복하거나 원본 조회를 다시 실행하지 마세요. 상태 확인은 저장 결과만 읽으며, 완료 결과는 원본의 성공·오류 상태를 유지합니다. HTML·작업 완료 시각과 원본 재고 갱신 시각은 다릅니다. 작업이 만료되거나 시간 초과된 경우에도 수량을0으로 바꾸지 않습니다.
+
+
웹 읽기 접근 거절이 계속되면 **현재 조건의 완성 URL을 넣은 복사용 문장**을 제시하고 “아래 문장을 그대로 복사해 이 대화에 보내주세요”라고 안내하세요. 사용자 재전송 뒤 한 번 조회합니다. 단순 허용 문구·URL 일부 제공·실행하지 않은 실패 보고로 대체하지 마세요. [복사 재전송 및 오류 구분](https://github.com/hmmhmmhm/daiso-mcp/blob/main/docs/web-reading-troubleshooting.md)을 따릅니다.
diff --git a/docs/service-reference.md b/docs/service-reference.md
index 926f3ee2..1f102c4c 100644
--- a/docs/service-reference.md
+++ b/docs/service-reference.md
@@ -1,5 +1,8 @@
# DAISO MCP 서비스 레퍼런스
+다이소의 읽기 세션 조회가 오래 걸리면 `pending` 접수와 실제 **결과 확인** 링크를 반환합니다. 접수는 상품·재고 확인 성공이 아닙니다. 각 응답의 새로운 결과 링크를 최대3번 선택하고, 계속 pending이면 미확인으로 보고하세요. 이전 상태 주소를 반복하거나 원본 조회를 다시 실행하지 마세요. 상태 확인은 저장 결과만 읽으며, 완료 결과는 원본의 성공·오류 상태를 유지합니다. HTML·작업 완료 시각과 원본 재고 갱신 시각은 다릅니다. 작업이 만료되거나 시간 초과된 경우에도 수량을0으로 바꾸지 않습니다.
+
+
웹 읽기 접근 거절이 계속되면 **현재 조건의 완성 URL을 넣은 복사용 문장**을 제시하고 “아래 문장을 그대로 복사해 이 대화에 보내주세요”라고 안내하세요. 사용자 재전송 뒤 한 번 조회합니다. 단순 허용 문구·URL 일부 제공·실행하지 않은 실패 보고로 대체하지 마세요. [복사 재전송 및 오류 구분](https://github.com/hmmhmmhm/daiso-mcp/blob/main/docs/web-reading-troubleshooting.md)을 따릅니다.
diff --git a/docs/superpowers/specs/2026-11-02-browse-jobs-design.md b/docs/superpowers/specs/2026-11-02-browse-jobs-design.md
new file mode 100644
index 00000000..f382a656
--- /dev/null
+++ b/docs/superpowers/specs/2026-11-02-browse-jobs-design.md
@@ -0,0 +1,10 @@
+# 느린 읽기의 접수·결과 전달
+
+2026-11-02 사용자 승인과 별도 Worker 실험을 따른다. 실제 ChatGPT 새 임시 채팅2건에서5초 가상 작업의 접수→서로 다른 실제 결과 링크→ready 및 서버 requestId 일치를 확인했다. 같은 주소 재시도는 실패/과거 pending 재사용이 관측됐다.
+
+- 우선 다이소 /browse/read 읽기 세션에만 적용하고 다른 서비스의 기존 경로는 유지한다. 실제 API 호출을 한 번 실행한다.1초 안에 끝나면 기존 응답을 반환한다. 느린 경우에만 별도 읽기 작업을 만들고 pending HTML 및 새 실제 결과 확인 링크를 반환한다.
+- 원본 요청의 권한·IP별 한도·캐시를 기존 API에서 적용한다. 후속 상태 확인은 저장 결과만 읽고 원본을 다시 호출하지 않는다. 공개 MCP10도구/JSON/CLI와 일반 /browse 경로를 유지한다.
+- 원본 호출과 HTML 장식을 waitUntil로 유지하고 사용자 연결 취소와 분리한다. 작업의25초 제한·5분 TTL·256KiB 저장 상한을 둔다. 제한·외부 오류·미확인 수량을 정상 재고0으로 바꾸지 않는다.
+- 요청 헤더·인증·쿠키·원본 조회 조건은 작업 저장소에 보관하지 않는다. 안전한 공개 응답 본문·상태·Content-Type과 재시도·리디렉션·보안 헤더 허용 목록만 저장한다. 작업 주소는 무작위 UUID이며 상태 응답마다 다른 실제 후속 링크를 제공한다. HTML 생성·작업 완료·원본 재고 갱신 시각은 구별한다.
+- 저장 실패 시 기존 동기 응답으로 복귀한다. 늦은 완료/알람의 경합으로 정상 결과를 덮어쓰지 않는다. 대기·만료는 HTML로 명확히 보고한다.
+- 저장 및 순차 실제 API 경계를 가상시계로 검증하고, 필수 CI/Coverage100%/리뷰/배포 이후 실제 공개 조회와 ChatGPT 웹에서 상태 링크를 확인한다. 검증용 브라우저는 소유 창1개, 검사와 겹치지 않는다.
diff --git a/docs/web-reading-troubleshooting.md b/docs/web-reading-troubleshooting.md
index 8b9c1230..6bf48b55 100644
--- a/docs/web-reading-troubleshooting.md
+++ b/docs/web-reading-troubleshooting.md
@@ -1,5 +1,8 @@
# 웹 읽기 접근 오류 진단
+다이소의 읽기 세션 조회가 오래 걸리면 `pending` 접수와 실제 **결과 확인** 링크를 반환합니다. 접수는 상품·재고 확인 성공이 아닙니다. 각 응답의 새로운 결과 링크를 최대3번 선택하고, 계속 pending이면 미확인으로 보고하세요. 이전 상태 주소를 반복하거나 원본 조회를 다시 실행하지 마세요. 상태 확인은 저장 결과만 읽으며, 완료 결과는 원본의 성공·오류 상태를 유지합니다. HTML·작업 완료 시각과 원본 재고 갱신 시각은 다릅니다. 작업이 만료되거나 시간 초과된 경우에도 수량을0으로 바꾸지 않습니다.
+
+
2026-10-26 실험에서는 같은 재고 주소가 일반 HTTPS, MCP SDK, Work 내장 브라우저에서는 정상 동작했지만 웹 읽기 도구에서는 접근 오류가 발생했습니다. 일부 기존 링크는 웹 읽기로 열렸으나 새 주소나 동일 주소의 직접 열기는 실패했습니다. 과거 수집 시점의 결과가 반환되는 경우도 확인했습니다.
Cloudflare 관측 구간에는 정상 페이지의 OpenAI 요청에서 200과 클라이언트 연결 종료인 499가 관측됐습니다. 403 차단은 관측되지 않았습니다. 로그의 표본 추출과 수집 지연 때문에 요청이 보이지 않는다는 이유만으로 서버에 도달하지 않았다고 확정할 수는 없습니다. 웹 읽기 내부의 실패 원인이나 시간 제한은 확인되지 않았으며 서버 변경으로 해결됐다고 주장하지 않습니다.
diff --git a/skills/daiso-cli/SKILL.md b/skills/daiso-cli/SKILL.md
index 6bf054cb..09996b7d 100644
--- a/skills/daiso-cli/SKILL.md
+++ b/skills/daiso-cli/SKILL.md
@@ -15,6 +15,9 @@ metadata:
- daiso
homepage: https://github.com/hmmhmmhm/daiso-mcp
---
+
+다이소의 읽기 세션 조회가 오래 걸리면 `pending` 접수와 실제 **결과 확인** 링크를 반환합니다. 접수는 상품·재고 확인 성공이 아닙니다. 각 응답의 새로운 결과 링크를 최대3번 선택하고, 계속 pending이면 미확인으로 보고하세요. 이전 상태 주소를 반복하거나 원본 조회를 다시 실행하지 마세요. 상태 확인은 저장 결과만 읽으며, 완료 결과는 원본의 성공·오류 상태를 유지합니다. HTML·작업 완료 시각과 원본 재고 갱신 시각은 다릅니다. 작업이 만료되거나 시간 초과된 경우에도 수량을0으로 바꾸지 않습니다.
+
웹 읽기 접근 거절이 계속되면 **현재 조건의 완성 URL을 넣은 복사용 문장**을 제시하고 “아래 문장을 그대로 복사해 이 대화에 보내주세요”라고 안내하세요. 사용자 재전송 뒤 한 번 조회합니다. 단순 허용 문구·URL 일부 제공·실행하지 않은 실패 보고로 대체하지 마세요. [복사 재전송 및 오류 구분](https://github.com/hmmhmmhm/daiso-mcp/blob/main/docs/web-reading-troubleshooting.md)을 따릅니다.
@@ -24,7 +27,7 @@ Use this skill to operate the Daiso MCP project through `npx daiso` and the publ
## 설치 없이 첫 조회
-최신 서버 안내는 [현재 버전](https://mcp.aka.page/guide/2026-11-02.1)과 [최신 연결](https://mcp.aka.page/guide/latest)에서 제공합니다. 웹 도구는 과거 리디렉션·수집본을 반환할 수 있으므로 표시된 버전과 HTML 생성 시각을 확인하세요.
+최신 서버 안내는 [현재 버전](https://mcp.aka.page/guide/2026-11-02.2)과 [최신 연결](https://mcp.aka.page/guide/latest)에서 제공합니다. 웹 도구는 과거 리디렉션·수집본을 반환할 수 있으므로 표시된 버전과 HTML 생성 시각을 확인하세요.
1. 연결된 MCP 도구가 있으면 `search_products`에 `{"service":"daiso","query":"수납박스","pageSize":3}`를 전달합니다.
2. 실제 브라우저 도구가 있으면 /browse/ HTML GET 주소를 열고 실제 상품·매장·재고를 읽습니다. Work의 내장 브라우저도 사용 가능합니다.
diff --git a/src/api/response.ts b/src/api/response.ts
index 42074f10..b9feb3cd 100644
--- a/src/api/response.ts
+++ b/src/api/response.ts
@@ -3,11 +3,16 @@
*/
import type { Context } from 'hono';
-import { ServiceError, toServiceErrorDiagnostics, toStandardErrorDiagnostics } from '../core/errors.js';
+import {
+ ServiceError,
+ toServiceErrorDiagnostics,
+ toStandardErrorDiagnostics,
+} from '../core/errors.js';
export interface AppBindings {
DAISO_JP_PRODUCT_RELAY_ENABLED?: string;
DAILY_RATE_LIMITER?: DurableObjectNamespace;
+ BROWSE_READ_JOBS?: DurableObjectNamespace;
UPSTREAM_ROUTE_HEALTH?: DurableObjectNamespace;
GEOCODE_GATEWAY?: DurableObjectNamespace;
ZYTE_API_KEY?: string;
@@ -92,9 +97,12 @@ export function errorResponse(
/** 원문이 제거된 외부 서비스 오류를 실제 상태와 함께 반환합니다. */
export function serviceErrorResponse(c: ApiContext, error: ServiceError, operation: string) {
if (error.retryAfter !== undefined) c.header('Retry-After', String(error.retryAfter));
- return c.json>({
- success: false,
- error: { code: error.code, message: error.message },
- diagnostics: toServiceErrorDiagnostics(error, operation),
- }, error.status);
+ return c.json>(
+ {
+ success: false,
+ error: { code: error.code, message: error.message },
+ diagnostics: toServiceErrorDiagnostics(error, operation),
+ },
+ error.status,
+ );
}
diff --git a/src/durableObjects/browseReadJob.ts b/src/durableObjects/browseReadJob.ts
new file mode 100644
index 00000000..d6a10dd5
--- /dev/null
+++ b/src/durableObjects/browseReadJob.ts
@@ -0,0 +1,120 @@
+/** 읽기 결과만 잠시 저장합니다. 요청 헤더·인증값·원본 호출 조건은 저장하지 않습니다. */
+export const BROWSE_RESULT_HEADERS = [
+ 'retry-after',
+ 'location',
+ 'content-security-policy',
+ 'x-content-type-options',
+ 'x-daiso-read-id',
+ 'x-ratelimit-limit',
+ 'x-ratelimit-remaining',
+ 'x-ratelimit-reset',
+];
+export const BROWSE_JOB_TTL = 300000;
+export const BROWSE_JOB_DEADLINE = 25000;
+export type ReadSnapshot = {
+ status: number;
+ body: string;
+ contentType: string;
+ headers?: Record;
+};
+type Job = {
+ status: 'pending' | 'ready';
+ createdAt: number;
+ completedAt?: string;
+ response?: ReadSnapshot;
+};
+export const expiredReadSnapshot = (): ReadSnapshot => ({
+ status: 504,
+ contentType: 'text/html; charset=utf-8',
+ body: '
조회가 제한 시간 안에 완료되지 않았습니다.
수량은 미확인입니다. 접수는 재고 조회 성공이 아닙니다.
',
+});
+
+function validSnapshot(input: unknown): input is ReadSnapshot {
+ if (!input || typeof input !== 'object') return false;
+ const v = input as ReadSnapshot;
+ return (
+ Number.isInteger(v.status) &&
+ v.status >= 200 &&
+ v.status <= 599 &&
+ ![204, 205, 304].includes(v.status) &&
+ typeof v.body === 'string' &&
+ new TextEncoder().encode(v.body).length <= 262144 &&
+ typeof v.contentType === 'string' &&
+ v.contentType.length <= 128 &&
+ !/[\r\n]/.test(v.contentType) &&
+ Object.entries(v.headers ?? {}).every(
+ ([key, value]) =>
+ BROWSE_RESULT_HEADERS.includes(key) &&
+ typeof value === 'string' &&
+ value.length <= 2048 &&
+ !/[\r\n]/.test(value),
+ )
+ );
+}
+
+export class BrowseReadJob {
+ constructor(private readonly state: DurableObjectState) {}
+ async fetch(request: Request): Promise {
+ const path = new URL(request.url).pathname;
+ const storage = this.state.storage;
+ const current = await storage.get('job');
+ if (request.method === 'GET') {
+ if (!current || Date.now() >= current.createdAt + BROWSE_JOB_TTL)
+ return new Response(null, { status: 404 });
+ return Response.json(current);
+ }
+ if (request.method === 'POST' && path === '/start') {
+ const accepted = await storage.transaction(async (tx) => {
+ if (await tx.get('job')) return false;
+ await tx.put('job', { status: 'pending', createdAt: Date.now() });
+ return true;
+ });
+ if (accepted) await storage.setAlarm(Date.now() + BROWSE_JOB_DEADLINE);
+ return new Response(null, { status: accepted ? 201 : 409 });
+ }
+ if (request.method !== 'POST' || path !== '/complete')
+ return new Response(null, { status: 405 });
+ let input: unknown;
+ try {
+ input = JSON.parse(await request.text());
+ } catch {
+ return new Response(null, { status: 400 });
+ }
+ if (!validSnapshot(input)) return new Response(null, { status: 400 });
+ const accepted = await storage.transaction(async (tx) => {
+ const job = await tx.get('job');
+ if (!job || job.status !== 'pending' || Date.now() >= job.createdAt + BROWSE_JOB_TTL)
+ return false;
+ await tx.put('job', {
+ ...job,
+ status: 'ready',
+ completedAt: new Date().toISOString(),
+ response: input,
+ });
+ return true;
+ });
+ return new Response(null, { status: accepted ? 200 : 409 });
+ }
+ async alarm(): Promise {
+ const job = await this.state.storage.get('job');
+ if (!job || Date.now() >= job.createdAt + BROWSE_JOB_TTL) {
+ await this.state.storage.deleteAll();
+ return;
+ }
+ await this.state.storage.transaction(async (tx) => {
+ const latest = await tx.get('job');
+ if (latest?.status === 'pending' && Date.now() >= latest.createdAt + BROWSE_JOB_DEADLINE) {
+ await tx.put('job', {
+ ...latest,
+ status: 'ready',
+ completedAt: new Date().toISOString(),
+ response: expiredReadSnapshot(),
+ });
+ }
+ });
+ await this.state.storage.setAlarm(
+ job.createdAt +
+ (Date.now() < job.createdAt + BROWSE_JOB_DEADLINE ? BROWSE_JOB_DEADLINE : BROWSE_JOB_TTL),
+ );
+ }
+}
diff --git a/src/index.ts b/src/index.ts
index a48df9e8..8d0948fe 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,3 +1,4 @@
+export { BrowseReadJob } from './durableObjects/browseReadJob.js';
import { registerBrowseRoutes } from './pages/browse.js';
import { applyCompactTools } from './core/compact/server.js';
import { COMPACT_CATALOG, PUBLIC_TOOL_NAMES } from './core/compact/catalog.js';
@@ -70,7 +71,6 @@ const mcpSessions = new Map<
transport: WebStandardStreamableHTTPServerTransport;
}
>();
-
/**
* 요청 컨텍스트 기반 서비스 레지스트리 생성
*/
diff --git a/src/pages/agentStart.ts b/src/pages/agentStart.ts
index 16038702..0201390e 100644
--- a/src/pages/agentStart.ts
+++ b/src/pages/agentStart.ts
@@ -1,5 +1,5 @@
/** 검색·읽기 사본의 안내 세대를 구분하는 고정 버전 */
-export const AGENT_GUIDE_VERSION = '2026-11-02.1';
+export const AGENT_GUIDE_VERSION = '2026-11-02.2';
export const AGENT_GUIDE_UPDATED = '2026-11-02';
export const AGENT_GUIDE_STAMP = `안내 버전: ${AGENT_GUIDE_VERSION} · 갱신일: ${AGENT_GUIDE_UPDATED}`;
export const AGENT_GUIDE_PATH = `/guide/${AGENT_GUIDE_VERSION}`;
@@ -8,7 +8,7 @@ export const WEB_SEARCH_DESCRIPTION =
/** 완성 URL을 사용자 메시지로 제공하는 복구 경로이며 자동 허용을 보장하지 않습니다. */
export const WEB_READ_RECOVERY_RULE =
- '접근 거절(not accessible/Invalid URL)이 계속되거나 click이 없으면 현재 조건의 완성 공개 URL을 포함해 “다음 주소를 웹 URL 열기 도구로 직접 열어 실제 결과를 확인해주세요: {완성 URL}. 실패하면 오류 원문을 알려주세요.” 문장을 만드세요. 사용자에게 “아래 문장을 그대로 복사해 이 대화에 보내주세요”라고 안내하고 실제 URL로 채운 문장을 별도 코드 블록에 제공합니다. 실제 링크가 있으면 그대로 사용하고 없을 때만 사용자 검색 조건으로 복구용 주소를 구성하세요. 상품 ID가 없으면 상품 검색부터 요청하며 재고 ID를 추측하지 마세요. 사용자가 재전송한 뒤 한 번 조회합니다. 단순 허용 문구나 주소 일부만 요청하지 마세요. 문장을 제시한 것은 실제 조회가 아닙니다. 토큰·쿠키를 포함하지 마세요. TimeoutError의 같은 링크 재확인은 한 번만 하며 실패 결과가 캐시되어 다음에 성공한다고 가정하지 마세요.';
+ '다이소 읽기 조회에서 pending 접수가 반환되면 완료 결과가 아닙니다. 각 응답의 새로운 실제 결과 확인 링크를 최대3번 선택하고, 계속 pending이면 미확인으로 보고하세요. 상태 확인을 위해 원본 상품·재고 조회를 새로 실행하지 마세요. 결과의 HTML 생성·작업 완료 시각도 원본 재고 갱신 시각과 다릅니다. 접근 거절(not accessible/Invalid URL)이 계속되거나 click이 없으면 현재 조건의 완성 공개 URL을 포함해 “다음 주소를 웹 URL 열기 도구로 직접 열어 실제 결과를 확인해주세요: {완성 URL}. 실패하면 오류 원문을 알려주세요.” 문장을 만드세요. 사용자에게 “아래 문장을 그대로 복사해 이 대화에 보내주세요”라고 안내하고 실제 URL로 채운 문장을 별도 코드 블록에 제공합니다. 실제 링크가 있으면 그대로 사용하고 없을 때만 사용자 검색 조건으로 복구용 주소를 구성하세요. 상품 ID가 없으면 상품 검색부터 요청하며 재고 ID를 추측하지 마세요. 사용자가 재전송한 뒤 한 번 조회합니다. 단순 허용 문구나 주소 일부만 요청하지 마세요. 문장을 제시한 것은 실제 조회가 아닙니다. 토큰·쿠키를 포함하지 마세요. TimeoutError의 같은 링크 재확인은 한 번만 하며 실패 결과가 캐시되어 다음에 성공한다고 가정하지 마세요.';
/** 웹 읽기 환경에서 쿼리 없는 HTML 경로를 우선합니다. */
export const WEB_READ_RULE =
diff --git a/src/pages/browseJobs.ts b/src/pages/browseJobs.ts
new file mode 100644
index 00000000..43ac865c
--- /dev/null
+++ b/src/pages/browseJobs.ts
@@ -0,0 +1,189 @@
+/** 느린 읽기만 접수형으로 전환하고 실제 원본 요청은 기존 API에서 한 번 실행합니다. */
+import type { Context, Hono } from 'hono';
+import type { AppBindings } from '../api/response.js';
+import {
+ BROWSE_JOB_DEADLINE,
+ BROWSE_RESULT_HEADERS,
+ expiredReadSnapshot,
+ type ReadSnapshot,
+} from '../durableObjects/browseReadJob.js';
+type Bindings = { Bindings: AppBindings };
+const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
+function page(body: string, status = 200): Response {
+ return new Response(
+ `DAISO MCP 읽기 작업${body}`,
+ {
+ status,
+ headers: {
+ 'Content-Type': 'text/html; charset=utf-8',
+ 'Cache-Control': 'no-store',
+ 'X-Robots-Tag': 'noindex, nofollow',
+ 'Content-Security-Policy': "default-src 'none'; base-uri 'none'; frame-ancestors 'none'",
+ },
+ },
+ );
+}
+function pending(id: string): Response {
+ const path = `/browse/jobs/${id}/${crypto.randomUUID()}`;
+ return page(
+ `
조회 접수 · pending
아직 결과가 없습니다. 접수는 조회 성공이나 재고0을 뜻하지 않습니다.
각 응답의 새로운 실제 링크를 최대3번 선택하고 계속 pending이면 미확인으로 보고하세요. 이전 주소를 반복하지 마세요.