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)。新绑定加这里。Files:
Modify: examples/mode_workflow/index.html
[ ] Step 1: 顶栏加按钮
在 <button class="btn seal" id="btn-new-search"> + 新建搜索 </button> 之后紧挨着加:
<button class="btn" id="btn-query-rule" style="margin-left:8px">Query 规则</button>
#search-modal 那段 modal 之后) <!-- 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>
<script> 内、初始化区之上) /* ════ 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
};
$("#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; };
cd /Users/max_liu/max_liu/company/Agent
git add examples/mode_workflow/index.html
git commit -m "feat(mode_workflow): Query 规则 顶栏按钮 + 弹层骨架 + 维度常量"
Files:
Modify: examples/mode_workflow/index.html
[ ] Step 1: 加 chips 渲染 + 打开时拉数据
在 Query 规则 script 区加:
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();
}
<style> 内,紧凑即可) .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。
Run(抽出 <script> 做 JS 语法检查,需 node):
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 可跳过,记为手测项)。
cd /Users/max_liu/max_liu/company/Agent
git add examples/mode_workflow/index.html
git commit -m "feat(mode_workflow): Query 规则 维度 chips(扁平单选 + 实质/形式 接口下钻)"
Files:
Modify: examples/mode_workflow/index.html
[ ] Step 1: 加拼词 + 建表函数
// 单格 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;
}
.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; }
Run: 重跑 Task 2 Step 3 的 node --check 片段 → ✔ JS 语法 OK。
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 底色)"
Files:
Modify: examples/mode_workflow/index.html
[ ] Step 1: 绑定「生成正交表 & 评估高亮」
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); });
};
Run: node --check 片段 → ✔ JS 语法 OK。
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 高亮)"
Files:
Modify: examples/mode_workflow/index.html
[ ] Step 1: 点高亮格搜 + 批量搜(事件委托 + 按钮)
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");
};
Run: node --check 片段 → ✔ JS 语法 OK。
cd /Users/max_liu/max_liu/company/Agent
git add examples/mode_workflow/index.html
git commit -m "feat(mode_workflow): Query 规则 点格搜索 + 搜全部达标(run_search 方向无关)"
Files: 无(用户在浏览器验证;server 需在跑)
keep 格蓝框 + 角标分数;hint 显示 keep 数与成本。qrCellQuery == _build_cells)→ Task 3 ✓;评估高亮(POST→轮询→GET→keep 描边+角标,缓存秒回)→ Task 4 ✓;点单格 + 搜全部达标(xhs,gzh×20、方向无关、rewrite 优先)→ Task 5 ✓;接口降级提示 → Task 2 qrOpen catch ✓。动作/类型 仅作表轴不进 chips(用户确认)✓。.chip 冲突,Task 2 Step 2 已注明复用不重复定义。${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) 签名一致。