# 0.6.6 计划：Qwen/DashScope 多模态 embedding + hybrid 语义召回

> 状态：**已实现（0.6.6）** · 目标版本：`0.6.6` · 前置：`0.6.5`（AP 继承 / CLIP / 卡片包）
> 范围：DashScope 多模态 embedding 一等 provider、统一 `qwen3-vl-embedding` 做文本+图片向量、hybrid 语义召回扩展。

## 1. DashScope 多模态 provider

- 不再推荐单独使用纯文本 embedding：**同一个多模态模型同时给文本和图片出向量**：
  - `qwen3-vl-embedding`（推荐）
  - `tongyi-embedding-vision-flash`（备选）
- 原生协议（DashScope 不支持 OpenAI 兼容模式的这两个模型，实测 404）：
  ```json
  POST https://dashscope.aliyuncs.com/api/v1/services/embeddings/multimodal-embedding/multimodal-embedding
  { "model": "qwen3-vl-embedding",
    "input": { "contents": [ {"text": "红色海报"}, {"image": "data:image/png;base64,..."} ] } }
  ```
  响应：`{ "output": { "embeddings": [ { "index": 0, "embedding": [...] } ] } }`
- 新增配置：
  - `designEmbeddingProvider: openai | dashscope`（文本 hybrid 的 wire protocol）
  - `designClipProvider: custom | dashscope`（以图搜图/文本→图）
  - provider 为 `dashscope` 且 endpoint 为空时，自动使用官方 native endpoint；
  - key 仍从环境变量读（`DWG_DESIGN_EMBEDDING_KEY` / `DWG_DESIGN_CLIP_KEY`）。
- `qwen3.7-text-embedding-flash` 从推荐栈中移除；本仓库不写死任何模型名，配置决定。

## 2. hybrid 语义召回扩展

0.6.4/0.6.5 的 hybrid 只是 **rerank**：只重排 BM25 已命中的候选，零字面重合的卡永远进不来。
0.6.6 改成 **recall → union → rerank**：

1. BM25 取 top-30（lexical）；
2. 对全部 eligible 卡片做 `cardEmbeddingText`（标题+摘要+理由+tokens+tags+stylePriority+donts）向量化；
3. 按余弦取 top-30 语义候选；
4. 与 lexical 候选取并集（语义独有卡片构造 score=0 的候选）；
5. `(1-w)×归一化BM25 + w×(cos+1)/2` 融合排序，取 topK；
6. 缓存仍按 `model + sha256(text)`；成功后不再显示“未配置/回退”的旧提示，note 显示
   `hybrid 召回：lexical X + semantic Y（并集 Z）`。

## 验收标准（可证伪）

| ID | 判据 | 验证方式 |
| --- | --- | --- |
| DS-1 | provider=dashscope 时文本 hybrid 走 native contents 协议，模型与 `input.contents` 可观测 | 单测：loopback 记录请求体 |
| DS-2 | 同一 provider 下 `imageText` 把图片 data URL 与文本放进同一 contents，并正确取回向量 | 单测：请求体 + 排序 |
| SR-1 | 查询与所有卡片零字面重合时，语义候选仍被召回并排第一 | 单测：BM25 无命中、embedding 命中 |
| SR-2 | 成功后 note 不再出现“hybrid 需要 embedding 端点”，且显示召回计数 | 单测：detail 断言 |
| SR-3 | 既有 lexical / hybrid 回退 / CLIP 自定义契约测试不回归 | 全量测试 |

## 实测限制（不伪造证据）

- 用当前账户实测 native 端点：
  - `tongyi-embedding-vision-flash` → 400 “Access denied … account … good standing / overdue payment”
  - `qwen3-vl-embedding` → 403 “Free quota exhausted”
- 说明**模型名与端点存在**，但该账户没有多模态额度；因此 0.6.6 的多模态路径只用 **mock 端点**做协议/逻辑测试，
  不宣称线上质量已验证。开通额度后可直接用同一配置实测。
- 纯文本兼容端点（`qwen3.7-text-embedding-flash`）此前实测 1024 维可用，但按用户要求从推荐栈移除。

## 兼容性

- 旧配置默认 `openai`/`custom`，行为不变；DashScope 是显式 opt-in；
- 语义召回多了“全卡向量化”的一次性成本（缓存，库大时首次较慢）；可用 `designEmbeddingTimeoutMs` 控制；
- `query.mjs` 仍为词法，不参与 embedding/召回扩展。
