Make IgnoredKeywords.watch a real Link column to Settings

Replace the hand-copied search_url (URL type) with a proper relation:
- watch: LinkToAnotherRecord (belongs-to, many keywords -> one Settings row),
  picked from a dropdown in the UI so a wrong URL can't silently disable a keyword
- search_url: Lookup column that auto-fills Settings.url through the link

_ensure_keyword_watch_link() builds both columns idempotently on bootstrap and
upgrades a legacy URL-typed search_url to Link+Lookup. load_ignored_keywords()
now normalises Lookup values (string or array).
This commit is contained in:
2026-09-13 18:11:51 +08:00
parent 9263b91276
commit 531da933ca
4 changed files with 106 additions and 20 deletions
+81 -3
View File
@@ -88,10 +88,10 @@ LISTINGS_COLS = [
IGNORED_SELLERS_COLS = [
("seller_name", "SingleLineText"),
]
# 忽略关键词(per-watch):search_url=Settings 里的 watch URL 原样复制,keyword 单行一条。
# 忽略关键词(per-watch):keyword 单行一条;watch=Link 指向 Settings 记录;
# search_url=Lookup 自动带出被链接 watch 的 Settings.url。
# 大小写不敏感,命中该 watch 的「标题」即跳过通知(仍归档)。
IGNORED_KEYWORDS_COLS = [
("search_url", "URL"),
("keyword", "SingleLineText"),
]
SETTINGS_COLS = [
@@ -247,9 +247,82 @@ def bootstrap():
settings_tid = _ensure_table("Settings", SETTINGS_COLS)
ignored_sellers_tid = _ensure_table("IgnoredSellers", IGNORED_SELLERS_COLS)
ignored_keywords_tid = _ensure_table("IgnoredKeywords", IGNORED_KEYWORDS_COLS)
_ensure_keyword_watch_link(ignored_keywords_tid, settings_tid)
return listings_tid, settings_tid, ignored_sellers_tid, ignored_keywords_tid
def _ensure_keyword_watch_link(kw_tid, settings_tid):
"""IgnoredKeywords 的 watch(Link→Settings) + search_url(Lookup→Settings.url)。
NocoDB 2026.08 的列创建 API 与旧版不同:
- Link 列: uidt="LinkToAnotherRecord", 顶层 parentId/childId/type
- 回读 link 列的 colOptions.fk_column_id(物理外键列)作为 Lookup 的
fk_relation_column_id
- Lookup 列: uidt="Lookup", 顶层 fk_relation_column_id + fk_lookup_column_id
幂等:列已存在就跳过。旧版遗留的 URL 型 search_url 会被替换成 Lookup。
"""
def _kw_meta():
st, m = nc("GET", f"/api/v2/meta/tables/{kw_tid}")
if st != 200:
raise RuntimeError(f"read IgnoredKeywords meta failed: {m}")
return m
m = _kw_meta()
cols = {c.get("title"): c for c in m.get("columns", [])}
# 1. watch: LinkToAnotherRecord -> Settings
link_col = None
if "watch" in cols:
if cols["watch"].get("uidt") != "LinkToAnotherRecord":
nc("DELETE", f"/api/v2/meta/columns/{cols['watch']['id']}")
else:
link_col = cols["watch"]
if link_col is None:
st, out = nc("POST", f"/api/v2/meta/tables/{kw_tid}/columns",
{"title": "watch", "uidt": "LinkToAnotherRecord",
"parentId": settings_tid, "childId": kw_tid,
"type": "bt", "colOptions": {}})
if st != 200:
raise RuntimeError(f"create watch link failed: {out}")
m = _kw_meta()
cols = {c.get("title"): c for c in m.get("columns", [])}
link_col = cols.get("watch")
if link_col is None:
raise RuntimeError("watch link column missing after create")
# 2. search_url: Lookup (auto-pull Settings.url through the link)
if "search_url" in cols and cols["search_url"].get("uidt") == "Lookup":
return # already good
# drop stale non-Lookup search_url (legacy URL column)
if "search_url" in cols:
nc("DELETE", f"/api/v2/meta/columns/{cols['search_url']['id']}")
fk_parent = (link_col.get("colOptions") or {}).get("fk_column_id")
if not fk_parent:
raise RuntimeError("watch link has no fk_column_id")
st, sm = nc("GET", f"/api/v2/meta/tables/{settings_tid}")
if st != 200:
raise RuntimeError(f"read Settings meta failed: {sm}")
url_col = next((c for c in sm.get("columns", [])
if c.get("title") == "url"), None)
if url_col is None:
raise RuntimeError("Settings.url column not found")
st, out = nc("POST", f"/api/v2/meta/tables/{kw_tid}/columns",
{"title": "search_url", "uidt": "Lookup",
"fk_relation_column_id": fk_parent,
"fk_lookup_column_id": url_col["id"],
"colOptions": {}})
if st != 200:
raise RuntimeError(f"create search_url lookup failed: {out}")
# --------------------------------------------------------------------------- #
# Carousell extraction
# --------------------------------------------------------------------------- #
@@ -384,13 +457,18 @@ def load_ignored_keywords(ignored_keywords_tid):
"""从 IgnoredKeywords 表读取忽略关键词,返回 {search_url: {小写关键词}}。
关键词按 watch(search_url)分组;匹配时大小写不敏感。
search_url 是 Lookup 列(经 watch 链接自动带出 Settings.url),
API 可能返回字符串或数组,这里统一归一化。
"""
ignored = {}
st, j = nc("GET", f"/api/v2/tables/{ignored_keywords_tid}/records?limit=1000")
if st != 200:
raise RuntimeError(f"load ignored keywords failed: {j}")
for r in j.get("list", []):
url = (r.get("search_url") or "").strip()
url = r.get("search_url") or ""
if isinstance(url, list): # Lookup 有时返回数组
url = url[0] if url else ""
url = str(url).strip()
kw = (r.get("keyword") or "").strip().lower()
if not url or not kw:
continue