# 從通訊軟體到社群與短影片：物件儲存、推薦、mini app 與會員安全

> 社群和短影片比通訊軟體多了公開內容、大檔案、推薦和第三方程式，伺服器又回到中心。整理 MinIO 之後的物件儲存選擇、用 Qdrant 做最小推薦、mini app 的驗證方式，以及台灣與歐盟的相關法規。

原文：https://sheng.page/posts/community-and-short-video-platforms/ · 發布：2026-10-06 · 作者：Sheng

這篇接著〈[從裝置出發打造通訊軟體](/posts/local-first-messaging/)〉談社群和短影片，它們比通訊軟體多了公開內容、大量影片、推薦，還有跑在自家 App 裡的第三方程式。下面照這個順序寫，最後補上會員安全和法規。

## 公開內容讓伺服器回到中心

通訊軟體可以把資料放回裝置，是因為內容只給收發雙方看。社群貼文和短影片一發出去就是給陌生人看的，伺服器得讀得到內容，才能建索引、算推薦、處理檢舉。端對端加密保留給私訊，公開內容照一般服務設計。

想讓使用者帶得走資料，現有的路是聯邦協定：

- ActivityPub 在 2018 年 1 月 23 日成為 W3C Recommendation，每個帳號有 inbox 和 outbox，伺服器之間互相投遞。Mastodon 這一系都用它。
- Bluesky 的 AT Protocol 在 2024 年 2 月 22 日開放聯邦，任何人都能架自己的 PDS（Personal Data Server），一開始有規模上的限制。
- Threads 在 2025 年 6 月 17 日加上獨立的 fediverse 動態與跨站搜尋，當時還不能從那個動態直接回覆。
- Matrix 的房間在各 homeserver 之間複製，規格寫明沒有任何一台 homeserver 擁有某個房間，適合社群聊天室。

## 影片檔放哪：MinIO 之後的選擇

自架 S3 相容儲存，過去幾年最常見的答案是 MinIO，但社群版這一年多變化很大：

- 2025 年 5 月 24 日的版本把內建管理介面標為 deprecated，移到 object-browser，LDAP／OIDC 登入只留在商業版 AIStor。
- 2025 年 10 月起，README 寫明社群版只提供原始碼，不再提供預先編譯的執行檔。
- 2025 年 12 月，README 加上 maintenance mode，不再接受新變更，安全修正個案評估。
- GitHub 頁面顯示 repo 在 2026 年 4 月 25 日封存為唯讀。

授權一直是 AGPLv3，舊版還能用，但社群版已不再開發。新專案可以考慮這些：

| 選項 | 授權 | 語言 | 定位與備註 |
|---|---|---|---|
| SeaweedFS | Apache 2.0 | Go | 物件儲存、檔案系統與 Iceberg table，強調大量小檔案 |
| Garage | AGPL-3.0 | Rust | 小規模、跨地點的自架部署，可以跑在機房以外 |
| RustFS | Apache 2.0 | Rust | 2026 年 9 月 16 日發布 1.0 GA，官方稱可用於正式環境，2025 年 7 月才開源 |
| Ceph RGW | LGPL | C++ | 建在 Ceph 的 RADOS 之上，S3 API 相容，要連同整套 Ceph 叢集一起維運 |
| Apache Ozone | Apache 2.0 | Java | 偏向資料分析的分散式儲存，原生支援 S3 協定 |

不自架的話，Cloudflare R2 的定價頁寫明出站流量不收費，影片讀多寫少，出站流量這筆值得先算進成本。不管選哪個，程式裡只用 S3 API，不要用到某家獨有的功能，之後換掉的成本就小。

影片本身的流程大致是：上傳原始檔、轉檔成多種解析度、切成 HLS 片段（RFC 8216，2017 年）、放進物件儲存、前面掛 CDN。要壓低延遲可以用 Low-Latency HLS，把片段再切成更小的 partial segment（Apple 文件的例子約 200 毫秒），這部分規則在 HLS 第二版草案（draft-pantos-hls-rfc8216bis）裡，還沒成為 RFC。

## 推薦：先用 Qdrant 做最小版本

YouTube 2016 年的論文把推薦拆成兩階段：先從大量影片裡挑出幾百個候選，再用另一個模型排序。ByteDance 2022 年的 Monolith 論文處理的是另一個問題，即時訓練：embedding table 沒有雜湊碰撞、會過期淘汰，模型在線上持續訓練，使用者的互動近即時反映到推薦上。

起步不需要這麼複雜，把影片的標題和 hashtag 轉成向量放進 Qdrant，拿使用者看完的影片當正例、滑掉的當負例，就是一個能用的候選產生器：

```python
from importlib.metadata import version
import tempfile

from fastembed import TextEmbedding
from qdrant_client import QdrantClient, models

# Each short video is described by its caption and hashtags.
VIDEOS = {
    1: "十分鐘學會手沖咖啡 #咖啡 #教學",
    2: "冷萃咖啡怎麼做才不苦 #咖啡",
    3: "陽明山夜景散步路線 #登山 #台北",
    4: "新手第一次爬百岳要準備什麼 #登山",
    5: "貓咪第一次看到雪 #貓 #寵物",
    6: "在家做義式濃縮的機器怎麼挑 #咖啡 #開箱",
    7: "股票新手常犯的五個錯 #理財",
    8: "合歡山日出要幾點出發 #登山 #日出",
}

MODEL = "sentence-transformers/paraphrase-multilingual-mpnet-base-v2"
embedder = TextEmbedding(MODEL)
vectors = dict(zip(VIDEOS, embedder.embed(list(VIDEOS.values()))))

liked, skipped = [1, 3], [7]  # watched to the end vs swiped away
seen = liked + skipped

with tempfile.TemporaryDirectory() as path:
    client = QdrantClient(path=path)
    client.create_collection(
        "videos",
        vectors_config=models.VectorParams(
            size=len(vectors[1]), distance=models.Distance.COSINE
        ),
    )
    client.upsert(
        "videos",
        points=[
            models.PointStruct(id=i, vector=v.tolist(), payload={"caption": VIDEOS[i]})
            for i, v in vectors.items()
        ],
    )
    not_seen = models.Filter(must_not=[models.HasIdCondition(has_id=seen)])
    for strategy in (
        models.RecommendStrategy.AVERAGE_VECTOR,
        models.RecommendStrategy.BEST_SCORE,
    ):
        hits = client.query_points(
            "videos",
            query=models.RecommendQuery(
                recommend=models.RecommendInput(
                    positive=liked, negative=skipped, strategy=strategy
                )
            ),
            query_filter=not_seen,
            limit=3,
        ).points
        print(strategy.value)
        for h in hits:
            print(f"  {h.score:.3f} {h.payload['caption']}")
    client.close()

print("qdrant-client", version("qdrant-client"), "fastembed", version("fastembed"))
```

輸出：

```text
average_vector
  0.548 合歡山日出要幾點出發 #登山 #日出
  0.448 冷萃咖啡怎麼做才不苦 #咖啡
  0.411 在家做義式濃縮的機器怎麼挑 #咖啡 #開箱
best_score
  0.712 合歡山日出要幾點出發 #登山 #日出
  0.707 冷萃咖啡怎麼做才不苦 #咖啡
  0.704 在家做義式濃縮的機器怎麼挑 #咖啡 #開箱
qdrant-client 1.19.1 fastembed 0.8.1
```

貓的影片沒被推出來（理財那支是負例，本來就排除了），登山和咖啡各有入選。兩種策略的分數尺度不同，不能互相比較：`average_vector` 把正負例合成一個查詢向量，成本跟一般搜尋一樣；`best_score` 逐一比對每個例子，例子越多越慢，好處是使用者同時喜歡兩種差很多的內容時，查詢不會被平均到兩邊都不像。同樣是登山，「新手第一次爬百岳」沒有進前三名，可見只靠標題文字的向量很粗，實際上線還得加進觀看時長、互動和熱門度。

這個版本只看內容本身。想做到「看過這支的人也看了那支」，Qdrant 官方的 MovieLens 範例把每個使用者的評分存成 sparse vector，先找相似的使用者，再統計他們看過的影片。冷啟動和多樣性最好從第一版就處理：

- 冷啟動：TikTok 2020 年的說明是新使用者可以先選興趣類別，跳過的人看到一般熱門影片。
- 多樣性：同一份說明寫到，動態通常不會連續出現同一個音樂或同一個創作者的影片，偶爾也會放使用者興趣以外的內容。這種規則放在排序之後做就好，跟向量搜尋分開。

## 透過 WebView 加入第三方 mini app

讓第三方開發者在自家 App 裡放小程式，幾個平台是這樣做的：

- 微信小程式的渲染層用 WebView，邏輯層在獨立的 JsCore 執行緒跑，兩邊透過原生用戶端溝通，網路請求也由用戶端代發。
- 支付寶小程式的文件寫明框架不在瀏覽器裡執行，`document`、`window` 這些物件不能用。
- LINE MINI App 2023 年 9 月 27 日在台灣開放申請，本質是 LIFF 網頁跑在 LINE 的 WebView 裡；2025 年 10 月 1 日起所有 MINI App 都能在一般瀏覽器開啟。
- Telegram Mini Apps 也是 WebView 載入網頁，啟動時由 Telegram 帶入簽過章的使用者資料。

上架 iOS 還要看 App Store 審核指南 4.7。2024 年 1 月 25 日的修訂把 mini app、mini game、chatbot 和 plug-in 都納入，宿主 App 要為這些軟體負責；4.7.2 規定沒有 Apple 事先同意，不能把原生 API 開放給這些程式，4.7.3 規定分享資料或隱私權限要每次都取得使用者明確同意。

技術上最常出事的是 JavaScript bridge。Android 4.2（API 17）之前，`addJavascriptInterface` 讓網頁可以透過反射呼叫任意 Java 方法（CVE-2012-6636）；現在的官方文件仍提醒，注入的物件會出現在 WebView 裡的每個 frame，包括 iframe。建議改用 `addWebMessageListener` 加上 `allowedOriginRules`，只接受指定來源的訊息，也不要拿 `WebView.getUrl()` 當安全判斷。iOS 的 `WKScriptMessage` 可以從 `frameInfo.securityOrigin` 確認訊息來自哪個 frame。

另一個常出問題的是身分。mini app 的前端拿到的使用者資料，送到後端之前都可能被改過。LINE 的文件直接寫明，不要把 `liff.getProfile()` 拿到的資料送到伺服器，要送 ID token，由伺服器向 LINE 驗證。Telegram 的做法是對啟動資料簽章：用 `"WebAppData"` 當 key 對 bot token 做 HMAC-SHA256 得到 secret key，再對排序後的欄位算 HMAC，跟 `hash` 欄位比對。下面模擬平台簽章、伺服器驗證，再試竄改和過期兩種情況：

```python
import hashlib
import hmac
import json
import time
from urllib.parse import parse_qsl, urlencode


class InitDataError(Exception):
    pass


def _check_string(fields: dict[str, str]) -> str:
    return "\n".join(f"{k}={v}" for k, v in sorted(fields.items()))


def _secret(bot_token: str) -> bytes:
    return hmac.new(b"WebAppData", bot_token.encode(), hashlib.sha256).digest()


def sign(fields: dict[str, str], bot_token: str) -> str:
    # What the platform does before handing init data to the mini app.
    digest = hmac.new(_secret(bot_token), _check_string(fields).encode(), hashlib.sha256)
    return urlencode({**fields, "hash": digest.hexdigest()})


def verify(init_data: str, bot_token: str, max_age: int = 3600) -> dict[str, str]:
    # What the mini app's own server must do before trusting anything.
    fields = dict(parse_qsl(init_data, strict_parsing=True))
    received = fields.pop("hash", "")
    expected = hmac.new(
        _secret(bot_token), _check_string(fields).encode(), hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(received, expected):
        raise InitDataError("bad hash")
    if time.time() - int(fields["auth_date"]) > max_age:
        raise InitDataError("expired")
    return fields


BOT_TOKEN = "123456:TEST-ONLY-TOKEN"  # dummy value for the example
now = str(int(time.time()))
user = json.dumps({"id": 42, "first_name": "Demo"}, separators=(",", ":"))

good = sign({"auth_date": now, "query_id": "AAE", "user": user}, BOT_TOKEN)
print("valid:", json.loads(verify(good, BOT_TOKEN)["user"])["id"])

forged = good.replace("%22id%22%3A42", "%22id%22%3A1")
old = sign({"auth_date": str(int(now) - 7200), "user": user}, BOT_TOKEN)
for name, data in (("forged", forged), ("old", old)):
    try:
        verify(data, BOT_TOKEN)
    except InitDataError as e:
        print(f"{name}: rejected ({e})")
```

輸出：

```text
valid: 42
forged: rejected (bad hash)
old: rejected (expired)
```

比對用 `hmac.compare_digest`，避免逐字元比較洩漏時間差；`auth_date` 要檢查，否則截到的舊資料可以一直重用。Telegram 在 2024 年 11 月 17 日的 Bot API 8.0 另外加了 Ed25519 簽章，讓不持有 bot token 的第三方也能驗證，自己做平台也可以比照：平台用私鑰簽，mini app 開發者拿公鑰驗。

金鑰放在前端有多常見，CCS 2023 有篇論文實際量過：掃了 3,450,586 個微信小程式，其中 40,880 個把 AppSecret 這種主金鑰放在前端程式碼裡，研究者示範了竄改手機號碼接管帳號、濫用優惠和盜用付費雲端服務。

## 會員系統夠不夠安全

用平台登入（LINE、Telegram、Apple、Google）只確認了使用者是誰，之後的 session、權限和資料保護都得自己做。可以對照兩份公開標準檢查：

- NIST SP 800-63B-4（2025 年 7 月 31 日定稿）：只用密碼時至少 15 字元，搭配多因素時至少 8 字元；不能強制大小寫符號這類組成規則，不能要求定期改密碼；新密碼要比對常見與已外洩的密碼清單；AAL2 等級要至少提供一種抗釣魚的驗證方式，passkey 屬於這一類。
- OWASP ASVS 5.0.0（2025 年 5 月 30 日發布）：逐項列出驗證、session、存取控制等檢查點，適合拿來當上線前的清單。

做 mini app 時，自家帳號和平台帳號的對應要放在伺服器，用驗證過的平台使用者 ID 當 key；前端送來的任何身分欄位都不能直接寫進資料庫。session token 要短命，敏感操作再驗一次。

## 法規：台灣與歐盟目前公布的內容

台灣：

- 個人資料保護法：2023 年 5 月 31 日公布的修正，增訂個人資料保護委員會為主管機關，非公務機關未採取安全措施時，不必先限期改正即可處罰，一般情況新臺幣 2 萬元至 200 萬元，情節重大 15 萬元至 1,500 萬元。2025 年 11 月 11 日公布的修正，規定非公務機關發生個資外洩要通知當事人並通報個資會，並給個資會行政檢查權，施行日期由行政院定。截至本文查詢時，個資會官網仍以籌備處名義運作。
- 詐欺犯罪危害防制條例：2024 年 7 月 31 日公布，規範網路廣告平臺、電商、第三方支付與網路連線遊戲業者。達一定規模的廣告平臺要驗證廣告主與出資者身分、標示廣告與 AI 生成的肖像，並在期限內下架詐騙廣告，相關子法定為 24 小時。數位發展部在 2024 年 9 月 16 日指定 Google、LY、Meta、TikTok 四家。
- 數位中介服務法草案：國家通訊傳播委員會在 2022 年 9 月把草案退回內部重新研議，沒有時間表；截至本文查詢時，沒有查到公布的新版本。
- 資通安全管理法：2025 年 9 月 24 日公布修正，民間部分的適用對象是特定非公務機關，主要是關鍵基礎設施提供者。

歐盟與其他地區：

- Digital Services Act 在 2024 年 2 月 17 日全面適用，歐盟月活躍使用者 4,500 萬以上的平台有額外義務。歐盟執委會在 2024 年 8 月 5 日接受 TikTok 永久撤回 TikTok Lite 獎勵計畫的承諾；2026 年 2 月 6 日初步認定 TikTok 的成癮設計（無限滑動、自動播放、推播、推薦）違反 DSA。
- 愛爾蘭資料保護委員會在 2025 年 5 月 2 日依 GDPR 對 TikTok 處以 5.3 億歐元罰款，理由是把歐洲使用者資料傳到中國的方式違法，以及透明度不足。
- 澳洲的 16 歲以下社群媒體年齡限制在 2025 年 12 月 10 日生效，涵蓋 Facebook、Instagram、Threads、TikTok、YouTube 等平台，最高罰款 4,950 萬澳幣。

## 補充筆記

- 公開內容要讓伺服器讀得到，端對端加密只留給私訊；想讓使用者帶得走資料，可以看 ActivityPub、AT Protocol。
- MinIO 社群版 2025 年起只剩原始碼、進入 maintenance mode，2026 年 4 月封存；替代品有 SeaweedFS、Garage、RustFS、Ceph RGW、Apache Ozone，程式裡只用 S3 API，之後最容易替換。
- 最小的推薦系統可以用 Qdrant 的 recommend 查詢，正例負例加上排除已看過的過濾條件；多樣性規則放在排序之後。
- mini app 的 bridge 只開給信任的來源，身分一律由伺服器驗證平台簽發的 token 或簽章，金鑰不放前端。
- 會員系統可以對照 NIST SP 800-63B-4 和 OWASP ASVS 5.0.0 檢查；台灣的詐騙廣告下架已入法，個資外洩通報義務已修法公布、施行日期未定。

## 延伸閱讀

- W3C〈[ActivityPub](https://www.w3.org/TR/activitypub/)〉：2018-01-23 成為 Recommendation 的規格本文。
- TechCrunch〈[Bluesky opens up federation](https://techcrunch.com/2024/02/22/bluesky-opens-up-federation-letting-anyone-run-their-own-server/)〉與〈[Threads expands open social web integrations](https://techcrunch.com/2025/06/17/threads-expands-open-social-web-integrations-with-fediverse-feed-user-profile-search)〉：聯邦開放的日期與功能，屬新聞報導。
- Matrix〈[Matrix Specification](https://spec.matrix.org/latest/)〉：房間在 homeserver 之間複製的說明，官方規格。
- MinIO〈[GitHub repository](https://github.com/minio/minio)〉與〈[RELEASE.2025-05-24T17-08-30Z](https://github.com/minio/minio/releases/tag/RELEASE.2025-05-24T17-08-30Z)〉：README 的 source only、maintenance mode 說明與封存狀態，官方 repo。
- It's FOSS〈[MinIO moves away from open source](https://itsfoss.com/news/minio-moves-away-from-open-source/)〉：MinIO 社群版變化的時間線整理，屬新聞報導。
- 各專案官方頁面：〈[SeaweedFS](https://github.com/seaweedfs/seaweedfs)〉、〈[Garage](https://garagehq.deuxfleurs.fr/)〉、〈[Announcing RustFS 1.0.0 GA](https://rustfs.com/blog/announcing-rustfs-1-0-0-ga/)〉、〈[Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/)〉、〈[Apache Ozone](https://ozone.apache.org/)〉，屬廠商或專案自述。
- Cloudflare〈[R2 pricing](https://developers.cloudflare.com/r2/pricing/)〉：出站流量不收費的說明，官方定價頁。
- IETF〈[RFC 8216: HTTP Live Streaming](https://www.rfc-editor.org/rfc/rfc8216)〉、〈[HLS 第二版草案](https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis)〉與 Apple〈[Enabling Low-Latency HLS](https://developer.apple.com/documentation/http-live-streaming/enabling-low-latency-http-live-streaming-hls)〉：HLS 規格與低延遲擴充。
- Covington 等〈[Deep Neural Networks for YouTube Recommendations](https://research.google/pubs/deep-neural-networks-for-youtube-recommendations/)〉：RecSys 2016，兩階段推薦架構。
- Liu 等〈[Monolith: Real Time Recommendation System With Collisionless Embedding Table](https://arxiv.org/abs/2209.07663)〉：ByteDance 2022，即時訓練與 embedding table 設計。
- TikTok〈[How TikTok recommends videos #ForYou](https://newsroom.tiktok.com/en-us/how-tiktok-recommends-videos-for-you)〉：2020-06-18，推薦訊號、冷啟動與多樣性的官方說明。
- Qdrant〈[Explore the data](https://qdrant.tech/documentation/concepts/explore/)〉與〈[Recommendation system with OVHcloud](https://qdrant.tech/documentation/examples/recommendation-system-ovhcloud/)〉：recommend 策略與 sparse vector 協同過濾範例，官方文件。
- 微信〈[Framework](https://developers.weixin.qq.com/miniprogram/en/dev/framework/quickstart/framework.html)〉與支付寶〈[Framework overview](https://miniprogram.alipay.com/docs/miniprogram/mpdev/framework_overview)〉：雙執行環境的官方說明。
- LINE〈[Using user profile](https://developers.line.biz/en/docs/liff/using-user-profile/)〉、〈[LINE MINI App 台灣開放申請](https://linecorp.com/tw/pr/news/tw/2023/4664)〉與〈[MINI App browser access](https://developers.line.biz/en/news/2025/09/26/mini-app-browser/)〉：伺服器驗證 token 的規定與開放時程，官方文件與公告。
- Telegram〈[Telegram Mini Apps](https://core.telegram.org/bots/webapps)〉：啟動資料驗證與 Bot API 8.0 第三方驗證，官方文件。
- Apple〈[App Store Review Guidelines](https://developer.apple.com/app-store/review/guidelines/)〉與〈[2024-01-25 更新公告](https://developer.apple.com/news/?id=7j1f99yf)〉：4.7 條文，官方。
- Android〈[Insecure WebView native bridges](https://developer.android.com/privacy-and-security/risks/insecure-webview-native-bridges)〉與〈[Native API access with JavaScript bridge](https://developer.android.com/develop/ui/views/layout/webapps/native-api-access-jsbridge)〉：bridge 風險與 `addWebMessageListener` 的建議，官方文件。
- NVD〈[CVE-2012-6636](https://nvd.nist.gov/vuln/detail/CVE-2012-6636)〉：`addJavascriptInterface` 的漏洞說明。
- Zhang、Yang、Lin〈[Don't Leak Your Keys: Understanding, Measuring, and Exploiting the AppSecret Leaks in Mini-Programs](https://frostwing98.com/papers/CCS23.pdf)〉：ACM CCS 2023，同儕審查的大規模量測。
- NIST〈[SP 800-63B-4](https://pages.nist.gov/800-63-4/sp800-63b.html)〉與 OWASP〈[ASVS](https://owasp.org/www-project-application-security-verification-standard/)〉：驗證與應用程式安全標準。
- 全國法規資料庫〈[個人資料保護法](https://law.moj.gov.tw/LawClass/LawAll.aspx?pcode=I0050021)〉、〈[詐欺犯罪危害防制條例](https://law.moj.gov.tw/LawClass/LawAll.aspx?pcode=D0080226)〉：條文與修正沿革。
- 個人資料保護委員會籌備處〈[2025 年修正常見問答](https://ws.pdpc.gov.tw/FS01/FilePath/3/relfile/30/1072/a842b3f2-3399-4742-96ae-3f6648bd071a.pdf)〉：外洩通報與檢查權的官方說明。
- 數位發展部〈[指定網路廣告平臺業者公告](https://moda.gov.tw/press/bulletin/14160)〉：2024-09-16 指定的四家業者。
- 中央社〈[NCC 數位中介服務法草案退回重議](https://www.cna.com.tw/news/aipl/202209070201.aspx)〉：2022 年 9 月的決定，屬新聞報導。
- 中央社〈[資通安全管理法修正三讀](https://www.cna.com.tw/news/aipl/202508290240.aspx)〉：2025 年修法內容，屬新聞報導；條文見全國法規資料庫〈[資通安全管理法](https://law.moj.gov.tw/LawClass/LawAll.aspx?pcode=A0030297)〉。
- European Commission〈[TikTok Lite Rewards](https://digital-strategy.ec.europa.eu/en/news/tiktok-commits-permanently-withdraw-tiktok-lite-rewards-programme-eu-comply-digital-services-act)〉與〈[TikTok addictive design](https://digital-strategy.ec.europa.eu/en/news/commission-preliminarily-finds-tiktoks-addictive-design-breach-digital-services-act)〉：DSA 執法紀錄，官方。
- Irish Data Protection Commission〈[TikTok €530 million fine](https://www.dataprotection.ie/en/news-media/latest-news/irish-data-protection-commission-fines-tiktok-eu530-million-and-orders-corrective-measures-following)〉：2025-05-02 決定，官方。
- Australian Government〈[Social media minimum age](https://www.infrastructure.gov.au/media-communications/internet/online-safety/social-media-minimum-age)〉：16 歲以下限制的官方說明。

> 想法與技術判斷出自 Sheng，和 Claude 一起起草 · 範例在 Python 3.13.12、qdrant-client 1.19.1、fastembed 0.8.1 實測。