|
|
@@ -0,0 +1,426 @@
|
|
|
+# Part A2:query-builder 前端弹层 Implementation Plan
|
|
|
+
|
|
|
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
+
|
|
|
+**Goal:** 在工作台顶栏「新建搜索」旁加「Query 规则」按钮,打开弹层:上半用维度 chips 设上下文(工具类型/模态/后缀 扁平单选 + 实质/形式 任意深度下钻),下半渲染 `动作×类型` 整张 27×50 正交表(tier 底色),点「评估高亮」用 Sonnet 给 tier≥1 格打分并高亮 `keep` 格,点单格或「搜全部达标」直接发起搜索。
|
|
|
+
|
|
|
+**Architecture:** 纯前端,全部加进单文件 `index.html`。复用现有 helper:`$("#id")`、`api(path,opt)`、`toast()`、`showTask(title,taskId,onDone)`(轮询 `/api/task_status`,完成回调)。后端接口(A1 已就绪):`GET /api/query_matrix`、`GET /api/category_tree?source_type=`、`POST /api/query_score`(返回 `{sel,task_id,cached}`)、`GET /api/query_score?sel=`(结果或 202)、`POST /api/run_search`(复用,默认 xhs,gzh×20、不传 mode_type)。
|
|
|
+
|
|
|
+**Tech Stack:** 原生 JS/HTML/CSS(同 index.html 现状,无框架)。无自动化前端测试——验证 = `node --check`(抽出的 script 语法)/ 浏览器手测(用户)。
|
|
|
+
|
|
|
+参考 spec:`docs/superpowers/specs/2026-06-18-query-builder-design.md`(Part A 前端、评分 Prompt、Image #1/#2 视觉)。
|
|
|
+
|
|
|
+**重要约定(已与用户确认):** `动作/类型` **只作正交表两轴,不做成 chips**。chips 仅 5 个维度:工具类型/模态/后缀(扁平单选)、实质/形式(下钻)。
|
|
|
+
|
|
|
+工作目录命令在 `examples/mode_workflow/`,提交在仓库根,分支 `dev-lxn`。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 实现前必读(现有模式,照抄风格)
|
|
|
+
|
|
|
+实现者先在 `index.html` 里读这些锚点,照其风格写新代码:
|
|
|
+- `const api = (p, opt) =>` (~2437):`await api("/api/x")` GET;`await api("/api/x",{method:"POST",body:JSON.stringify(b)})`。失败抛异常,`e.body?.error`/`e.status` 可取。
|
|
|
+- `function toast(msg, type, ms)` (~2422):`toast("...", "warn"|"error"|"info")`。
|
|
|
+- `function showTask(title, taskId, onDone)` (~3581):弹任务面板 + 轮询 `/api/task_status`,`done` 时调 `onDone()`。
|
|
|
+- `$("#btn-new-search").onclick` / `$("#s-go").onclick` (~3659):新建搜索 modal 开关 + 组 body + `POST /api/run_search` + `showTask`。**镜像它**。
|
|
|
+- `#search-modal` 的 HTML(`grep -n 'id="search-modal"' index.html`,~2347):modal 结构/类(`.modal-bg`/`.modal`)参照它。
|
|
|
+- 事件绑定集中在底部 `<script>` 的初始化区(`$("#m-process").onclick = ...` 一带,~3699)。新绑定加这里。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: 顶栏按钮 + 弹层骨架 + 维度常量
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `examples/mode_workflow/index.html`
|
|
|
+
|
|
|
+- [ ] **Step 1: 顶栏加按钮**
|
|
|
+
|
|
|
+在 `<button class="btn seal" id="btn-new-search"> + 新建搜索 </button>` 之后紧挨着加:
|
|
|
+
|
|
|
+```html
|
|
|
+ <button class="btn" id="btn-query-rule" style="margin-left:8px">Query 规则</button>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 加弹层 HTML(放在 `#search-modal` 那段 modal 之后)**
|
|
|
+
|
|
|
+```html
|
|
|
+ <!-- Query 规则组织器 弹层 -->
|
|
|
+ <div class="modal-bg" id="qr-modal" hidden>
|
|
|
+ <div class="modal" style="max-width:1180px;width:96vw;max-height:92vh;display:flex;flex-direction:column">
|
|
|
+ <h2>Query 词组织器</h2>
|
|
|
+ <div id="qr-dims" style="margin:6px 0 10px"></div>
|
|
|
+ <div style="display:flex;gap:8px;align-items:center;margin:6px 0 10px">
|
|
|
+ <button class="btn seal" id="qr-score">生成正交表 & 评估高亮</button>
|
|
|
+ <button class="btn" id="qr-search-all" hidden>搜全部达标</button>
|
|
|
+ <span id="qr-hint" style="color:#888;font-size:12px"></span>
|
|
|
+ <div style="flex:1"></div>
|
|
|
+ <button class="btn" id="qr-close">关闭</button>
|
|
|
+ </div>
|
|
|
+ <div id="qr-table-wrap" style="overflow:auto;flex:1;border:1px solid #eee;border-radius:8px"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 加维度常量 + 状态(放在底部 `<script>` 内、初始化区之上)**
|
|
|
+
|
|
|
+```javascript
|
|
|
+ /* ════ Query 规则组织器 ════ */
|
|
|
+ // 扁平维度词表(移植自 query-builder.html DIMS);实质/形式 改为接口下钻,不在此。
|
|
|
+ const QR_FLAT = [
|
|
|
+ { id: "tool_type", label: "工具类型", items: ["AI", "桌面 APP", "云端 Web", "API·CLI", "插件扩展"] },
|
|
|
+ { id: "modality", label: "模态", items: ["图片", "视频"] },
|
|
|
+ { id: "suffix", label: "后缀", items: ["怎么做"] },
|
|
|
+ ];
|
|
|
+ const QR_TREE = [
|
|
|
+ { id: "substance", label: "实质", source: "实质" },
|
|
|
+ { id: "form", label: "形式", source: "形式" },
|
|
|
+ ];
|
|
|
+ const qrState = {
|
|
|
+ flat: { tool_type: null, modality: null, suffix: null },
|
|
|
+ treePath: { substance: [], form: [] }, // 选中节点的祖先+自身 name 路径
|
|
|
+ treeData: { substance: null, form: null }, // 接口返回的 {tree:[...]}
|
|
|
+ matrix: null, // /api/query_matrix
|
|
|
+ scores: null, // 最近一次评分结果 cells
|
|
|
+ };
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 开关弹层绑定(初始化区,照 search-modal 写法)**
|
|
|
+
|
|
|
+```javascript
|
|
|
+ $("#btn-query-rule").onclick = () => { $("#qr-modal").hidden = false; qrOpen(); };
|
|
|
+ $("#qr-close").onclick = () => { $("#qr-modal").hidden = true; };
|
|
|
+ $("#qr-modal").onclick = (e) => { if (e.target === $("#qr-modal")) $("#qr-modal").hidden = true; };
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /Users/max_liu/max_liu/company/Agent
|
|
|
+git add examples/mode_workflow/index.html
|
|
|
+git commit -m "feat(mode_workflow): Query 规则 顶栏按钮 + 弹层骨架 + 维度常量"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: 维度 chips(扁平单选 + 实质/形式 下钻)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `examples/mode_workflow/index.html`
|
|
|
+
|
|
|
+- [ ] **Step 1: 加 chips 渲染 + 打开时拉数据**
|
|
|
+
|
|
|
+在 Query 规则 script 区加:
|
|
|
+
|
|
|
+```javascript
|
|
|
+ function qrChip(label, active, on) {
|
|
|
+ const b = document.createElement("button");
|
|
|
+ b.className = "chip" + (active ? " on" : "");
|
|
|
+ b.textContent = label;
|
|
|
+ b.onclick = on;
|
|
|
+ return b;
|
|
|
+ }
|
|
|
+ function renderQrDims() {
|
|
|
+ const root = $("#qr-dims");
|
|
|
+ root.innerHTML = "";
|
|
|
+ // 扁平维度
|
|
|
+ QR_FLAT.forEach((d) => {
|
|
|
+ const row = document.createElement("div");
|
|
|
+ row.className = "qr-row";
|
|
|
+ row.innerHTML = `<span class="qr-lab">· ${d.label}</span>`;
|
|
|
+ const wrap = document.createElement("span");
|
|
|
+ wrap.className = "qr-chips";
|
|
|
+ wrap.appendChild(qrChip("无", qrState.flat[d.id] == null, () => { qrState.flat[d.id] = null; renderQrDims(); }));
|
|
|
+ d.items.forEach((it) =>
|
|
|
+ wrap.appendChild(qrChip(it, qrState.flat[d.id] === it, () => { qrState.flat[d.id] = it; renderQrDims(); })));
|
|
|
+ row.appendChild(wrap);
|
|
|
+ root.appendChild(row);
|
|
|
+ });
|
|
|
+ // 实质/形式 下钻:每一层一行,展示当前层可选项;选中后再展开下一层
|
|
|
+ QR_TREE.forEach((d) => {
|
|
|
+ const data = qrState.treeData[d.id];
|
|
|
+ const path = qrState.treePath[d.id];
|
|
|
+ // 沿 path 定位到当前层的 children 列表(逐层)
|
|
|
+ let level = data ? data.tree : [];
|
|
|
+ const rows = [];
|
|
|
+ for (let depth = 0; ; depth++) {
|
|
|
+ const labtxt = depth === 0 ? `· ${d.label}` : "";
|
|
|
+ const row = document.createElement("div");
|
|
|
+ row.className = "qr-row";
|
|
|
+ row.innerHTML = `<span class="qr-lab">${labtxt}</span>`;
|
|
|
+ const wrap = document.createElement("span");
|
|
|
+ wrap.className = "qr-chips";
|
|
|
+ wrap.appendChild(qrChip("无", path.length === depth, () => { qrState.treePath[d.id] = path.slice(0, depth); renderQrDims(); }));
|
|
|
+ (level || []).forEach((node) =>
|
|
|
+ wrap.appendChild(qrChip(node.name, path[depth] === node.name,
|
|
|
+ () => { qrState.treePath[d.id] = [...path.slice(0, depth), node.name]; renderQrDims(); })));
|
|
|
+ row.appendChild(wrap);
|
|
|
+ rows.push(row);
|
|
|
+ // 进入已选中节点的下一层
|
|
|
+ const sel = (level || []).find((n) => n.name === path[depth]);
|
|
|
+ if (!sel || !(sel.children && sel.children.length)) break;
|
|
|
+ level = sel.children;
|
|
|
+ }
|
|
|
+ rows.forEach((r) => root.appendChild(r));
|
|
|
+ });
|
|
|
+ }
|
|
|
+ async function qrOpen() {
|
|
|
+ renderQrDims();
|
|
|
+ // 并行拉矩阵 + 两棵分类树(接口挂则该维度降级:提示不可选)
|
|
|
+ try { qrState.matrix = qrState.matrix || await api("/api/query_matrix"); }
|
|
|
+ catch (e) { return toast("内容树矩阵加载失败", "error"); }
|
|
|
+ for (const d of QR_TREE) {
|
|
|
+ if (qrState.treeData[d.id]) continue;
|
|
|
+ try { qrState.treeData[d.id] = await api("/api/category_tree?source_type=" + encodeURIComponent(d.source)); }
|
|
|
+ catch (e) { toast(`${d.label} 接口不可达,该维度暂不可选`, "warn"); }
|
|
|
+ }
|
|
|
+ renderQrDims();
|
|
|
+ renderQrTable();
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 加 chips/行的样式(放在页面 `<style>` 内,紧凑即可)**
|
|
|
+
|
|
|
+```css
|
|
|
+ .qr-row { display:flex; align-items:flex-start; gap:10px; padding:5px 0; border-bottom:1px solid #f4f4f4; }
|
|
|
+ .qr-lab { min-width:64px; color:#999; font-size:12.5px; padding-top:6px; flex-shrink:0; }
|
|
|
+ .qr-chips { display:flex; flex-wrap:wrap; gap:7px; flex:1; }
|
|
|
+ .chip { padding:4px 13px; border-radius:18px; font-size:12.5px; cursor:pointer; border:1.5px solid #e2e2e2; background:#fff; color:#666; }
|
|
|
+ .chip:hover { border-color:#bbb; color:#333; }
|
|
|
+ .chip.on { background:#1a1a1a; border-color:#1a1a1a; color:#fff; }
|
|
|
+```
|
|
|
+
|
|
|
+> 若 `.chip` 类已存在(被别处用),复用即可、勿重复定义;只补 `.qr-row/.qr-lab/.qr-chips`。
|
|
|
+
|
|
|
+- [ ] **Step 3: 浏览器手测占位(语法检查)**
|
|
|
+
|
|
|
+Run(抽出 `<script>` 做 JS 语法检查,需 node):
|
|
|
+```bash
|
|
|
+cd /Users/max_liu/max_liu/company/Agent/examples/mode_workflow
|
|
|
+python3 - <<'PY'
|
|
|
+import re, pathlib, subprocess, tempfile, os
|
|
|
+html = pathlib.Path("index.html").read_text(encoding="utf-8")
|
|
|
+scripts = re.findall(r"<script>(.*?)</script>", html, re.S)
|
|
|
+src = "\n".join(scripts)
|
|
|
+f = tempfile.NamedTemporaryFile("w", suffix=".js", delete=False, encoding="utf-8"); f.write(src); f.close()
|
|
|
+r = subprocess.run(["node", "--check", f.name], capture_output=True, text=True)
|
|
|
+os.unlink(f.name)
|
|
|
+print("✔ JS 语法 OK" if r.returncode == 0 else "✗ 语法错:\n" + r.stderr)
|
|
|
+PY
|
|
|
+```
|
|
|
+Expected: `✔ JS 语法 OK`(若无 node 可跳过,记为手测项)。
|
|
|
+
|
|
|
+- [ ] **Step 4: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /Users/max_liu/max_liu/company/Agent
|
|
|
+git add examples/mode_workflow/index.html
|
|
|
+git commit -m "feat(mode_workflow): Query 规则 维度 chips(扁平单选 + 实质/形式 接口下钻)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 3: 动作×类型 正交表渲染
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `examples/mode_workflow/index.html`
|
|
|
+
|
|
|
+- [ ] **Step 1: 加拼词 + 建表函数**
|
|
|
+
|
|
|
+```javascript
|
|
|
+ // 单格 query = [工具类型] 动作叶 类型叶 [模态] [后缀](与后端 query_score._build_cells 一致)
|
|
|
+ function qrCellQuery(action, type) {
|
|
|
+ const f = qrState.flat;
|
|
|
+ return [f.tool_type, action, type, f.modality, f.suffix].filter((x) => x && x !== "无").join(" ");
|
|
|
+ }
|
|
|
+ function renderQrTable() {
|
|
|
+ const m = qrState.matrix;
|
|
|
+ if (!m) return;
|
|
|
+ const { actions, types, matrix } = m;
|
|
|
+ const sc = qrState.scores; // {"ai_ti":{keep,score,rewrite,...}}
|
|
|
+ // 表头:动作按 l1 分组(参考 Image #2,列头两行:l1 跨列 + 叶子)
|
|
|
+ let html = '<table class="qr-tab"><thead>';
|
|
|
+ // l1 行
|
|
|
+ html += '<tr><th class="qr-corner" rowspan="2">类型 \\ 动作</th>';
|
|
|
+ const groups = [];
|
|
|
+ actions.forEach((a, ai) => {
|
|
|
+ const last = groups[groups.length - 1];
|
|
|
+ if (last && last.l1 === a.l1) last.span++;
|
|
|
+ else groups.push({ l1: a.l1, span: 1, start: ai });
|
|
|
+ });
|
|
|
+ groups.forEach((g) => { html += `<th colspan="${g.span}" class="qr-gh">${g.l1}</th>`; });
|
|
|
+ html += "</tr><tr>";
|
|
|
+ actions.forEach((a) => { html += `<th class="qr-ah">${a.name}</th>`; });
|
|
|
+ html += "</tr></thead><tbody>";
|
|
|
+ // 行:类型按 l1 分组,组首插一行组标题
|
|
|
+ let curL1 = null;
|
|
|
+ types.forEach((t, ti) => {
|
|
|
+ if (t.l1 !== curL1) {
|
|
|
+ curL1 = t.l1;
|
|
|
+ html += `<tr><td class="qr-tl1" colspan="${actions.length + 1}">${t.l1}</td></tr>`;
|
|
|
+ }
|
|
|
+ html += `<tr><th class="qr-th">${t.name}</th>`;
|
|
|
+ actions.forEach((a, ai) => {
|
|
|
+ const cell = matrix[ai][ti] || {};
|
|
|
+ const tier = cell.tier || 0;
|
|
|
+ const q = qrCellQuery(a.name, t.name);
|
|
|
+ const v = sc ? sc[`${ai}_${ti}`] : null;
|
|
|
+ const keep = v && v.keep;
|
|
|
+ const cls = `qr-c t${tier}` + (tier === 0 ? " dead" : "") + (keep ? " keep" : "");
|
|
|
+ const badge = keep && v.score != null ? `<i class="qr-bd">${v.score}</i>` : "";
|
|
|
+ const title = v ? `${v.reason || ""} (n${v.natural}/f${v.findable}/u${v.useful})` : `tier ${tier}`;
|
|
|
+ html += `<td class="${cls}" data-ai="${ai}" data-ti="${ti}" title="${title.replace(/"/g, """)}">`
|
|
|
+ + `${q}${badge}</td>`;
|
|
|
+ });
|
|
|
+ html += "</tr>";
|
|
|
+ });
|
|
|
+ html += "</tbody></table>";
|
|
|
+ $("#qr-table-wrap").innerHTML = html;
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 加表格样式**
|
|
|
+
|
|
|
+```css
|
|
|
+ .qr-tab { border-collapse:collapse; font-size:11px; }
|
|
|
+ .qr-tab th, .qr-tab td { border:1px solid #eee; padding:3px 6px; white-space:nowrap; }
|
|
|
+ .qr-tab thead th { position:sticky; top:0; background:#f7f7f7; z-index:2; }
|
|
|
+ .qr-corner { position:sticky; left:0; z-index:3; background:#f0f0f0; }
|
|
|
+ .qr-th { position:sticky; left:0; background:#fafafa; z-index:1; text-align:left; }
|
|
|
+ .qr-tl1 { background:#f3f4f6; font-weight:600; color:#444; }
|
|
|
+ .qr-c.t0 { background:#fafafa; color:#bbb; } .qr-c.t1 { background:#f1f8f1; }
|
|
|
+ .qr-c.t2 { background:#dcecdc; } .qr-c.t3 { background:#bfe0bf; }
|
|
|
+ .qr-c.dead { color:#ccc; } .qr-c.keep { outline:2px solid #2563eb; outline-offset:-2px; cursor:pointer; font-weight:600; }
|
|
|
+ .qr-bd { display:inline-block; margin-left:4px; font-style:normal; font-size:9px; color:#2563eb; }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 语法检查 + 提交**
|
|
|
+
|
|
|
+Run: 重跑 Task 2 Step 3 的 `node --check` 片段 → `✔ JS 语法 OK`。
|
|
|
+```bash
|
|
|
+cd /Users/max_liu/max_liu/company/Agent
|
|
|
+git add examples/mode_workflow/index.html
|
|
|
+git commit -m "feat(mode_workflow): Query 规则 动作×类型 27×50 正交表渲染(tier 底色)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 4: 评估高亮(POST query_score + 轮询 + 渲染)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `examples/mode_workflow/index.html`
|
|
|
+
|
|
|
+- [ ] **Step 1: 绑定「生成正交表 & 评估高亮」**
|
|
|
+
|
|
|
+```javascript
|
|
|
+ function qrSelBody() {
|
|
|
+ return {
|
|
|
+ tool_type: qrState.flat.tool_type || "",
|
|
|
+ modality: qrState.flat.modality || "",
|
|
|
+ suffix: qrState.flat.suffix || "",
|
|
|
+ substance_path: qrState.treePath.substance,
|
|
|
+ form_path: qrState.treePath.form,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ async function qrLoadScores(sel) {
|
|
|
+ const r = await api("/api/query_score?sel=" + encodeURIComponent(sel));
|
|
|
+ if (r && r.pending) return false;
|
|
|
+ qrState.scores = r.cells || {};
|
|
|
+ renderQrTable();
|
|
|
+ const kept = Object.values(qrState.scores).filter((v) => v.keep).length;
|
|
|
+ $("#qr-hint").textContent = `评估完成:keep ${kept} 格 · $${r.cost_usd ?? "?"}`;
|
|
|
+ $("#qr-search-all").hidden = kept === 0;
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ $("#qr-score").onclick = async () => {
|
|
|
+ renderQrTable(); // 先按当前 chips 刷新拼词
|
|
|
+ $("#qr-hint").textContent = "提交评分…";
|
|
|
+ let r;
|
|
|
+ try { r = await api("/api/query_score", { method: "POST", body: JSON.stringify(qrSelBody()) }); }
|
|
|
+ catch (e) { return toast("评分启动失败:" + (e.body?.error || e.status), "error"); }
|
|
|
+ if (r.cached) { await qrLoadScores(r.sel); return; }
|
|
|
+ showTask("Query 评分 · 643 格", r.task_id, async () => { await qrLoadScores(r.sel); });
|
|
|
+ };
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 语法检查 + 提交**
|
|
|
+
|
|
|
+Run: `node --check` 片段 → `✔ JS 语法 OK`。
|
|
|
+```bash
|
|
|
+cd /Users/max_liu/max_liu/company/Agent
|
|
|
+git add examples/mode_workflow/index.html
|
|
|
+git commit -m "feat(mode_workflow): Query 规则 评估高亮(POST query_score + 轮询 + keep 高亮)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 5: 发起搜索(点单格 + 搜全部达标)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `examples/mode_workflow/index.html`
|
|
|
+
|
|
|
+- [ ] **Step 1: 点高亮格搜 + 批量搜(事件委托 + 按钮)**
|
|
|
+
|
|
|
+```javascript
|
|
|
+ async function qrRunSearch(query) {
|
|
|
+ const body = { query, platforms: "xhs,gzh", max_count: 20 }; // 方向无关,标签自动路由
|
|
|
+ const r = await api("/api/run_search", { method: "POST", body: JSON.stringify(body) });
|
|
|
+ return r; // {task_id, query_id}
|
|
|
+ }
|
|
|
+ // 点高亮格 → 确认 → 搜
|
|
|
+ $("#qr-table-wrap").onclick = async (e) => {
|
|
|
+ const td = e.target.closest("td.qr-c.keep");
|
|
|
+ if (!td) return;
|
|
|
+ const v = qrState.scores[`${td.dataset.ai}_${td.dataset.ti}`];
|
|
|
+ const query = (v && v.rewrite) || td.textContent.replace(/\d+(\.\d+)?$/, "").trim();
|
|
|
+ if (!confirm(`发起搜索(小红书+公众号 各20):\n${query}`)) return;
|
|
|
+ try {
|
|
|
+ const r = await qrRunSearch(query);
|
|
|
+ showTask(`搜索 · ${r.query_id} ${query}`, r.task_id, null);
|
|
|
+ } catch (err) { toast("搜索启动失败:" + (err.body?.error || err.status), "error"); }
|
|
|
+ };
|
|
|
+ // 搜全部达标:逐格起任务(朴素循环,失败不阻断)
|
|
|
+ $("#qr-search-all").onclick = async () => {
|
|
|
+ const keeps = Object.entries(qrState.scores || {}).filter(([, v]) => v.keep);
|
|
|
+ if (!keeps.length) return;
|
|
|
+ if (!confirm(`将对 ${keeps.length} 个达标 query 各起一次搜索(小红书+公众号 各20),确认?`)) return;
|
|
|
+ let ok = 0;
|
|
|
+ for (const [, v] of keeps) {
|
|
|
+ try { await qrRunSearch(v.rewrite || v.query); ok++; }
|
|
|
+ catch (e) { /* 单格失败不阻断 */ }
|
|
|
+ }
|
|
|
+ toast(`已发起 ${ok}/${keeps.length} 个搜索任务`, "info");
|
|
|
+ };
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 语法检查 + 提交**
|
|
|
+
|
|
|
+Run: `node --check` 片段 → `✔ JS 语法 OK`。
|
|
|
+```bash
|
|
|
+cd /Users/max_liu/max_liu/company/Agent
|
|
|
+git add examples/mode_workflow/index.html
|
|
|
+git commit -m "feat(mode_workflow): Query 规则 点格搜索 + 搜全部达标(run_search 方向无关)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 6: 浏览器手测清单(交用户)
|
|
|
+
|
|
|
+**Files:** 无(用户在浏览器验证;server 需在跑)
|
|
|
+
|
|
|
+- [ ] **手测项(重启 server 后于 http://localhost:8772 操作)**
|
|
|
+
|
|
|
+1. 顶栏「Query 规则」打开弹层;5 个维度 chips 出现;实质/形式 选一层后**自动展开下一层**。
|
|
|
+2. 下半出现 27×50 表,tier 底色梯度(0 灰 / 1~3 渐绿),tier0 格灰显。
|
|
|
+3. 选若干 chips(如 工具类型=AI、后缀=怎么做),点「生成正交表 & 评估高亮」→ 任务面板轮询 → 完成后 `keep` 格蓝框 + 角标分数;hint 显示 keep 数与成本。
|
|
|
+4. 再点一次同样选择 → 秒回(命中缓存,无新任务)。
|
|
|
+5. 点某个蓝框格 → 确认框显示 query → 确认后起搜索任务(任务面板可见)。
|
|
|
+6. 「搜全部达标」→ 确认 → toast 显示已发起 N 个任务。
|
|
|
+7. 切到 Dataset 工序/工具 tab,能看到新搜出的 query 与按标签入库的帖子。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Self-Review
|
|
|
+
|
|
|
+- **Spec coverage(Part A 前端)**:顶栏按钮+弹层 → Task 1 ✓;chips(工具类型/模态/后缀 扁平 + 实质/形式 接口下钻任意深度)→ Task 2 ✓;27×50 正交表 l1 分组+tier 底色+tier0 灰 → Task 3 ✓;拼词与后端一致(`qrCellQuery` == `_build_cells`)→ Task 3 ✓;评估高亮(POST→轮询→GET→keep 描边+角标,缓存秒回)→ Task 4 ✓;点单格 + 搜全部达标(xhs,gzh×20、方向无关、rewrite 优先)→ Task 5 ✓;接口降级提示 → Task 2 `qrOpen` catch ✓。`动作/类型` 仅作表轴不进 chips(用户确认)✓。
|
|
|
+- **Placeholder scan**:无 TBD;每步给完整代码或精确 HTML/CSS/JS。CSS 若与现有 `.chip` 冲突,Task 2 Step 2 已注明复用不重复定义。
|
|
|
+- **Type consistency**:评分结果键 `${ai}_${ti}`(Task 3/4/5 一致,对齐后端 `<a_idx>_<t_idx>`);`qrSelBody()` 字段(tool_type/modality/suffix/substance_path/form_path)对齐 `POST /api/query_score`;`qrRunSearch` body(query/platforms/max_count,无 mode_type)对齐 Part B 后的 run_search;复用 `showTask(title,taskId,onDone)` 签名一致。
|