shopping-cli 3.0.2__tar.gz → 3.1.0__tar.gz
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.
- {shopping_cli-3.0.2/shopping_cli.egg-info → shopping_cli-3.1.0}/PKG-INFO +1 -1
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/pyproject.toml +1 -1
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/__init__.py +1 -1
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/agents/merchant_daemon.py +5 -1
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/fastapi_routes.py +13 -4
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/handlers/catalog.py +4 -1
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/handlers/listings_projection.py +25 -3
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/route_table.py +8 -6
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/core/catalog.py +25 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/core/catalog_views.py +5 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/db/migrations.py +58 -1
- {shopping_cli-3.0.2 → shopping_cli-3.1.0/shopping_cli.egg-info}/PKG-INFO +1 -1
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/LICENSE +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/MANIFEST.in +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/README.md +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/setup.cfg +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/agents/__init__.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/agents/agent_cli.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/agents/buyer_cli.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/agents/merchant_agent.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/agents/merchant_state_helpers.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/agents/tools.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/__init__.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/app.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/auth.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/error_handlers.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/error_response.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/fallback_asgi.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/handlers/__init__.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/handlers/agents.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/handlers/audit.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/handlers/buyer.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/handlers/common.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/handlers/conversations.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/handlers/erp.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/handlers/human_review.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/handlers/negotiation.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/idempotency.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/limits.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/request_dispatch.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/request_limits.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/route_matching.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/route_registry.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/routes_agents.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/routes_conversations.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/routes_marketplace.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/routes_merchants.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/api/server.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/cli.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/cli_agent_commands.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/cli_buyer_commands.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/cli_catalog_commands.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/cli_common.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/cli_conversation_commands.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/cli_erp_commands.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/cli_listing_commands.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/commerce/__init__.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/commerce/adapters.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/commerce/data_source.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/config.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/contracts/kiwi-contracts.lock.json +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/contracts/shopping.negotiation/0.1/capabilities.schema.json +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/contracts/shopping.negotiation/0.1/decision.schema.json +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/contracts/shopping.negotiation/0.1/policy-result.schema.json +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/contracts/shopping.negotiation/0.1/snapshot.schema.json +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/core/__init__.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/core/catalog_scoring.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/core/catalog_text.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/core/channels.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/core/conversations.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/core/delivery.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/core/errors.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/core/harness.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/core/limits.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/core/negotiation.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/core/negotiation_contracts.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/core/policies.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/core/risk.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/core/tokens.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/data_sources/__init__.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/data_sources/erp_source.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/db/__init__.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/db/models.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/db/provenance.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/db/session.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/http_client.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/listings/__init__.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/listings/projection.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/services/__init__.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/services/agents.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/services/audit.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/services/buyer_bootstrap.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/services/conversations.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/services/human_review.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/services/negotiation.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/services/negotiation_gates.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/services/negotiation_message.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/services/negotiation_policy_helpers.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/services/negotiation_policy_result.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/services/negotiation_snapshot.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/services/negotiation_snapshot_projection.py +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/services/tokens.py +1 -1
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli.egg-info/SOURCES.txt +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli.egg-info/dependency_links.txt +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli.egg-info/entry_points.txt +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli.egg-info/requires.txt +0 -0
- {shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli.egg-info/top_level.txt +0 -0
|
@@ -180,11 +180,15 @@ class _PidFileLock:
|
|
|
180
180
|
|
|
181
181
|
def __enter__(self) -> "_PidFileLock":
|
|
182
182
|
self.lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
183
|
+
# 审查 P3-03:与日志/stop 文件对齐 0600(此前 0644);os.open 的
|
|
184
|
+
# mode 只在新建时生效,已存在的遗留文件也显式 chmod(同
|
|
185
|
+
# _open_log_append 的 P2-07 模式)。
|
|
183
186
|
self._fd = os.open(
|
|
184
187
|
str(self.lock_path),
|
|
185
188
|
os.O_RDWR | os.O_CREAT,
|
|
186
|
-
|
|
189
|
+
0o600,
|
|
187
190
|
)
|
|
191
|
+
os.chmod(self.lock_path, 0o600)
|
|
188
192
|
if fcntl is not None:
|
|
189
193
|
try:
|
|
190
194
|
fcntl.flock(self._fd, fcntl.LOCK_EX)
|
|
@@ -226,12 +226,21 @@ def register_fastapi_routes(app: Any, db_path: str | Path) -> None:
|
|
|
226
226
|
return _update_product(db_path, sku, api_auth.payload_with_auth(payload, authorization))
|
|
227
227
|
|
|
228
228
|
@app.get("/v1/merchant/listings/projections")
|
|
229
|
-
def merchant_listing_projections(
|
|
230
|
-
|
|
229
|
+
def merchant_listing_projections(
|
|
230
|
+
merchant_id: str = "",
|
|
231
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
232
|
+
) -> dict[str, Any]:
|
|
233
|
+
# 审查 P2-B:handoff_destination 仅向所属商户本人开放(owner 判定在 handler)
|
|
234
|
+
return _list_listing_projections(
|
|
235
|
+
db_path, {"merchant_id": merchant_id}, api_auth.payload_with_auth({}, authorization)
|
|
236
|
+
)
|
|
231
237
|
|
|
232
238
|
@app.get("/v1/merchant/listings/{sku}/projection")
|
|
233
|
-
def merchant_listing_projection(
|
|
234
|
-
|
|
239
|
+
def merchant_listing_projection(
|
|
240
|
+
sku: str,
|
|
241
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
242
|
+
) -> dict[str, Any]:
|
|
243
|
+
return _get_listing_projection(db_path, sku, api_auth.payload_with_auth({}, authorization))
|
|
235
244
|
|
|
236
245
|
@app.post("/v1/merchant/erp/sync")
|
|
237
246
|
def merchant_erp_sync(
|
|
@@ -29,7 +29,6 @@ from .common import (
|
|
|
29
29
|
)
|
|
30
30
|
from shopping_cli.core import catalog_views
|
|
31
31
|
|
|
32
|
-
|
|
33
32
|
def health(db_path: str | Path) -> dict[str, Any]:
|
|
34
33
|
profile = deployment_profile_from()
|
|
35
34
|
checks: dict[str, Any] = production_config_checks()
|
|
@@ -320,6 +319,7 @@ def create_product(
|
|
|
320
319
|
tags=payload.get("tags") or [],
|
|
321
320
|
description=str(payload.get("description") or ""),
|
|
322
321
|
delivery_attributes=payload.get("delivery_attributes") or [],
|
|
322
|
+
handoff_destination=str(payload.get("handoff_destination") or ""),
|
|
323
323
|
)
|
|
324
324
|
return {"ok": True, "product": product}
|
|
325
325
|
|
|
@@ -346,6 +346,9 @@ def update_product(
|
|
|
346
346
|
tags=payload.get("tags") if "tags" in payload else None,
|
|
347
347
|
description=payload.get("description"),
|
|
348
348
|
delivery_attributes=payload.get("delivery_attributes") if "delivery_attributes" in payload else None,
|
|
349
|
+
handoff_destination=payload.get("handoff_destination")
|
|
350
|
+
if "handoff_destination" in payload
|
|
351
|
+
else None,
|
|
349
352
|
)
|
|
350
353
|
return {"ok": True, "product": product}
|
|
351
354
|
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
公开读免鉴权(沿用 products 现状);发布动作只经 CLI 不进 API(评审:
|
|
7
7
|
写面保持 push-first 单向,API 不暴露发布副作用)。
|
|
8
|
+
|
|
9
|
+
handoff_destination 是商家私有成交入口(审查 P2-B):匿名/非本人读一律
|
|
10
|
+
剥离;仅持商品所属商户本人有效 token 的调用保留(kiwi merchant agent
|
|
11
|
+
发布/成交取数路径)。
|
|
8
12
|
"""
|
|
9
13
|
|
|
10
14
|
from __future__ import annotations
|
|
@@ -12,6 +16,7 @@ from __future__ import annotations
|
|
|
12
16
|
from pathlib import Path
|
|
13
17
|
from typing import Any
|
|
14
18
|
|
|
19
|
+
from shopping_cli.api.handlers.catalog import _owner_merchant_from_payload
|
|
15
20
|
from shopping_cli.core.errors import NotFoundError, ValidationError
|
|
16
21
|
from shopping_cli.db.session import db_session
|
|
17
22
|
from shopping_cli.listings.projection import (
|
|
@@ -21,15 +26,28 @@ from shopping_cli.listings.projection import (
|
|
|
21
26
|
)
|
|
22
27
|
|
|
23
28
|
|
|
24
|
-
def list_listing_projections(
|
|
25
|
-
|
|
29
|
+
def list_listing_projections(
|
|
30
|
+
db_path: str | Path, query: dict[str, Any], payload: dict[str, Any] | None = None
|
|
31
|
+
) -> dict[str, Any]:
|
|
32
|
+
"""GET /v1/merchant/listings/projections —— 可发布投影(public-only)。
|
|
33
|
+
|
|
34
|
+
必须带 ``merchant_id`` 过滤(审查 P3-01:无过滤的匿名枚举会暴露全部
|
|
35
|
+
商家的可发布投影);匿名或 token 不属于该商户本人时剥离
|
|
36
|
+
handoff_destination(审查 P2-B)。
|
|
37
|
+
"""
|
|
26
38
|
merchant_id = str(query.get("merchant_id") or "").strip()
|
|
39
|
+
if not merchant_id:
|
|
40
|
+
raise ValidationError("merchant_id is required")
|
|
27
41
|
with db_session(db_path) as conn:
|
|
42
|
+
owner = _owner_merchant_from_payload(conn, payload)
|
|
28
43
|
projections = list_publishable_listings(conn, merchant_id=merchant_id)
|
|
44
|
+
if not (owner and owner == merchant_id):
|
|
45
|
+
for projection in projections:
|
|
46
|
+
projection.pop("handoff_destination", None)
|
|
29
47
|
return {"ok": True, "results": projections, "count": len(projections)}
|
|
30
48
|
|
|
31
49
|
|
|
32
|
-
def get_listing_projection(db_path: str | Path, sku: str) -> dict[str, Any]:
|
|
50
|
+
def get_listing_projection(db_path: str | Path, sku: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
33
51
|
"""GET /v1/merchant/listings/{sku}/projection —— 单条投影。"""
|
|
34
52
|
sku = str(sku).strip()
|
|
35
53
|
if not sku:
|
|
@@ -39,4 +57,8 @@ def get_listing_projection(db_path: str | Path, sku: str) -> dict[str, Any]:
|
|
|
39
57
|
projection = project_product_listing(conn, sku)
|
|
40
58
|
except ProjectionError as exc:
|
|
41
59
|
raise NotFoundError(str(exc)) from exc
|
|
60
|
+
owner = _owner_merchant_from_payload(conn, payload)
|
|
61
|
+
row = conn.execute("select merchant_id from products where sku = ?", (sku,)).fetchone()
|
|
62
|
+
if not (owner and row is not None and owner == str(row["merchant_id"] or "")):
|
|
63
|
+
projection.pop("handoff_destination", None)
|
|
42
64
|
return {"ok": True, "projection": projection}
|
|
@@ -88,16 +88,18 @@ def _get_product(db_path: str | Path, sku: str, payload: dict[str, Any] | None =
|
|
|
88
88
|
return catalog_handlers.get_product(db_path, sku, payload)
|
|
89
89
|
|
|
90
90
|
|
|
91
|
-
def _list_listing_projections(
|
|
92
|
-
|
|
91
|
+
def _list_listing_projections(
|
|
92
|
+
db_path: str | Path, query: dict[str, Any], payload: dict[str, Any] | None = None
|
|
93
|
+
) -> dict[str, Any]:
|
|
94
|
+
return listings_projection_handlers.list_listing_projections(db_path, query, payload)
|
|
93
95
|
|
|
94
96
|
|
|
95
97
|
def _sync_erp(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
96
98
|
return erp_handlers.sync_erp(db_path, payload)
|
|
97
99
|
|
|
98
100
|
|
|
99
|
-
def _get_listing_projection(db_path: str | Path, sku: str) -> dict[str, Any]:
|
|
100
|
-
return listings_projection_handlers.get_listing_projection(db_path, sku)
|
|
101
|
+
def _get_listing_projection(db_path: str | Path, sku: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
102
|
+
return listings_projection_handlers.get_listing_projection(db_path, sku, payload)
|
|
101
103
|
|
|
102
104
|
|
|
103
105
|
def _search_products(db_path: str | Path, query: dict[str, Any], payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
@@ -402,12 +404,12 @@ _ROUTE_TABLE: tuple[RouteEntry, ...] = (
|
|
|
402
404
|
RouteEntry(
|
|
403
405
|
{"GET"},
|
|
404
406
|
"/v1/merchant/listings/projections",
|
|
405
|
-
lambda db_path, payload, query, **kw: _list_listing_projections(db_path, query or {}),
|
|
407
|
+
lambda db_path, payload, query, **kw: _list_listing_projections(db_path, query or {}, payload),
|
|
406
408
|
),
|
|
407
409
|
RouteEntry(
|
|
408
410
|
{"GET"},
|
|
409
411
|
"/v1/merchant/listings/{sku}/projection",
|
|
410
|
-
lambda db_path, payload, query, sku: _get_listing_projection(db_path, sku),
|
|
412
|
+
lambda db_path, payload, query, sku: _get_listing_projection(db_path, sku, payload),
|
|
411
413
|
),
|
|
412
414
|
RouteEntry(
|
|
413
415
|
{"POST"},
|
|
@@ -6,6 +6,7 @@ import math
|
|
|
6
6
|
import re
|
|
7
7
|
import sqlite3
|
|
8
8
|
from typing import Any, Mapping
|
|
9
|
+
from urllib.parse import urlparse
|
|
9
10
|
|
|
10
11
|
from shopping_cli.core.errors import ConflictError, NotFoundError, ValidationError
|
|
11
12
|
from shopping_cli.core.harness import append_audit_event
|
|
@@ -114,6 +115,27 @@ def _whole_int(value: Any, message: str) -> int:
|
|
|
114
115
|
|
|
115
116
|
# safe_non_negative_int / safe_non_negative_float are imported from core.limits
|
|
116
117
|
|
|
118
|
+
# 审查 P2-A:可无 ``://`` 出现的危险伪 scheme,一律拒绝(opaque 放行见下)。
|
|
119
|
+
_BLOCKED_HANDOFF_SCHEMES = frozenset({"javascript", "data", "file", "vbscript", "blob"})
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _validate_handoff_destination(value: str) -> None:
|
|
123
|
+
"""handoff_destination 形态校验(审查 P2-A)。
|
|
124
|
+
|
|
125
|
+
空值允许;值呈现 URL 形态(含 ``://``)必须是合法 http/https origin,
|
|
126
|
+
拒绝其他一切 scheme;``javascript:``/``data:`` 等危险伪 scheme 即使
|
|
127
|
+
无 ``://`` 也拒绝;纯 opaque 引用串(chat-id、文档引用等)原样允许。
|
|
128
|
+
"""
|
|
129
|
+
if not value:
|
|
130
|
+
return
|
|
131
|
+
parsed = urlparse(value)
|
|
132
|
+
scheme = parsed.scheme.lower()
|
|
133
|
+
if "://" in value:
|
|
134
|
+
if scheme not in ("http", "https") or not parsed.hostname:
|
|
135
|
+
raise ValidationError("handoff destination URL must be a valid http(s) origin")
|
|
136
|
+
elif scheme in _BLOCKED_HANDOFF_SCHEMES:
|
|
137
|
+
raise ValidationError(f"handoff destination scheme is not allowed: {scheme}")
|
|
138
|
+
|
|
117
139
|
|
|
118
140
|
def _audit_catalog(conn: sqlite3.Connection, merchant_id: str, event: str, details: dict) -> None:
|
|
119
141
|
"""catalog 写操作审计(conversation_id=''——非会话域事件)。
|
|
@@ -329,6 +351,7 @@ def create_product(
|
|
|
329
351
|
category = bounded_text(category, "product category", MAX_SHORT_TEXT_CHARS)
|
|
330
352
|
currency = bounded_text(currency, "currency", 16)
|
|
331
353
|
handoff_destination = bounded_text(handoff_destination, "handoff destination", MAX_PERSISTED_TEXT_CHARS)
|
|
354
|
+
_validate_handoff_destination(handoff_destination)
|
|
332
355
|
if not merchant_id:
|
|
333
356
|
raise ValidationError("merchant id is required")
|
|
334
357
|
if not sku:
|
|
@@ -405,6 +428,7 @@ def update_product(
|
|
|
405
428
|
handoff_destination = bounded_text(
|
|
406
429
|
handoff_destination, "handoff destination", MAX_PERSISTED_TEXT_CHARS
|
|
407
430
|
)
|
|
431
|
+
_validate_handoff_destination(handoff_destination)
|
|
408
432
|
if price is not None:
|
|
409
433
|
price = _price_with_precision(price, "--price must be finite")
|
|
410
434
|
if stock is not None:
|
|
@@ -606,6 +630,7 @@ def _product_summary_from_search_row(row: sqlite3.Row) -> dict[str, Any]:
|
|
|
606
630
|
"currency": row["currency"],
|
|
607
631
|
"stock": _safe_non_negative_int(row["stock"]),
|
|
608
632
|
"delivery_attributes": decode_json(row["delivery_attributes_json"], []),
|
|
633
|
+
"handoff_destination": row["handoff_destination"] or "",
|
|
609
634
|
"merchant": merchant,
|
|
610
635
|
"delivery": merchant["delivery"],
|
|
611
636
|
"warnings": product_warnings(row, merchant),
|
|
@@ -21,10 +21,15 @@ def public_product_summary(product: dict[str, Any]) -> dict[str, Any]:
|
|
|
21
21
|
出网精确 stock,可逐 SKU 枚举全平台库存)。需要精确库存的商家走
|
|
22
22
|
merchant_product_summary(handler 层 owner 鉴权门)。
|
|
23
23
|
|
|
24
|
+
handoff_destination 是商家私有成交入口(谈判达成后由商家 Agent 点对点
|
|
25
|
+
交给买家),公开/匿名投影一律剥离(审查 P2-B),同款纪律参照
|
|
26
|
+
public_merchant_summary 剥离 contact;认证商家读走 merchant_product_summary。
|
|
27
|
+
|
|
24
28
|
对已投影数据幂等:含 ``stock`` 时替换为 availability_hint;不含则保留
|
|
25
29
|
已有 hint(审查 P1-02:幂等回放响应二次投影不丢失 in_stock)。
|
|
26
30
|
"""
|
|
27
31
|
summary = dict(product)
|
|
32
|
+
summary.pop("handoff_destination", None)
|
|
28
33
|
if "stock" in summary:
|
|
29
34
|
stock = summary.pop("stock", None)
|
|
30
35
|
summary["availability_hint"] = (
|
|
@@ -10,7 +10,7 @@ from typing import Callable
|
|
|
10
10
|
|
|
11
11
|
from shopping_cli.core.tokens import is_sha256_digest, token_digest, token_prefix, token_suffix
|
|
12
12
|
|
|
13
|
-
CURRENT_SCHEMA_VERSION =
|
|
13
|
+
CURRENT_SCHEMA_VERSION = 23
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@dataclass(frozen=True)
|
|
@@ -386,6 +386,62 @@ def migration_022_product_handoff_destination(conn: sqlite3.Connection) -> None:
|
|
|
386
386
|
ensure_column(conn, "products", "handoff_destination", "text not null default ''")
|
|
387
387
|
|
|
388
388
|
|
|
389
|
+
def migration_023_remove_scheme_a_stub_merchants(conn: sqlite3.Connection) -> None:
|
|
390
|
+
"""方案A 窗口期 stub 商家行清理(审查 P3-02)。
|
|
391
|
+
|
|
392
|
+
已拆除的方案A ``_ensure_merchant_exists``(5ad9ec8 移除)曾插入
|
|
393
|
+
``name == id``、无任何 api_tokens 行的 stub 商家:这些行无法认证,却挡
|
|
394
|
+
``POST /merchants`` 同 id 重建(ConflictError)。删除无 token 且名下无
|
|
395
|
+
业务行的 stub;仍有 products/policies/delivery_rules/conversations 行的
|
|
396
|
+
跳过并记 audit_events——外键无 ON DELETE CASCADE,不静默丢业务数据。
|
|
397
|
+
"""
|
|
398
|
+
child_tables = ("products", "policies", "delivery_rules", "conversations")
|
|
399
|
+
rows = conn.execute(
|
|
400
|
+
"""
|
|
401
|
+
select id from merchants
|
|
402
|
+
where name = id
|
|
403
|
+
and not exists (
|
|
404
|
+
select 1 from api_tokens where api_tokens.merchant_id = merchants.id
|
|
405
|
+
)
|
|
406
|
+
"""
|
|
407
|
+
).fetchall()
|
|
408
|
+
now = datetime.now().replace(microsecond=0).isoformat()
|
|
409
|
+
for row in rows:
|
|
410
|
+
merchant_id = str(row["id"])
|
|
411
|
+
dependents = {
|
|
412
|
+
table: int(
|
|
413
|
+
conn.execute(
|
|
414
|
+
f"select count(*) from {table} where merchant_id = ?", (merchant_id,)
|
|
415
|
+
).fetchone()[0]
|
|
416
|
+
)
|
|
417
|
+
for table in child_tables
|
|
418
|
+
}
|
|
419
|
+
dependents = {table: count for table, count in dependents.items() if count}
|
|
420
|
+
if dependents:
|
|
421
|
+
conn.execute(
|
|
422
|
+
"""
|
|
423
|
+
insert into audit_events(conversation_id, actor, event, details_json, created_at)
|
|
424
|
+
values ('', 'system', 'stub_merchant_retained', ?, ?)
|
|
425
|
+
""",
|
|
426
|
+
(
|
|
427
|
+
json.dumps(
|
|
428
|
+
{
|
|
429
|
+
"event_type": "stub_merchant_retained",
|
|
430
|
+
"merchant_id": merchant_id,
|
|
431
|
+
"dependents": dependents,
|
|
432
|
+
"reason": "scheme_a_stub_with_business_rows",
|
|
433
|
+
"source": "migration_023_remove_scheme_a_stub_merchants",
|
|
434
|
+
},
|
|
435
|
+
ensure_ascii=False,
|
|
436
|
+
sort_keys=True,
|
|
437
|
+
),
|
|
438
|
+
now,
|
|
439
|
+
),
|
|
440
|
+
)
|
|
441
|
+
continue
|
|
442
|
+
conn.execute("delete from merchants where id = ?", (merchant_id,))
|
|
443
|
+
|
|
444
|
+
|
|
389
445
|
MIGRATIONS: tuple[Migration, ...] = (
|
|
390
446
|
Migration(1, "conversation_next_actor", migration_001_conversation_next_actor),
|
|
391
447
|
Migration(2, "agent_runtime_columns", migration_002_agent_runtime_columns),
|
|
@@ -402,6 +458,7 @@ MIGRATIONS: tuple[Migration, ...] = (
|
|
|
402
458
|
Migration(20, "buyer_ledger_buyer_dimension", migration_020_buyer_ledger_buyer_dimension),
|
|
403
459
|
Migration(21, "channel_ingress_stale_marker", migration_021_channel_ingress_stale_marker),
|
|
404
460
|
Migration(22, "product_handoff_destination", migration_022_product_handoff_destination),
|
|
461
|
+
Migration(23, "remove_scheme_a_stub_merchants", migration_023_remove_scheme_a_stub_merchants),
|
|
405
462
|
)
|
|
406
463
|
|
|
407
464
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/services/negotiation_policy_helpers.py
RENAMED
|
File without changes
|
{shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/services/negotiation_policy_result.py
RENAMED
|
File without changes
|
|
File without changes
|
{shopping_cli-3.0.2 → shopping_cli-3.1.0}/shopping_cli/services/negotiation_snapshot_projection.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|