@nomadamas/k-skill 0.2.5 → 0.4.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/package.json +1 -1
- package/skills/bok-ecos-stats/instruction.md +1 -1
- package/skills/bunjang-search/references/DISCLAIMER.md +23 -0
- package/skills/coupang-product-search/references/DISCLAIMER.md +23 -0
- package/skills/daangn-cars-search/references/DISCLAIMER.md +23 -0
- package/skills/daangn-jobs-search/references/DISCLAIMER.md +23 -0
- package/skills/daangn-realty-search/references/DISCLAIMER.md +23 -0
- package/skills/daangn-used-goods-search/references/DISCLAIMER.md +23 -0
- package/skills/daishin-report-search/references/DISCLAIMER.md +23 -0
- package/skills/daiso-product-search/references/DISCLAIMER.md +23 -0
- package/skills/danawa-price-search/references/DISCLAIMER.md +23 -0
- package/skills/delivery-tracking/references/DISCLAIMER.md +23 -0
- package/skills/express-bus-booking/references/DISCLAIMER.md +23 -0
- package/skills/fine-dust-location/skill.json +1 -7
- package/skills/flight-ticket-search/references/DISCLAIMER.md +23 -0
- package/skills/foresttrip-vacancy/references/DISCLAIMER.md +23 -0
- package/skills/intercity-bus-booking/references/DISCLAIMER.md +23 -0
- package/skills/job-posting-match/references/DISCLAIMER.md +23 -0
- package/skills/jobkorea-talent-search/references/DISCLAIMER.md +23 -0
- package/skills/k-skill-setup/skill.json +1 -7
- package/skills/kakao-bar-nearby/references/DISCLAIMER.md +15 -0
- package/skills/kakao-map/references/DISCLAIMER.md +15 -0
- package/skills/kakaotalk-mac/references/DISCLAIMER.md +15 -0
- package/skills/kbl-results/references/DISCLAIMER.md +13 -0
- package/skills/kbo-results/references/DISCLAIMER.md +13 -0
- package/skills/keris-academic-search/references/DISCLAIMER.md +13 -0
- package/skills/keris-academic-search/references/TRADEMARK-LEGAL-STATEMENT.md +7 -0
- package/skills/kleague-results/references/DISCLAIMER.md +13 -0
- package/skills/kopis-performance-search/references/DISCLAIMER.md +13 -0
- package/skills/kopis-performance-search/references/TRADEMARK-LEGAL-STATEMENT.md +7 -0
- package/skills/kopis-performance-search/skill.json +3 -4
- package/skills/korean-cinema-search/references/DISCLAIMER.md +13 -0
- package/skills/korean-stock-search/references/DISCLAIMER.md +14 -0
- package/skills/korean-stock-search/references/TRADEMARK-LEGAL-STATEMENT.md +7 -0
- package/skills/korean-transit-route/references/DISCLAIMER.md +13 -0
- package/skills/ktx-booking/instruction.md +38 -221
- package/skills/ktx-booking/references/AUTOMATION-LEGAL-STATEMENT.md +10 -0
- package/skills/ktx-booking/references/DISCLAIMER.md +14 -0
- package/skills/ktx-booking/scripts/ktx_booking.py +176 -1274
- package/skills/ktx-booking/scripts/ktx_booking.py.lock +27 -0
- package/skills/ktx-booking/scripts/ktx_timetable.py +141 -0
- package/skills/ktx-booking/skill.json +3 -11
- package/skills/lck-analytics/references/DISCLAIMER.md +13 -0
- package/skills/lotto-results/references/DISCLAIMER.md +13 -0
- package/skills/market-kurly-search/references/DISCLAIMER.md +13 -0
- package/skills/myrealtrip-search/instruction.md +3 -1
- package/skills/myrealtrip-search/references/DISCLAIMER.md +13 -0
- package/skills/myrealtrip-search/scripts/myrealtrip_mcp.py +30 -8
- package/skills/myrealtrip-search/scripts/test_myrealtrip_mcp.py +62 -0
- package/skills/naver-ad-performance/references/DISCLAIMER.md +13 -0
- package/skills/naver-blog-research/references/DISCLAIMER.md +13 -0
- package/skills/naver-news-search/references/DISCLAIMER.md +13 -0
- package/skills/naver-shopping-search/references/DISCLAIMER.md +13 -0
- package/skills/ohou-today-deal/references/DISCLAIMER.md +13 -0
- package/skills/olive-young-search/references/DISCLAIMER.md +13 -0
- package/skills/popbill/references/DISCLAIMER.md +13 -0
- package/skills/saramin-talent-search/references/DISCLAIMER.md +13 -0
- package/skills/seoul-weather-risk/instruction.md +28 -35
- package/skills/seoul-weather-risk/scripts/seoul_weather_risk.py +15 -68
- package/skills/seoul-weather-risk/skill.json +2 -2
- package/skills/srt-booking/instruction.md +48 -154
- package/skills/srt-booking/references/AUTOMATION-LEGAL-STATEMENT.md +25 -0
- package/skills/srt-booking/references/DISCLAIMER.md +13 -0
- package/skills/srt-booking/scripts/srt_booking.py +134 -235
- package/skills/srt-booking/scripts/srt_booking.py.lock +207 -0
- package/skills/srt-booking/skill.json +3 -15
- package/skills/store-longevity-radar/scripts/__pycache__/store_longevity_download.cpython-312.pyc +0 -0
- package/skills/ticket-availability/references/DISCLAIMER.md +13 -0
- package/skills/{toss-securities → toss-investment}/instruction.md +1 -1
- package/skills/toss-investment/references/DISCLAIMER.md +14 -0
- package/skills/{toss-securities → toss-investment}/references/TRADEMARK-LEGAL-STATEMENT.md +1 -1
- package/skills/{toss-securities → toss-investment}/skill.json +2 -2
- package/src/assemble.js +1 -1
- package/src/execute.js +13 -3
- package/skills/lovebug-report/instruction.md +0 -185
- package/skills/lovebug-report/skill.json +0 -9
- package/skills/srt-booking/scripts/srt_seats.py +0 -156
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## What this skill does
|
|
4
4
|
|
|
5
|
-
서울 행정동 이름을 정규 `place_id`로 해석해 ASK 서울의 장소별 기상 위험 예상 시간대 단일 제품(`weather_place_risk_window`)을 읽기 전용으로 탐색한다. 기본 helper는 hosted `k-skill-proxy
|
|
5
|
+
서울 행정동 이름을 정규 `place_id`로 해석해 ASK 서울의 장소별 기상 위험 예상 시간대 단일 제품(`weather_place_risk_window`)을 읽기 전용으로 탐색한다. 기본 helper는 hosted `k-skill-proxy`만 호출하며, 사용자 API Key나 현재 작업 폴더의 `.env`를 읽지 않는다. 실패 또는 미준비 상태를 fixture나 추정값으로 대체하지 않는다.
|
|
6
6
|
|
|
7
7
|
## Product
|
|
8
8
|
|
|
@@ -26,58 +26,52 @@
|
|
|
26
26
|
|
|
27
27
|
## Workflow
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
### Standard user query (fast path)
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
npx -y @nomadamas/k-skill@0 exec seoul-weather-risk scripts/seoul_weather_risk.py -- preflight
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
2. bundle에서 이 제품의 준비 상태를 확인한다.
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
npx -y @nomadamas/k-skill@0 exec seoul-weather-risk scripts/seoul_weather_risk.py -- catalog
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
3. 제품의 grain, 기본키, 시간축, 공개 column 및 증거 metadata를 확인한다.
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
npx -y @nomadamas/k-skill@0 exec seoul-weather-risk scripts/seoul_weather_risk.py -- describe --product-id weather_place_risk_window
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
4. 행정동 이름과 `1..500` 범위의 limit으로 data page를 조회한다.
|
|
31
|
+
사용자가 오늘 위험 시간대를 묻는 기본 경로는 `query --fast` 한 번만 실행한다. 이 경로는 bundled 행정동 매핑과 날짜·limit 검증을 유지하면서 hosted data route만 한 번 호출하므로 bundle·product metadata 왕복을 생략한다. `--fast`에서는 `--filter`를 사용하지 않고 `--admin-dong`, `--gu`, 날짜, `--limit`, `--cursor`만 사용한다.
|
|
48
32
|
|
|
49
33
|
```bash
|
|
50
|
-
npx -y @nomadamas/k-skill@0 exec seoul-weather-risk scripts/seoul_weather_risk.py -- query \
|
|
34
|
+
npx -y @nomadamas/k-skill@0 exec seoul-weather-risk scripts/seoul_weather_risk.py -- query --fast \
|
|
51
35
|
--product-id weather_place_risk_window \
|
|
52
36
|
--admin-dong 잠실본동 \
|
|
53
|
-
--from 2026-08-
|
|
54
|
-
--to 2026-08-
|
|
37
|
+
--from 2026-08-12 \
|
|
38
|
+
--to 2026-08-12 \
|
|
55
39
|
--limit 100
|
|
56
40
|
```
|
|
57
41
|
|
|
58
|
-
동명이명인 `신사동`은 자치구를 확인한 뒤
|
|
42
|
+
동명이명인 `신사동`은 자치구를 확인한 뒤 fast path에도 `--gu`를 함께 전달한다.
|
|
59
43
|
|
|
60
44
|
```bash
|
|
61
|
-
npx -y @nomadamas/k-skill@0 exec seoul-weather-risk scripts/seoul_weather_risk.py -- query \
|
|
45
|
+
npx -y @nomadamas/k-skill@0 exec seoul-weather-risk scripts/seoul_weather_risk.py -- query --fast \
|
|
62
46
|
--product-id weather_place_risk_window \
|
|
63
47
|
--admin-dong 신사동 \
|
|
64
48
|
--gu 강남구 \
|
|
65
49
|
--limit 100
|
|
66
50
|
```
|
|
67
51
|
|
|
68
|
-
|
|
52
|
+
`--filter`가 필요하거나 게시 계약을 점검해야 할 때는 `--fast`를 빼고 full-contract query를 사용한다. fast query가 `product_not_ready` 또는 계약 오류를 반환하면 fixture나 추정값으로 대체하지 말고 아래 진단 흐름을 수행한다.
|
|
53
|
+
|
|
54
|
+
### Contract diagnostics (only when needed)
|
|
69
55
|
|
|
70
|
-
|
|
56
|
+
1. 환경 설정만 확인한다. 이 명령은 네트워크를 호출하지 않는다.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx -y @nomadamas/k-skill@0 exec seoul-weather-risk scripts/seoul_weather_risk.py -- preflight
|
|
60
|
+
```
|
|
71
61
|
|
|
72
|
-
|
|
62
|
+
2. bundle에서 이 제품의 준비 상태를 확인한다.
|
|
73
63
|
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
64
|
+
```bash
|
|
65
|
+
npx -y @nomadamas/k-skill@0 exec seoul-weather-risk scripts/seoul_weather_risk.py -- catalog
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
3. 제품의 grain, 기본키, 시간축, 공개 column 및 증거 metadata를 확인한다.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npx -y @nomadamas/k-skill@0 exec seoul-weather-risk scripts/seoul_weather_risk.py -- describe --product-id weather_place_risk_window
|
|
78
72
|
```
|
|
79
73
|
|
|
80
|
-
|
|
74
|
+
진단 응답의 `registration_ready`, `publication_id`, `blockers`를 확인한다. fast/full data 응답의 `next_cursor`는 같은 제품의 다음 page에만 그대로 재사용하며 publication이 바뀌면 cursor는 `409`로 만료된다.
|
|
81
75
|
|
|
82
76
|
## Boundaries
|
|
83
77
|
|
|
@@ -85,9 +79,9 @@ MARKETPLACE_API_KEY=<기존 로컬 Marketplace 키>
|
|
|
85
79
|
- 알 수 없는 제품이나 필터를 추측해 보정하지 않는다.
|
|
86
80
|
- 행정동 이름을 fuzzy match하거나 모호한 후보 중 하나로 임의 선택하지 않는다. 생활권·통칭 또는 부분 이름(예: `성수동`)도 행정동으로 추측하지 않는다. helper는 로컬 reference에서 `place_id`를 해석하고 proxy에는 행정동·자치구 문자열을 보내지 않는다.
|
|
87
81
|
- 기본 proxy origin은 `https://k-skill-proxy.nomadamas.org`이다. 별도 self-host proxy를 쓸 때만 `KSKILL_PROXY_BASE_URL`을 HTTPS origin으로 설정한다. 값은 명령행 인수, 문서, 로그에 넣지 않는다.
|
|
88
|
-
- hosted-proxy 모드에서는 사용자 API Key와 `Authorization` 헤더를 사용하지 않는다. ASK Seoul 전용 서비스 키는 proxy 운영 환경에만 두며, Marketplace의 `k-skill-proxy:seoul-weather-risk` principal에 `skill:seoul-weather-risk:read` scope로 등록한다. 이 scope는 bundle·product·data 읽기만 허용하고 다른 Marketplace API를 거부한다.
|
|
82
|
+
- hosted-proxy 모드에서는 사용자 API Key와 `Authorization` 헤더를 사용하지 않는다. ASK Seoul 전용 서비스 키는 proxy 운영 환경에만 두며, Marketplace의 `k-skill-proxy:seoul-weather-risk` principal에 `skill:seoul-weather-risk:read` scope로 등록한다. 이 scope는 bundle·product·data 읽기만 허용하고 다른 Marketplace API를 거부한다. 어떤 모드에서도 키를 출력·로그·skill 파일에 넣지 않는다.
|
|
89
83
|
- proxy는 bundle, 단일 product, 그 data 조회만 노출한다. `table name`, SQL, join, sort, aggregate 및 비허용 query field는 upstream으로 전달하지 않는다.
|
|
90
|
-
- `/
|
|
84
|
+
- `/v1/ask-seoul/weather-risk/bundle`의 제품 집합이 `weather_place_risk_window` 단일 제품과 다르면 응답 계약 오류로 중단한다.
|
|
91
85
|
- live 실패를 fixture나 synthetic 결과로 대체하지 않는다.
|
|
92
86
|
- 이 제품은 예보값 임계치 기반 참고 정보이며 기상청 공식 특보를 대체하지 않는다는 점을 응답에서 명확히 한다.
|
|
93
87
|
|
|
@@ -104,7 +98,6 @@ MARKETPLACE_API_KEY=<기존 로컬 Marketplace 키>
|
|
|
104
98
|
- `ambiguous_admin_dong`: 동명이거나 별칭 후보가 충돌해 `--gu`가 필요함. `details.candidates`에서 가능한 자치구를 확인한다.
|
|
105
99
|
- `location_mapping_invalid`: bundled 행정동 reference의 버전·스키마·행 수 계약 오류
|
|
106
100
|
- `proxy_disabled`, `invalid_proxy_base_url`: proxy 환경 설정 오류
|
|
107
|
-
- `local_direct_not_configured`, `invalid_local_direct_base_url`: local-direct 환경 설정 오류
|
|
108
101
|
- `unauthorized`/`api_key_missing`(401), `forbidden`/`api_key_forbidden`(403), `unknown_product`(404)
|
|
109
102
|
- `cursor_expired`(409), `rate_limited`(429), `product_not_ready`(503)
|
|
110
103
|
- `upstream_not_configured`(503): proxy 운영 환경에 ASK Seoul 전용 서비스 키 또는 origin이 설정되지 않음
|
|
@@ -20,16 +20,7 @@ from urllib.request import HTTPRedirectHandler, Request, build_opener
|
|
|
20
20
|
SKILL_BUNDLE_ID = "seoul-weather-risk"
|
|
21
21
|
PROXY_BASE_URL_ENV = "KSKILL_PROXY_BASE_URL"
|
|
22
22
|
DEFAULT_PROXY_BASE_URL = "https://k-skill-proxy.nomadamas.org"
|
|
23
|
-
LOCAL_DIRECT_ENV = "KSKILL_LOCAL_DIRECT"
|
|
24
|
-
SKILL_API_BASE_URL_ENV = "ASK_SEOUL_SKILL_API_BASE_URL"
|
|
25
|
-
MARKETPLACE_API_KEY_ENV = "MARKETPLACE_API_KEY"
|
|
26
23
|
PROXY_DISABLED_VALUES = frozenset({"off", "false", "0", "disable", "disabled", "none"})
|
|
27
|
-
LOCAL_DIRECT_ENABLED_VALUES = frozenset({"1", "true", "on", "yes"})
|
|
28
|
-
LOCAL_DIRECT_DOTENV_NAMES = frozenset({
|
|
29
|
-
LOCAL_DIRECT_ENV,
|
|
30
|
-
SKILL_API_BASE_URL_ENV,
|
|
31
|
-
MARKETPLACE_API_KEY_ENV,
|
|
32
|
-
})
|
|
33
24
|
PROXY_ROUTE_ROOT = "/v1/ask-seoul/weather-risk"
|
|
34
25
|
LOCATION_MAPPING_PATH = pathlib.Path(__file__).resolve().parents[1] / "references" / "admin-dong-place-map.json"
|
|
35
26
|
LOCATION_MAPPING_VERSION = "kma_admin_dong_grid_20260325"
|
|
@@ -65,54 +56,14 @@ class _NoRedirect(HTTPRedirectHandler):
|
|
|
65
56
|
@dataclass(frozen=True)
|
|
66
57
|
class ApiConfig:
|
|
67
58
|
base_url: str
|
|
68
|
-
mode: str = "hosted_proxy"
|
|
69
|
-
bearer_token: str | None = None
|
|
70
59
|
|
|
71
60
|
|
|
72
61
|
def _json(value: Any) -> str:
|
|
73
62
|
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
74
63
|
|
|
75
64
|
|
|
76
|
-
def _local_direct_config(values: dict[str, str]) -> ApiConfig:
|
|
77
|
-
base_url = values.get(SKILL_API_BASE_URL_ENV, "").strip()
|
|
78
|
-
bearer_token = values.get(MARKETPLACE_API_KEY_ENV, "").strip()
|
|
79
|
-
if not base_url or base_url == "replace-me":
|
|
80
|
-
raise SkillError("local_direct_not_configured", "local direct API base URL이 필요합니다.")
|
|
81
|
-
if not bearer_token or bearer_token == "replace-me":
|
|
82
|
-
raise SkillError("local_direct_not_configured", "local direct Marketplace API key가 필요합니다.")
|
|
83
|
-
|
|
84
|
-
parsed = urlparse(base_url)
|
|
85
|
-
local_http = parsed.scheme == "http" and parsed.hostname in {"127.0.0.1", "localhost", "::1"}
|
|
86
|
-
if (parsed.scheme != "https" and not local_http) or not parsed.netloc or parsed.path not in {"", "/"} or parsed.query or parsed.fragment:
|
|
87
|
-
raise SkillError("invalid_local_direct_base_url", "local direct API base URL은 HTTPS origin이어야 합니다.")
|
|
88
|
-
if parsed.username or parsed.password:
|
|
89
|
-
raise SkillError("invalid_local_direct_base_url", "local direct API base URL에 사용자 정보는 포함할 수 없습니다.")
|
|
90
|
-
return ApiConfig(base_url=base_url.rstrip("/"), mode="local_direct", bearer_token=bearer_token)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def _current_directory_dotenv() -> dict[str, str]:
|
|
94
|
-
path = pathlib.Path.cwd() / ".env"
|
|
95
|
-
try:
|
|
96
|
-
lines = path.read_text(encoding="utf-8").splitlines()
|
|
97
|
-
except OSError:
|
|
98
|
-
return {}
|
|
99
|
-
|
|
100
|
-
values: dict[str, str] = {}
|
|
101
|
-
for line in lines:
|
|
102
|
-
stripped = line.strip()
|
|
103
|
-
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
|
104
|
-
continue
|
|
105
|
-
name, value = stripped.split("=", 1)
|
|
106
|
-
name = name.strip()
|
|
107
|
-
if name in LOCAL_DIRECT_DOTENV_NAMES:
|
|
108
|
-
values[name] = value.strip().strip('"').strip("'")
|
|
109
|
-
return values
|
|
110
|
-
|
|
111
|
-
|
|
112
65
|
def _api_config(environ: dict[str, str] | None = None) -> ApiConfig:
|
|
113
|
-
values =
|
|
114
|
-
if values.get(LOCAL_DIRECT_ENV, "").strip().casefold() in LOCAL_DIRECT_ENABLED_VALUES:
|
|
115
|
-
return _local_direct_config(values)
|
|
66
|
+
values = os.environ if environ is None else environ
|
|
116
67
|
configured = values.get(PROXY_BASE_URL_ENV, "").strip()
|
|
117
68
|
if configured.casefold() in PROXY_DISABLED_VALUES:
|
|
118
69
|
raise SkillError("proxy_disabled", f"{PROXY_BASE_URL_ENV}가 비활성화되어 있습니다.")
|
|
@@ -159,8 +110,6 @@ def _request_json(config: ApiConfig, path: str, query: dict[str, str] | None = N
|
|
|
159
110
|
"Accept": "application/json",
|
|
160
111
|
"User-Agent": "k-skill-seoul-weather-risk/1",
|
|
161
112
|
}
|
|
162
|
-
if config.bearer_token:
|
|
163
|
-
headers["Authorization"] = f"Bearer {config.bearer_token}"
|
|
164
113
|
request = Request(url, headers=headers)
|
|
165
114
|
try:
|
|
166
115
|
with build_opener(_NoRedirect).open(request, timeout=15) as response:
|
|
@@ -399,22 +348,16 @@ def _validate_product_id(product_id: str) -> None:
|
|
|
399
348
|
|
|
400
349
|
|
|
401
350
|
def _bundle(config: ApiConfig) -> dict[str, Any]:
|
|
402
|
-
if config.mode == "local_direct":
|
|
403
|
-
return _validate_bundle(_request_json(config, f"/skill/v1/bundles/{SKILL_BUNDLE_ID}"))
|
|
404
351
|
return _validate_bundle(_request_json(config, f"{PROXY_ROUTE_ROOT}/bundle"))
|
|
405
352
|
|
|
406
353
|
|
|
407
354
|
def _detail(config: ApiConfig, product_id: str) -> dict[str, Any]:
|
|
408
355
|
_validate_product_id(product_id)
|
|
409
|
-
if config.mode == "local_direct":
|
|
410
|
-
return _validate_product(_request_json(config, f"/skill/v1/products/{product_id}"), product_id)
|
|
411
356
|
return _validate_product(_request_json(config, f"{PROXY_ROUTE_ROOT}/product"), product_id)
|
|
412
357
|
|
|
413
358
|
|
|
414
359
|
def _data(config: ApiConfig, product_id: str, query: dict[str, str], limit: int) -> dict[str, Any]:
|
|
415
360
|
_validate_product_id(product_id)
|
|
416
|
-
if config.mode == "local_direct":
|
|
417
|
-
return _validate_data(_request_json(config, f"/skill/v1/products/{product_id}/data", query), product_id, limit)
|
|
418
361
|
return _validate_data(_request_json(config, f"{PROXY_ROUTE_ROOT}/data", query), product_id, limit)
|
|
419
362
|
|
|
420
363
|
|
|
@@ -428,6 +371,7 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
428
371
|
describe.add_argument("--product-id", required=True)
|
|
429
372
|
|
|
430
373
|
query = commands.add_parser("query", help="제품 data page 조회")
|
|
374
|
+
query.add_argument("--fast", action="store_true", help="bundle/product metadata 확인을 생략하고 data만 조회")
|
|
431
375
|
query.add_argument("--product-id", required=True)
|
|
432
376
|
query.add_argument("--filter", action="append", default=[], metavar="COLUMN=VALUE")
|
|
433
377
|
query.add_argument("--admin-dong", help="서울 행정동 이름")
|
|
@@ -446,11 +390,9 @@ def run(argv: list[str]) -> int:
|
|
|
446
390
|
if args.command == "preflight":
|
|
447
391
|
result = {
|
|
448
392
|
"status": "ok",
|
|
449
|
-
"mode":
|
|
393
|
+
"mode": "hosted_proxy",
|
|
450
394
|
"live_network": False,
|
|
451
|
-
"
|
|
452
|
-
"proxy_base_url_configured": config.mode == "hosted_proxy",
|
|
453
|
-
"local_direct_base_url_configured": config.mode == "local_direct",
|
|
395
|
+
"proxy_base_url_configured": True,
|
|
454
396
|
}
|
|
455
397
|
elif args.command == "catalog":
|
|
456
398
|
result = _bundle(config)
|
|
@@ -460,8 +402,12 @@ def run(argv: list[str]) -> int:
|
|
|
460
402
|
else:
|
|
461
403
|
if not 1 <= args.limit <= 500:
|
|
462
404
|
raise SkillError("invalid_limit", "limit은 1부터 500 사이여야 합니다.")
|
|
463
|
-
|
|
464
|
-
|
|
405
|
+
if args.fast and args.filter:
|
|
406
|
+
raise SkillError("fast_query_filter_unsupported", "--fast에서는 --filter를 사용할 수 없습니다.")
|
|
407
|
+
detail: dict[str, Any] | None = None
|
|
408
|
+
if not args.fast:
|
|
409
|
+
_bundle(config)
|
|
410
|
+
detail = _detail(config, args.product_id)
|
|
465
411
|
filters = _filters(args.filter)
|
|
466
412
|
if args.gu is not None and args.admin_dong is None:
|
|
467
413
|
raise SkillError("invalid_location_input", "--gu는 --admin-dong과 함께 사용해야 합니다.")
|
|
@@ -477,10 +423,11 @@ def run(argv: list[str]) -> int:
|
|
|
477
423
|
)
|
|
478
424
|
resolved = _resolve_admin_dong(args.admin_dong, args.gu)
|
|
479
425
|
filters["place_id"] = resolved["place_id"]
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
426
|
+
if detail is not None:
|
|
427
|
+
allowed_columns = {column["name"] for column in detail["metadata"].get("columns", [])}
|
|
428
|
+
unknown = sorted(set(filters) - allowed_columns)
|
|
429
|
+
if unknown:
|
|
430
|
+
raise SkillError("unknown_filter", f"공개 projection에 없는 필터입니다: {', '.join(unknown)}")
|
|
484
431
|
request_query = {**filters, "limit": str(args.limit)}
|
|
485
432
|
if args.from_value is not None:
|
|
486
433
|
request_query["from"] = _time_bound(args.from_value, "from")
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "seoul-weather-risk",
|
|
3
|
-
"description": "서울 행정동 이름을 정식명 우선·허용된 결정적 표기 별칭만으로 정규 place_id로 해석해 ASK 서울의 장소별 기상 위험 예상 시간대(weather_place_risk_window) 단일 제품을 hosted k-skill proxy에서 읽기 전용 조회한다.
|
|
3
|
+
"description": "서울 행정동 이름을 정식명 우선·허용된 결정적 표기 별칭만으로 정규 place_id로 해석해 ASK 서울의 장소별 기상 위험 예상 시간대(weather_place_risk_window) 단일 제품을 hosted k-skill proxy에서 읽기 전용 조회한다. 사용자 API Key와 place_id 입력은 필요하지 않다.",
|
|
4
4
|
"profiles": [
|
|
5
5
|
"proxy",
|
|
6
6
|
"lookup"
|
|
7
7
|
],
|
|
8
|
-
"frontmatter": "name: seoul-weather-risk\ndescription: 서울 행정동 이름을 정식명 우선·허용된 결정적 표기 별칭만으로 정규 place_id로 해석해 ASK 서울의 장소별 기상 위험 예상 시간대(weather_place_risk_window) 단일 제품을 hosted k-skill proxy에서 읽기 전용 조회한다.
|
|
8
|
+
"frontmatter": "name: seoul-weather-risk\ndescription: 서울 행정동 이름을 정식명 우선·허용된 결정적 표기 별칭만으로 정규 place_id로 해석해 ASK 서울의 장소별 기상 위험 예상 시간대(weather_place_risk_window) 단일 제품을 hosted k-skill proxy에서 읽기 전용 조회한다. 사용자 API Key와 place_id 입력은 필요하지 않다.\nlicense: MIT\nmetadata:\n category: public-data\n locale: ko-KR\n phase: live-client"
|
|
9
9
|
}
|
|
@@ -1,181 +1,75 @@
|
|
|
1
|
-
# SRT
|
|
1
|
+
# SRT Live Timetable Lookup
|
|
2
2
|
|
|
3
3
|
## What this skill does
|
|
4
4
|
|
|
5
|
-
`SRTrain
|
|
5
|
+
`SRTrain`의 시간표 검색 경로를 이용해 현재 SRT 운행 후보와 일반실·특실 예약 가능 여부를 조회한다.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
이 스킬은 회원 로그인 불필요한 **라이브 조회 전용**이다.
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
- "예약 내역 확인해줘"
|
|
15
|
-
- "이 SRT 예약 취소해줘"
|
|
9
|
+
- `auto_login=False` 익명 client를 사용한다.
|
|
10
|
+
- NetFunnel 대기열과 시간표 검색 요청만 실행한다.
|
|
11
|
+
- 예약, 예약대기, 좌석 선점, 결제, 취소, 자동 재조회는 실행하지 않는다.
|
|
12
|
+
- 정확한 호차·좌석번호를 조회하지 않는다.
|
|
13
|
+
- 구매는 공식 SRT 페이지에서 사용자가 직접 진행한다.
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
실행 환경에는 Python 3.11 이상과 `uv`가 필요하다. `uv`가 없으면 공식 설치 문서를 안내하고 조회를 실행하지 않는다.
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
- 비밀번호를 채팅창에 직접 보내려는 경우
|
|
21
|
-
- SRT가 아니라 KTX/Korail 예매인 경우
|
|
22
|
-
|
|
23
|
-
## Prerequisites
|
|
24
|
-
|
|
25
|
-
- Python 3.10+
|
|
26
|
-
- `python3 -m pip install SRTrain`
|
|
27
|
-
|
|
28
|
-
## Required environment variables
|
|
29
|
-
|
|
30
|
-
- `KSKILL_SRT_ID`
|
|
31
|
-
- `KSKILL_SRT_PASSWORD`
|
|
32
|
-
|
|
33
|
-
### Credential handling
|
|
34
|
-
|
|
35
|
-
- 돌쇠 credential mode에서는 `vault-run` capability를 사용하고, 없으면 `request_vault_credential`을 호출한다. ID/PW 원문을 채팅이나 shell에 넣지 않는다.
|
|
36
|
-
- 그 밖의 환경에서는 이미 주입된 환경변수 → host vault → `~/.config/k-skill/secrets.env` 순서로 사용한다.
|
|
37
|
-
|
|
38
|
-
## Inputs
|
|
39
|
-
|
|
40
|
-
- 출발역
|
|
41
|
-
- 도착역
|
|
42
|
-
- 날짜: `YYYYMMDD`
|
|
43
|
-
- 희망 시작 시각: `HHMMSS`
|
|
44
|
-
- 인원 수와 승객 유형
|
|
45
|
-
- 좌석 선호: 일반실 / 특실
|
|
46
|
-
- 좌석 상세 조건: 객실 등급, 호차 번호, 좌석 번호, 빈 좌석만 보기, 탐색 우선순위
|
|
47
|
-
|
|
48
|
-
## Workflow
|
|
49
|
-
|
|
50
|
-
### 0. Install the package globally when missing
|
|
51
|
-
|
|
52
|
-
`python3 -c 'import SRT'` 가 실패하면 다른 구현으로 우회하지 말고 전역 Python 패키지 설치를 먼저 시도한다.
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
python3 -m pip install SRTrain
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### 1. Ensure credentials are available
|
|
59
|
-
|
|
60
|
-
돌쇠 credential mode에서는 `vault-run`의 SRT capability를 확인하고, 없으면 `request_vault_credential`로 앱 vault 입력 UI를 호출한다. generic fallback에서만 `KSKILL_SRT_ID`, `KSKILL_SRT_PASSWORD` 환경변수를 확인한다.
|
|
61
|
-
|
|
62
|
-
시크릿이 없다는 이유로 웹사이트를 직접 긁거나 다른 비공식 경로를 찾지 않는다.
|
|
63
|
-
|
|
64
|
-
### 2. Search first
|
|
65
|
-
|
|
66
|
-
먼저 helper 로 조회해서 후보를 요약한다.
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
npx -y @nomadamas/k-skill@0 exec srt-booking scripts/srt_booking.py -- search 수서 부산 20260328 080000 --time-limit 120000 --limit 5
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### 3. Summarize options before side effects
|
|
73
|
-
|
|
74
|
-
예약 전에는 항상 아래를 짧게 정리한다.
|
|
75
|
-
|
|
76
|
-
- 출발/도착 시각
|
|
77
|
-
- 일반실/특실 가능 여부
|
|
78
|
-
- 예상 운임
|
|
79
|
-
|
|
80
|
-
### 4. Inspect detailed seats when the user asks for seat numbers
|
|
81
|
-
|
|
82
|
-
`search` 의 좌석 가능 여부는 열차 단위 플래그다. 사용자가 "남은 좌석 번호", "호차별 좌석", "특정 좌석", "창측/순방향 자리", "예약 전에 자리 확인"처럼 구체적인 좌석을 물으면 예약 전에 `seats` 를 호출한다.
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
npx -y @nomadamas/k-skill@0 exec srt-booking scripts/srt_booking.py -- seats 수서 부산 20260328 080000 --train-id <train_id>
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
특정 호차의 빈 좌석만 확인하려면 `--car-no` 와 `--available-only` 를 쓴다.
|
|
17
|
+
## Commands
|
|
89
18
|
|
|
90
19
|
```bash
|
|
91
|
-
npx -y @nomadamas/k-skill@0 exec srt-booking scripts/srt_booking.py --
|
|
20
|
+
npx -y @nomadamas/k-skill@0 exec srt-booking scripts/srt_booking.py -- \
|
|
21
|
+
search \
|
|
22
|
+
--dep 수서 \
|
|
23
|
+
--arr 부산 \
|
|
24
|
+
--date 20260819 \
|
|
25
|
+
--time 0600 \
|
|
26
|
+
--time-limit 1200 \
|
|
27
|
+
--limit 5
|
|
92
28
|
```
|
|
93
29
|
|
|
94
|
-
|
|
30
|
+
현재 라이브 조회 endpoint와 안전 경계:
|
|
95
31
|
|
|
96
32
|
```bash
|
|
97
|
-
npx -y @nomadamas/k-skill@0 exec srt-booking scripts/srt_booking.py --
|
|
33
|
+
npx -y @nomadamas/k-skill@0 exec srt-booking scripts/srt_booking.py -- source
|
|
98
34
|
```
|
|
99
35
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
상세 좌석 응답을 보여줄 때는 아래를 우선 요약한다.
|
|
103
|
-
|
|
104
|
-
- 호차별 `available_seat_count`
|
|
105
|
-
- 남은 좌석 번호 (`available_seats`)
|
|
106
|
-
- 좌석별 `direction`, `position`
|
|
107
|
-
- 특정 좌석 요청이면 `requested_seat_available`
|
|
108
|
-
|
|
109
|
-
이 기능은 좌석을 선택/선점하지 않는다. 실제 예약은 다음 단계에서만 진행한다.
|
|
110
|
-
|
|
111
|
-
### 5. Reserve only after the train is fixed
|
|
36
|
+
출력:
|
|
112
37
|
|
|
113
|
-
|
|
38
|
+
- 열차번호·열차종류
|
|
39
|
+
- 출발역·도착역
|
|
40
|
+
- 현재 출발·도착 시각
|
|
41
|
+
- 일반실·특실 예약 가능 여부
|
|
42
|
+
- 사용한 라이브 search endpoint
|
|
43
|
+
- 공식 SRT 페이지
|
|
114
44
|
|
|
115
|
-
|
|
116
|
-
python3 - <<'PY'
|
|
117
|
-
import os
|
|
118
|
-
from SRT import Adult, SRT, SeatType
|
|
119
|
-
|
|
120
|
-
srt = SRT(os.environ["KSKILL_SRT_ID"], os.environ["KSKILL_SRT_PASSWORD"])
|
|
121
|
-
trains = srt.search_train("수서", "부산", "20260328", "080000", time_limit="120000")
|
|
122
|
-
reservation = srt.reserve(
|
|
123
|
-
trains[0],
|
|
124
|
-
passengers=[Adult(1)],
|
|
125
|
-
special_seat=SeatType.GENERAL_FIRST,
|
|
126
|
-
)
|
|
127
|
-
print(reservation)
|
|
128
|
-
PY
|
|
129
|
-
```
|
|
45
|
+
## Workflow
|
|
130
46
|
|
|
131
|
-
|
|
47
|
+
1. 출발역, 도착역, 날짜, 시간대를 확인한다.
|
|
48
|
+
2. `search`를 한 번 실행한다. `수서역`처럼 `역`이 붙은 입력은 helper가 표준 역명으로 정규화한다.
|
|
49
|
+
3. 현재 운행 후보를 제시한다.
|
|
50
|
+
4. 좌석 구매가 필요하면 `booking_url`을 제공하고 종료한다.
|
|
51
|
+
5. 사용자가 다시 요청하지 않는 한 polling·매진 감시를 시작하지 않는다.
|
|
132
52
|
|
|
133
|
-
|
|
53
|
+
## Hard boundaries
|
|
134
54
|
|
|
135
|
-
|
|
55
|
+
- `KSKILL_SRT_ID`, `KSKILL_SRT_PASSWORD` 또는 회원 로그인을 요구하지 않는다.
|
|
56
|
+
- helper에 `reserve`, `reservations`, `cancel`, `payment`, waiting-list 명령을 추가하지 않는다.
|
|
57
|
+
- 예약 endpoint, 결제 endpoint 또는 계정 endpoint를 호출하지 않는다.
|
|
58
|
+
- NetFunnel key를 예약·선점 자동화에 사용하지 않는다.
|
|
59
|
+
- CAPTCHA·접근 거부·계정 제한이 발생하면 즉시 중단한다.
|
|
60
|
+
- 사용자 요청 한 번을 반복 수집이나 장기 실행으로 확장하지 않는다.
|
|
136
61
|
|
|
137
|
-
|
|
138
|
-
2. vault-backed login을 사용하고 결제수단/할인/승객/예약 정보를 확인한다.
|
|
139
|
-
3. 실제 결제 버튼 직전에 `clarify`로 열차, 날짜·시각, 승객, 좌석 등급, 총액을 보여주고 승인받는다.
|
|
140
|
-
4. 승인되면 결제를 실행하고 결제 완료 화면, 예약번호, 영수증/결제 상태를 확인한다.
|
|
62
|
+
## Failure modes
|
|
141
63
|
|
|
142
|
-
|
|
64
|
+
- NetFunnel 대기 또는 SRT 접근 제한
|
|
65
|
+
- `SRTrain` 설치·호환성 문제
|
|
66
|
+
- 날짜·시간 또는 역명 오류
|
|
67
|
+
- 조건에 맞는 열차 없음
|
|
143
68
|
|
|
144
|
-
|
|
69
|
+
이 경우 차단을 우회하지 않고 [SRT 공식 조회 페이지](https://etk.srail.kr/hpg/hra/01/selectScheduleList.do?pageId=TK0101010000)를 안내한다.
|
|
145
70
|
|
|
146
|
-
|
|
71
|
+
## Legal notice
|
|
147
72
|
|
|
148
73
|
```bash
|
|
149
|
-
|
|
150
|
-
import os
|
|
151
|
-
from SRT import SRT
|
|
152
|
-
|
|
153
|
-
srt = SRT(os.environ["KSKILL_SRT_ID"], os.environ["KSKILL_SRT_PASSWORD"])
|
|
154
|
-
reservations = srt.get_reservations()
|
|
155
|
-
print(reservations)
|
|
156
|
-
PY
|
|
74
|
+
npx -y @nomadamas/k-skill@0 read srt-booking references/AUTOMATION-LEGAL-STATEMENT.md
|
|
157
75
|
```
|
|
158
|
-
|
|
159
|
-
취소 실행 직전에 `clarify`로 예약번호, 열차, 날짜·시각, 승객, 환불/위약금 정보를 확인하고 승인받는다.
|
|
160
|
-
|
|
161
|
-
## Done when
|
|
162
|
-
|
|
163
|
-
- 조회 요청이면 후보 열차가 정리되어 있다
|
|
164
|
-
- 좌석 상세 확인이면 호차별 남은 좌석번호나 특정 좌석 공석 여부가 정리되어 있다
|
|
165
|
-
- 예약 요청이면 예약 결과, 운임, 구입기한이 확인되어 좌석 확보 완료를 안내했다
|
|
166
|
-
- 돌쇠의 예매 완료 요청이면 `clarify` 승인 후 결제 완료 상태와 영수증/예약번호를 확인했다
|
|
167
|
-
- 취소 요청이면 어떤 예약을 취소했는지 명확하다
|
|
168
|
-
|
|
169
|
-
## Failure modes
|
|
170
|
-
|
|
171
|
-
- 로그인 오류: 계정 정보나 SRT site policy 변경 가능성 확인
|
|
172
|
-
- 매진: 다른 시간대나 좌석 타입으로 재조회
|
|
173
|
-
- 좌석선택 페이지 형식 변경: helper 파서 업데이트 필요
|
|
174
|
-
- 네트워크 오류: 짧게 재시도하되 aggressive polling은 피하기
|
|
175
|
-
|
|
176
|
-
## Notes
|
|
177
|
-
|
|
178
|
-
- 상세 좌석 확인은 SRT 웹 좌석선택 페이지를 조회 전용으로 읽는다
|
|
179
|
-
- `SRTrain`은 SRT 전용 라이브러리라서 스킬 의도가 더 선명하다
|
|
180
|
-
- 결제 자동화 금지는 generic fallback에만 적용한다. 돌쇠에서는 `clarify` 승인 후 공식 결제 표면으로 완료한다
|
|
181
|
-
- 자동 재시도 루프는 계정 보호 차원에서 짧고 보수적으로 유지한다
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# SRT 라이브 자동조회 관련 고지
|
|
2
|
+
|
|
3
|
+
> 2026-08-12에 확인한 공개 자료를 바탕으로 한 일반적인 위험 안내이며 법률자문이나 주식회사 에스알의 유권해석이 아니다.
|
|
4
|
+
|
|
5
|
+
## 결론
|
|
6
|
+
|
|
7
|
+
이 스킬은 로그인 없이 `SRTrain`의 시간표 검색 endpoint만 호출하고 예약·결제·취소·좌석 선점을 제공하지 않는다. 따라서 기존 자동예매보다 위험은 낮다.
|
|
8
|
+
|
|
9
|
+
그러나 에스알은 공식 공지에서 매크로 프로그램을 이용한 **승차권 예매**를 금지한다. 조회 전용이라는 사실이 자동 접근에 대한 에스알의 허가를 뜻하지는 않는다.
|
|
10
|
+
|
|
11
|
+
## 기술적 경계
|
|
12
|
+
|
|
13
|
+
- `SRT("", "", auto_login=False)` 익명 client
|
|
14
|
+
- NetFunnel 대기열과 `selectListAra10007_n.do` 시간표 검색만 실행
|
|
15
|
+
- 일반실·특실 가능 여부만 조회
|
|
16
|
+
- 예약·예약대기·결제·취소·계정 endpoint 없음
|
|
17
|
+
- 한 사용자 요청에 한 번만 실행하고 polling 없음
|
|
18
|
+
|
|
19
|
+
## 공식 근거
|
|
20
|
+
|
|
21
|
+
[매크로 프로그램을 이용한 승차권 예매 금지 안내](https://etk.srail.kr/cms/article/view.do?pageId=TK0502000000&postNo=793)는 매크로를 이용한 예매가 시스템 과부하와 다른 고객의 예매 기회 방해를 초래할 수 있고 적발 시 회원 탈퇴 처리될 수 있다고 안내한다.
|
|
22
|
+
|
|
23
|
+
[에스알 회원가입 및 이용에 관한 약관](https://etk.srail.kr/cms/archive.do?pageId=TK0503010000) 제10조는 다량 예약 후 임박 취소, 타인 계정 도용과 건전한 서비스 이용 저해를 탈퇴 사유로 규정한다.
|
|
24
|
+
|
|
25
|
+
NetFunnel 오류, 접근 거부, CAPTCHA 또는 정책 변경이 발생하면 우회하지 않고 즉시 중단한다.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# DISCLAIMER — `srt-booking`
|
|
2
|
+
|
|
3
|
+
이 스킬은 에스알·SRT 또는 철도 운영사의 공식 기능 또는 공식 지원 도구가 아니며, 공식 제휴·후원·승인·인증 또는 협업한 사실이 전혀 없습니다. 상표와 서비스명은 열차·좌석 조회 기능과 대상을 설명하기 위해서만 사용합니다.
|
|
4
|
+
|
|
5
|
+
대법원 2005. 6. 10. 선고 [2005도1637 판결](https://www.law.go.kr/LSW/precInfoP.do?precSeq=83920)(소니용 리모컨 사건)은 기능 설명용 표장과 출처표시를 구별했습니다. [상표법 제2조](https://www.law.go.kr/법령/상표법/제2조), [제89조](https://www.law.go.kr/법령/상표법/제89조), [제90조](https://www.law.go.kr/법령/상표법/제90조), [제108조](https://www.law.go.kr/법령/상표법/제108조)에 따른 출처 혼동 판단은 별도입니다.
|
|
6
|
+
|
|
7
|
+
대법원 2022. 5. 12. 선고 [2021도1533 판결](https://www.law.go.kr/LSW/precInfoP.do?precSeq=221765)은 공개정보 수집만으로 곧바로 [정보통신망법 제48조](https://www.law.go.kr/법령/정보통신망이용촉진및정보보호등에관한법률/제48조) 위반이 되는 것은 아닌 사정을 제시했지만, 철도 예매 자동화·접근통제 우회·반복 선점을 허용하지 않습니다. [저작권법 제93조](https://www.law.go.kr/법령/저작권법/제93조)과 [형법 제314조 제2항](https://www.law.go.kr/법령/형법/제314조)은 별도입니다.
|
|
8
|
+
|
|
9
|
+
- 공개 운행·좌석정보 자동 수집은 반드시 개인의 정보 조회용으로만 사용합니다.
|
|
10
|
+
- 매진 감시, 조직적·대량 크롤링, 반복 선점·예약·취소, 공격적 polling을 하지 않습니다.
|
|
11
|
+
- CAPTCHA·anti-bot·로그인·접근통제·rate limit·IP 차단을 우회하거나 정상 예매 영업을 방해하지 않습니다.
|
|
12
|
+
|
|
13
|
+
이 문서는 적법성을 보증하는 법률 자문이 아닙니다.
|