feat: publish xianyu hunter hermes skill package

This commit is contained in:
2026-05-06 19:26:45 +08:00
commit 6384156097
59 changed files with 9627 additions and 0 deletions
@@ -0,0 +1,68 @@
# Cookie JSON Import Panel and Timeout Pitfall — 2026-05
Context: while adding Web UI account controls for the local BOSS `xianyu-hunter` fork, `POST /api/login/import-cookies` initially timed out even for valid Cookie JSON.
## Durable lesson
Do **not** make the Cookie JSON import endpoint perform a real Goofish page navigation immediately after `ctx.addCookies()`. In practice this can hang for 2030s+ because the persistent Playwright context may already have a stale/blocked tab, Goofish network verification may stall, or the login probe page may hit a login wall/captcha.
Instead, import should be a fast local persistence operation:
1. Parse payload as either `[...]`, `{ "cookies": [...] }`, or `{ "data": [...] }`.
2. Normalize cookie fields for Playwright (`name`, `value`, `domain`, `path`, `secure`, `httpOnly`, `sameSite`, optional `expires`).
3. `await ctx.addCookies(cookies)`.
4. Return local status from `await ctx.cookies()` only: imported count, relevant Goofish/Taobao/Alibaba cookie count, timestamp, and storage path.
5. Tell the UI/user to click **检测登录状态** for the slower network probe.
## Known-good response shape
For valid import, return quickly with JSON similar to:
```json
{
"ok": true,
"imported": 1,
"loggedIn": true,
"cookieCount": 10,
"url": "cookie-import://local",
"title": "Cookie 已导入,点击“检测登录状态”可联网验证",
"storage": "browser-data/"
}
```
`loggedIn` here is only a heuristic from cookie count, not a real network login verification.
## Verification commands
Run from `/Users/chick/.Hermes/workspace/research/xianyu-hunter`:
```bash
node --check lib/login.mjs && node --check server.mjs && node --check public/app.js
curl -sS -i -m 10 -X POST http://127.0.0.1:3000/api/login/import-cookies \
-H 'Content-Type: application/json' \
-d '{}' | sed -n '1,60p'
curl -sS -i -m 10 -X POST http://127.0.0.1:3000/api/login/import-cookies \
-H 'Content-Type: application/json' \
-d '{"cookies":[{"name":"test_cookie_for_import_probe","value":"1","domain":".goofish.com","path":"/"}]}' | sed -n '1,120p'
curl -sS -m 10 http://127.0.0.1:3000/ | grep -E '上传 Cookie JSON|账号登录 / Cookie'
```
Expected:
- Invalid payload returns HTTP 400 with the Chinese format error.
- Valid payload returns HTTP 200 within 10 seconds.
- UI contains `账号登录 / Cookie` and `上传 Cookie JSON`.
## Restart pitfall
If changes appear not to take effect, kill the exact listener PID on port 3000. The wrapper/background shell may leave a stale `node server.mjs` and Playwright Chromium process alive, so a normal restart can accidentally leave the old code serving requests.
```bash
for p in $(lsof -tiTCP:3000 -sTCP:LISTEN 2>/dev/null); do kill "$p" 2>/dev/null || true; done
sleep 1
for p in $(lsof -tiTCP:3000 -sTCP:LISTEN 2>/dev/null); do kill -9 "$p" 2>/dev/null || true; done
PORT=3000 node server.mjs
```
@@ -0,0 +1,101 @@
# Hermes 主控重构记录(2026-05
## 背景
BOSS 明确希望闲鱼采集项目由 Hermes 直接操作:登录检查、创建任务、分析结果、定时执行;Web UI 仅作为备用渠道。
## 当前本地项目定位
工作目录:
```bash
/Users/chick/.Hermes/workspace/research/xianyu-hunter
```
当前模式:
- Hermes 是主控入口。
- Web UI 仅备用查看。
- 项目不需要配置 AI API / API Key / 模型。
- 项目不会自动联系卖家、不会聊天、不会询价。
- 项目只负责:搜索采集、详情采集、本地保守预筛、持久化结果。
- 深度判断由 Hermes 根据 `data/tasks.json` 或报告完成。
## 新增/主要入口
统一 CLI
```bash
node tools/xianyu_ops.mjs <command>
```
命令:
```bash
node tools/xianyu_ops.mjs status --port 3000
node tools/xianyu_ops.mjs start --port 3000
node tools/xianyu_ops.mjs stop
node tools/xianyu_ops.mjs login
node tools/xianyu_ops.mjs create --name '<任务名>' --queries '<关键词1,关键词2>' --requirements '<需求>'
node tools/xianyu_ops.mjs run --name '<任务名>' --queries '<关键词1,关键词2>' --requirements '<需求>' --limit 30
node tools/xianyu_ops.mjs report --task <task_id> --limit 30
```
`run` 会:检查/启动服务 → 创建任务 → 等待完成 → 生成报告。
## 报告与定时
报告脚本:
```bash
node tools/hermes_report.mjs --task <task_id> --limit 30
```
报告输出:
```text
reports/<task_id>-hermes-report.md
```
定时任务提示模板:
```text
tools/cron_prompt_template.md
```
定时任务要显式声明:不联系卖家、不下单、不承诺交易;如遇登录/验证码,报告需要人工处理,不绕过验证。
## 已验证
已做语法检查:
```bash
node --check server.mjs
node --check lib/task.mjs
node --check lib/filter.mjs
node --check public/app.js
node --check tools/xianyu_ops.mjs
node --check tools/hermes_report.mjs
```
已验证服务可启动:
```bash
node tools/xianyu_ops.mjs start --port 3000
node tools/xianyu_ops.mjs status --port 3000
```
注意:真实闲鱼端到端采集仍取决于账号登录状态、验证码和 Goofish DOM 选择器是否仍有效。
## 未来会话触发语
用户说以下任意内容时,应加载 `xianyu-hunter-monitor`
- “帮我监测闲鱼 …”
- “蹲一下闲鱼 …”
- “分析最新闲鱼采集结果”
- “检查闲鱼登录状态”
- “启动闲鱼助手”
- “每 X 小时监测 …”
优先用 `tools/xianyu_ops.mjs`,不要让 BOSS 手动打开 Web 除非需要查看或排错。
@@ -0,0 +1,115 @@
# Xianyu-hunter Hermes send_message notifications (2026-05)
Session learning: BOSS wanted xianyu-hunter notification settings changed from a Hermes webhook URL to a Web-visible platform selector that sends through Hermes `send_message` directly.
## Implementation pattern
- Backend file: `/Users/chick/.Hermes/workspace/research/xianyu-hunter/lib/notifier.mjs`.
- Keep `GET/PUT /api/notify-config`, but add `targetOptions` to the returned config for UI dropdowns.
- Use channel type `hermes-send-message` instead of `hermes-webhook`.
- Migrate existing `hermes-webhook` channels to `hermes-send-message` and map bare `feishu` to the current Feishu DM when appropriate.
- Server-side send implementation can spawn Hermes Agent's Python venv and call the tool handler:
```js
const script = `import json\nfrom tools.send_message_tool import send_message_tool\nprint(send_message_tool({"action":"send","target":${JSON.stringify(target)},"message":${JSON.stringify(message)}}))\n`;
const child = spawn(
process.env.HERMES_PYTHON || '/Users/chick/.hermes/hermes-agent/venv/bin/python3',
['-c', script],
{ cwd: process.env.HERMES_AGENT_DIR || '/Users/chick/.hermes/hermes-agent' }
);
```
## UI pattern
- Frontend files: `public/app.js`, `public/index.html`.
- In notification modal, label Hermes channel as **Hermes 平台消息**.
- Provide a dropdown populated from `notifyConfig.targetOptions` and a separate optional custom target input.
- Target examples:
- `feishu:oc_3e963a150d98f254d8f38881bcfa1812`
- `weixin:o9cq809DWSYDlvbA9pj1akncjrhY@im.wechat`
- `weixin:o9cq808qCd1W-cIDo0iWjteE5lHY@im.wechat`
- `qqbot` if configured
- Bump static cache query strings (`app.js?v=...`, `style.css?v=...`) after UI edits.
## Verification
```bash
cd /Users/chick/.Hermes/workspace/research/xianyu-hunter
node --check server.mjs
node --check lib/notifier.mjs
node --check public/app.js
node tools/xianyu_ops.mjs stop || true
node tools/xianyu_ops.mjs start --port 3000
curl -sS http://127.0.0.1:3000/api/notify-config | python3 -m json.tool | head -90
```
Smoke-test notifier without running a real task:
```bash
node --input-type=module <<'JS'
import { sendNotifications } from './lib/notifier.mjs';
const task = { id: 'smoke-test', name: '通知配置 smoke test' };
const summary = {
minScore: 75, total: 1, recommendedCount: 1, cautionCount: 0, rejectedCount: 0,
reportPath: 'smoke-test',
topItems: [{ title: 'xianyu-hunter 通知测试', score: 99, verdict: 'recommend', price: '测试', location: '本机', reason: '验证服务端可直接调用 Hermes send_message', href: '' }]
};
const config = { enabled: true, notifyOnlyGood: false, minScore: 75, channels: [{ id:'smoke-feishu', type:'hermes-send-message', name:'飞书 smoke', enabled:true, target:'feishu:oc_3e963a150d98f254d8f38881bcfa1812' }] };
const r = await sendNotifications(task, summary, { force: true, config });
console.log(JSON.stringify(r, null, 2));
if (!r.ok) process.exit(1);
JS
```
## Test notification channel
When BOSS asks to add/test notification channels, add a Web-visible test path rather than relying on real task analysis:
- Backend route in `server.mjs`:
- `POST /api/notify-test`
- Accepts `{ config, title }` where `config` has the same shape as `/api/notify-config`.
- Builds a fake task `{ id: 'notify-test', name: '通知渠道测试' }` plus a one-item `summary`.
- Calls `sendNotifications(task, summary, { force: true, config: { ...config, enabled: true, notifyOnlyGood: false } })` so the test bypasses score/good-only gating.
- Return HTTP 200 if `result.ok`, otherwise 500 with per-channel errors.
- Frontend in `public/app.js`:
- Add `testNotifyConfigFromForm()` beside `saveNotifyConfigFromForm()`.
- It calls `collectNotifyConfig()`, requires at least one enabled channel, posts to `/api/notify-test`, and writes ✅/❌ per-channel results into `#notify-log`.
- It should not require saving first; this lets BOSS test temporary target edits directly from the modal.
- UI in `public/index.html`:
- Add a modal footer button: `发送测试通知` wired to `testNotifyConfigFromForm()`.
- Bump static cache query strings (`app.js?v=...`, `style.css?v=...`).
Verification command:
```bash
curl -sS -X POST http://127.0.0.1:3000/api/notify-test \
-H 'Content-Type: application/json' \
-d '{"config":{"enabled":true,"notifyOnlyGood":false,"minScore":75,"channels":[{"id":"api-test-feishu","type":"hermes-send-message","name":"API测试飞书","enabled":true,"target":"feishu:oc_3e963a150d98f254d8f38881bcfa1812"}]},"title":"xianyu-hunter API 测试通知"}' \
| python3 -m json.tool
```
A known-good result has `ok: true` and `results[0].ok: true`; in the 2026-05 run this sent a real Feishu test message successfully.
Do **not** spawn Homebrew Python such as `/Users/chick/homebrew/opt/python@3.11/bin/python3.11` for Hermes `send_message`. It may lack Hermes dependencies (`yaml`, gateway modules) and fail with:
```text
Failed to load gateway config: No module named 'yaml'
```
Use Hermes' own venv interpreter:
```text
/Users/chick/.hermes/hermes-agent/venv/bin/python3
```
You can verify it with:
```bash
/Users/chick/.hermes/hermes-agent/venv/bin/python3 - <<'PY'
import yaml
import gateway.config
from tools.send_message_tool import send_message_tool
print(send_message_tool({'action':'list'}))
PY
```
@@ -0,0 +1,78 @@
# Goofish Login UX, Browser Lock, and Partial-Result Recovery — 2026-05
## What happened
During a Goofish/Xianyu search task, a temporary helper opened a direct Taobao unified-login URL and sent its screenshot as a "real login QR". BOSS correctly objected that it looked like a Taobao login page and not the expected Xianyu entry.
Goofish web login is backed by Alibaba/Taobao account infrastructure, so `login.taobao.com` can be normal after following the Goofish login flow. However, for BOSS UX, do **not** present a direct Taobao URL/screenshot as if it were the Xianyu login entry.
## Preferred login workflow
1. Start/keep the dashboard service.
2. Open `https://www.goofish.com/` in the persistent Playwright profile (`browser-data/`).
3. Let BOSS interact with the visible browser window and click the Goofish page's own login/modal/QR controls.
4. If a screenshot is needed, label it accurately: "Goofish page login/modal; may redirect to Taobao unified login".
5. After BOSS says login is done, close any temporary helper Chromium using the same profile before running task automation.
6. Verify with `GET /api/login/status` against a real search page, not homepage only.
## Pitfall: ProcessSingleton / SingletonLock
If a temp QR/login helper is still using `browser-data/`, later dashboard/API/task runs may fail with:
```text
browserType.launchPersistentContext: Failed to create a ProcessSingleton for your profile directory
... browser-data/SingletonLock: File exists (17)
```
Safe recovery used in this session:
```bash
cd /Users/chick/.Hermes/workspace/research/xianyu-hunter
pkill -f 'xianyu_goofish_login_entry_tmp.mjs|xianyu_qr_login_tmp.mjs' || true
pkill -f 'Google Chrome for Testing.*xianyu-hunter/browser-data' || true
sleep 2
for p in $(lsof -tiTCP:3000 -sTCP:LISTEN 2>/dev/null); do kill "$p" 2>/dev/null || true; done
sleep 1
PORT=3000 node server.mjs
```
Then verify:
```bash
curl -sS -m 60 http://127.0.0.1:3000/api/login/status | python3 -m json.tool
```
A good result includes `loggedIn: true`, `loginWall: false`, nonzero `cookieCount`, and product cards on a search URL.
## Partial-result recovery
If a run is interrupted during `fine_filter`, `tools/hermes_report.mjs` may report `候选 0` because it only sees `fineFiltered`. Do not conclude there were no products. Inspect `data/tasks.json` and mine `products.coarseFiltered` or `products.raw` directly.
Example used for E3-1245v3:
```bash
python3 - <<'PY'
import json,re
from pathlib import Path
p=Path('data/tasks.json')
data=json.loads(p.read_text())
t=[x for x in data if x.get('id')=='<task_id>'][0]
items=t.get('products',{}).get('coarseFiltered',[]) or t.get('products',{}).get('raw',[])
def price_num(v):
m=re.search(r'(\d+(?:\.\d+)?)', str(v or '').replace(',',''))
return float(m.group(1)) if m else None
for it in items:
title=(it.get('title') or '').lower().replace('-',' ')
text=' '.join(str(it.get(k,'')) for k in ['title','description','detailDescription']).lower()
if '1245' in title and 'v3' in title and not any(w in text for w in ['回收','求购','主板','套装','整机','散热','维修','不包含 cpu']):
print(it.get('price') or it.get('detailPrice'), it.get('location') or it.get('sellerLocation'), it.get('title'), it.get('href') or it.get('url'))
PY
```
## Presentation guidance
When reporting results to BOSS:
- State the live status first (login ok, task id, counts raw/coarse/fine, whether run completed or was interrupted).
- If a report says 0 candidates but raw/coarse has data, explicitly explain that this is a stage/report limitation.
- Rank candidates by closeness to budget and risk, and call out exclusions (e.g. "不包含 CPU", "不能进系统", "回收", "主板套装").
@@ -0,0 +1,62 @@
# Real QR Login Capture for Xianyu/Goofish
Session learning: BOSS found Cookie JSON import inconvenient and asked for the real login QR code directly in chat. The web app's `POST /api/login/open` opens a visible Chromium window on the Mac, but that is not enough for Feishu/mobile use; generating a screenshot and returning it as `MEDIA:/tmp/xianyu-login-qr.png` is more convenient.
## Working pattern
From `/Users/chick/.Hermes/workspace/research/xianyu-hunter`:
1. Close the existing Playwright browser context through the app if it may hold the profile lock:
```bash
curl -sS -m 10 -X POST http://127.0.0.1:3000/api/login/close-browser \
-H 'Content-Type: application/json' -d '{}'
```
2. Run a Playwright helper from inside the repo, not `/tmp`, so `import { chromium } from 'playwright'` resolves against the project dependencies.
3. Launch a persistent context using the real project profile:
```js
const userDataDir = '/Users/chick/.Hermes/workspace/research/xianyu-hunter/browser-data';
const ctx = await chromium.launchPersistentContext(userDataDir, {
headless: false,
viewport: { width: 1100, height: 900 },
locale: 'zh-CN',
args: ['--disable-blink-features=AutomationControlled', '--no-first-run'],
});
```
4. Navigate to Taobao/Goofish login:
```js
const loginUrl = 'https://login.taobao.com/member/login.jhtml?redirectURL=' + encodeURIComponent('https://www.goofish.com/');
await page.goto(loginUrl, { waitUntil: 'domcontentloaded', timeout: 45000 });
```
5. If possible, click QR-login affordances such as `扫码登录`, `二维码登录`, `.icon-qrcode`, or `[class*=qrcode]`, wait a few seconds, then screenshot:
```js
await page.screenshot({ path: '/tmp/xianyu-login-qr.png', fullPage: false });
```
6. Keep the process alive for about 5 minutes so the QR remains valid while BOSS scans:
```js
await page.waitForTimeout(5 * 60 * 1000);
```
7. Reply with Feishu media syntax:
```text
MEDIA:/tmp/xianyu-login-qr.png
```
After BOSS scans, call `/api/login/status` or run the normal task/login verification.
## Pitfalls
- Running the helper from `/tmp` failed with `ERR_MODULE_NOT_FOUND: Cannot find package 'playwright'`; create/run the helper inside the xianyu-hunter repo or set module resolution explicitly.
- A persistent Chromium profile can be locked by the dashboard's existing browser context. Use `/api/login/close-browser` first or kill stale Goofish Playwright processes if navigation hangs.
- `vision_analyze` can fail due provider/API key issues; do not depend on it to validate the QR screenshot. If the screenshot exists and is recent, send it; BOSS can scan/confirm.
- Do not print or export raw cookies. QR login is a user-driven login flow and is preferable to asking BOSS to handle Cookie JSON when convenience is the goal.
@@ -0,0 +1,99 @@
# Web Hermes Analysis + Notifications (2026-05)
Session learning from extending BOSS's local `xianyu-hunter` fork with Web-visible analysis and notification controls.
## Product shape BOSS expects
When adding analysis/notification capability, make it visible in the Web dashboard, not only as backend APIs:
- Header button: **通知设置**.
- Task controls: **执行分析** beside edit/duplicate/start/rerun/delete.
- New task tab: **Hermes 分析** with recommendation/caution/reject summary and item cards.
- Product list should show analysis verdict badges and concise reasons inline.
- Analysis should persist into `data/tasks.json` and generate a Markdown report under `reports/`.
## Backend API pattern
Add a lightweight local analysis layer and notifier:
- `lib/analyzer.mjs`
- `buildAnalysis(task, { limit })` returns `{ items, summary }`.
- `renderAnalysisMarkdown(task, summary)` writes `reports/<task_id>-analysis.md`.
- Include item-level `hermesAnalysis: { score, verdict, reason, flags, positives, analyzedAt }`.
- `lib/notifier.mjs`
- `GET /api/notify-config` and `PUT /api/notify-config` backed by `data/notify-config.json`.
- `sendNotifications(task, summary, { force })` supports:
- `hermes-webhook`: POST `{ message, target, source, taskId }`.
- `bark`: POST `{ title, body, group }` to `https://api.day.app/<key>` or configured endpoint.
- `pushplus`: POST `{ token, title, content, template:'txt' }`.
- `webhook`: generic JSON `{ title, message, taskId }`.
- `server.mjs`
- `POST /api/tasks/:id/analyze` should call `taskManager.analyzeTask`, write report, persist summary, and optionally notify.
## TaskManager persistence details
- Add `getTaskFull(id)` for routes that need full in-memory task data.
- Add `setTaskAnalysis(id, analysis)` that merges item `hermesAnalysis` into `products.fineFiltered`, stores `task.hermesAnalysis`, persists to disk, and emits `task:updated`.
- Include `hermesAnalysis` in `_serialize`, `_serializeFull`, and `_loadFromDisk`.
- Clear `task.hermesAnalysis = null` when resetting products/rerunning a task.
- New/duplicated tasks should initialize `hermesAnalysis: null`.
## Frontend implementation details
- Bump asset query versions in `index.html` after changing CSS/JS to avoid stale browser cache (`style.css?v=11`, `app.js?v=11`, etc.).
- `public/app.js` helpers:
- `analyzeTaskNow(id)` calls `POST /api/tasks/:id/analyze` with `{ notify: true }`, then refreshes task detail.
- `renderAnalysis()` renders empty state, stat cards, report path, and scored item cards.
- When rendering raw product rows, map analysis from `products.fineFiltered` by `id`; raw items often do not contain the merged `hermesAnalysis` directly.
- Preserve notification channel `id` when saving from the form; otherwise repeated saves create new channel IDs.
## Verification checklist
Run syntax checks:
```bash
cd /Users/chick/.Hermes/workspace/research/xianyu-hunter
node --check server.mjs
node --check lib/task.mjs
node --check lib/analyzer.mjs
node --check lib/notifier.mjs
node --check public/app.js
```
Restart exact stale listener if `xianyu_ops.mjs stop` says `no-recorded-server` but port is still occupied:
```bash
pid=$(lsof -nP -iTCP:3000 -sTCP:LISTEN -t || true)
[ -z "$pid" ] || kill "$pid"
node tools/xianyu_ops.mjs start --port 3000
```
Smoke test APIs:
```bash
curl -fsS http://127.0.0.1:3000/api/notify-config | python3 -m json.tool
curl -fsS http://127.0.0.1:3000/api/tasks -o /tmp/xh_tasks.json
TASK=$(python3 - <<'PY'
import json
j=json.load(open('/tmp/xh_tasks.json'))
print(j[0]['id'] if j else '')
PY
)
curl -fsS -X POST "http://127.0.0.1:3000/api/tasks/$TASK/analyze" \
-H 'Content-Type: application/json' \
-d '{"notify":false,"limit":10}' | python3 -m json.tool
```
Important pitfall: piping a large `/api/tasks` response directly into `python3 - <<'PY'` consumes stdin for the script, not JSON. Save curl output to a temp file first.
## Model-specific scoring pitfall: E3-1245v3
For CPU searches like `E3-1245v3`, scoring must distinguish true target models from related models used as SEO keywords.
Rules added in `lib/analyzer.mjs`:
- If the task query indicates `E3-1245v3`, require an exact-ish match: `e3\s*-?\s*1245\s*[_-]?\s*v3`.
- Penalize titles whose main model is another E3 v3, e.g. `E3-1275v3 ... 性能超 E3-1245v3`, because the title may only use 1245v3 as a comparison/keyword.
- Flag failure terms beyond obvious `坏/不亮/暗病`, including `不能进系统`.
This reduced false recommendations in the E3-1245v3 task from related CPUs and broken parts.
@@ -0,0 +1,62 @@
# Web Cache Cleanup Entry — 2026-05
BOSS requested a Web UI entry to clear xianyu-hunter cached collection results while optionally preserving Hermes-approved quality results.
Implemented in local clone:
```text
/Users/chick/.Hermes/workspace/research/xianyu-hunter
```
Files changed:
- `lib/task.mjs`
- Added `TaskManager.clearTaskCache(id, { mode, minScore })`.
- Refuses running tasks by returning `null`.
- `mode: 'all'`: clears `products.raw`, `products.coarseFiltered`, `products.fineFiltered`, `hermesAnalysis`, chat arrays; resets stage to `pending`.
- `mode: 'hermes-recommend'`: keeps only `fineFiltered` items with `item.hermesAnalysis.verdict === 'recommend'`.
- `mode: 'hermes-pass'`: keeps `recommend` or `caution` items whose score is >= `minScore`.
- For keep modes, retained items are written to all three product buckets so the task still visibly has cached quality results.
- Adds a log line and broadcasts `task:updated`.
- `server.mjs`
- Added `POST /api/tasks/:id/clear-cache` with JSON body:
```json
{ "mode": "all|hermes-recommend|hermes-pass", "minScore": 75 }
```
- `public/index.html`
- Header button: `清除缓存`.
- Modal `#clear-cache-modal-overlay` with scope (`active`/`all`), mode, min score, preview, and log.
- Cache-busted assets to `style.css?v=15` and `app.js?v=15`.
- `public/app.js`
- Added `showClearCacheModal`, `hideClearCacheModal`, `renderClearCachePreview`, `clearCacheFromModal`, `getClearTargets`, `countQualityItems`.
- Frontend can clean current selected task or all non-running tasks.
- Shows preview of raw/coarse/fine counts and how many quality items will be retained.
- `public/style.css`
- Added `.clear-cache-modal`, `.cache-warning`, `.cache-preview*`, `.cache-counts` styles.
Verification performed:
```bash
node --check public/app.js
node --check server.mjs
node --check lib/task.mjs
```
Restart caveat:
- `node tools/xianyu_ops.mjs start --port 3000` reported running but did not replace stale PID 9407, so new route initially 404'd.
- Correct recovery was to kill the stale listener (`kill 9407`) and start tracked server with `PORT=3000 node server.mjs`.
- Active verified process after implementation: PID 11594 from Hermes background session `proc_41df821080bd`.
Smoke test:
1. Fetch `/` and verify `app.js?v=15`, `clear-cache-modal-overlay`.
2. Create save-only `__cache_smoke_test__` task via `POST /api/tasks`.
3. Call `POST /api/tasks/<id>/clear-cache` with `{ "mode": "all" }`.
4. Verify response `ok: true` and all counts zero.
5. Delete smoke task.
@@ -0,0 +1,50 @@
# Web Home Dashboard Clickable Cards (2026-05)
Session context: BOSS liked the xianyu-hunter Web dashboard visual polish, then requested that the home cards be actionable instead of decorative.
Implementation pattern used in `/Users/chick/.Hermes/workspace/research/xianyu-hunter`:
- Keep the home dashboard as the landing/empty state, but make overview cards real `<button>` elements.
- Add a generic routing helper in `public/app.js`:
- `findLatestTask(predicate)` sorts tasks by `updatedAt || createdAt` descending.
- `openHomeCard(target)` maps dashboard metrics to the most relevant task:
- `tasks` → latest task
- `raw` → latest task with `products.rawCount > 0`
- `fine` → latest task with `products.fineCount > 0`
- `recommend` → latest task with `hermesAnalysis.recommendedCount > 0`, then open `analysis` tab
- `running` → latest running task
- `scheduled` → latest task with `config.schedule.enabled`
- If no matching task exists, open `showCreateModal()` instead of silently doing nothing.
- Extend `selectTask(id, tab = 'products')` so cards can deep-link into either the product list or Hermes analysis tab after `refreshTask()` and `renderTaskDetail()`.
- Convert workflow steps into clickable buttons:
- 登录 → `showLoginModal()`
- 新建任务 → `showCreateModal()`
- 采集/预筛 → `openHomeCard('fine')`
- Hermes 分析 → `openHomeCard('recommend')`
- Style clickable cards with visible affordances:
- `cursor: pointer`, hover translate, border/glow changes, focus-visible outline.
- Include a small action line such as `查看分析结果 →` so the behavior is legible.
Verification checklist:
```bash
cd /Users/chick/.Hermes/workspace/research/xianyu-hunter
node --check public/app.js
git diff --check -- public/index.html public/style.css public/app.js
node tools/xianyu_ops.mjs status --port 3000
node --input-type=module <<'JS'
const base = 'http://127.0.0.1:3000';
for (const path of ['/', '/style.css?v=<CACHE_VERSION>', '/app.js?v=<CACHE_VERSION>']) {
const res = await fetch(base + path);
const text = await res.text();
console.log(path, res.status, text.length);
if (!res.ok) process.exitCode = 1;
}
JS
```
Pitfalls:
- If cards are changed from `div` to `button`, preserve visual reset (`font: inherit`, `text-align: left`, `color: var(--text)`, `width: 100%`) or they will inherit browser button styling.
- Bump both `style.css?v=N` and `app.js?v=N` in `public/index.html` after Web UI changes, otherwise BOSS may see stale assets.
- Do not make a dashboard card a dead click target; when there is no matching data, open the task creation modal as a useful fallback.
@@ -0,0 +1,49 @@
# Web home dashboard polish and verification (2026-05)
Use this note when continuing productization of BOSS's local `xianyu-hunter` Web dashboard.
## Context
BOSS disliked an empty/weak landing page and prefers productized, visible Web UI improvements over backend-only work. The local fork's homepage was changed from a plain empty state into a dashboard in `public/index.html`, with runtime data populated by `renderHomeDashboard()` in `public/app.js`.
## Implemented shape
- `public/index.html`
- Main content empty state is now `.home-dashboard`.
- Includes hero card, overview grid `#home-overview`, recent task list `#home-task-list`, and workflow panel.
- Cache-busting versions bumped to `style.css?v=17` and `app.js?v=17`.
- `public/app.js`
- `renderHomeDashboard()` computes totals: task count, running count, raw cached products, fine candidates, Hermes recommendations, scheduled task count.
- Renders recent tasks sorted by `updatedAt || createdAt`.
- Called after WebSocket connect/task create/update/finish and when no active task is selected.
- `public/style.css`
- Added `/* Home dashboard */` section: hero card, overview cards, recent task cards, workflow cards, responsive breakpoints, light/dark theme support.
## Verification commands
Run from `/Users/chick/.Hermes/workspace/research/xianyu-hunter`:
```bash
node --check public/app.js
node --check server.mjs
git diff --check -- public/index.html public/style.css public/app.js
node tools/xianyu_ops.mjs status --port 3000
node --input-type=module <<'JS'
const base = 'http://127.0.0.1:3000';
for (const path of ['/', '/style.css?v=17', '/app.js?v=17', '/api/tasks']) {
const res = await fetch(base + path);
const text = await res.text();
console.log(path, res.status, text.length, text.slice(0, 80).replace(/\n/g, ' '));
if (!res.ok) process.exitCode = 1;
}
JS
```
Expected: all syntax/diff checks pass, service is running on port 3000, and all HTTP probes return 200.
## Pitfalls
- After executing tool calls, always convert results into a visible response for BOSS; an empty assistant message after tools is treated as a failure.
- Use `git diff --check -- <paths>` directly; do not combine `--cached --no-index` incorrectly.
- When adding UI assets, bump query-string cache versions in `public/index.html` so BOSS sees changes immediately.
- Keep dashboard changes directly visible at `http://127.0.0.1:3000/` or LAN address; BOSS values immediately inspectable Web UI.
@@ -0,0 +1,43 @@
# Web Home Return Navigation — 2026-05
Context: after making the xianyu-hunter Web home dashboard cards clickable, BOSS pointed out that users could jump into a task detail page but had no obvious way back to the home dashboard.
Reusable fix pattern:
1. Add a `showHome()` function in `public/app.js` that:
- sets `activeTaskId = null`;
- calls `renderTaskList()` to clear sidebar active state;
- hides `#task-detail`;
- shows `#empty-state` / home dashboard;
- resets home scroll position if available;
- calls `renderHomeDashboard()` so stats/recent tasks refresh.
2. Make the top brand/title clickable and call `showHome()`.
3. Add a visible header button labelled `首页` for discoverability.
4. Add a task-detail pipeline control `← 首页` next to task actions so the return path remains visible after jumping into a task.
5. Add CSS for `.brand-button`, `.btn-home-main`, and `.btn-home` so the controls look intentional and keyboard focus is visible.
6. Bump static asset cache versions in `public/index.html` after changing `public/app.js` or `public/style.css`.
Verification used:
```bash
cd /Users/chick/.Hermes/workspace/research/xianyu-hunter
node --check public/app.js
git diff --check -- public/index.html public/style.css public/app.js
node tools/xianyu_ops.mjs status --port 3000
node --input-type=module <<'JS'
const base = 'http://127.0.0.1:3000';
const checks = [
['/', 'showHome'],
['/style.css?v=<version>', 'brand-button'],
['/app.js?v=<version>', 'function showHome()'],
];
for (const [path, needle] of checks) {
const res = await fetch(base + path);
const text = await res.text();
console.log(path, res.status, text.length, text.includes(needle));
if (!res.ok || !text.includes(needle)) process.exitCode = 1;
}
JS
```
UX lesson: when dashboard cards deep-link into content, always provide an equally obvious return path. Do not rely only on browser back, especially in a single-page Web UI with hidden/shown panels.
@@ -0,0 +1,80 @@
# Web Login + Cookie Persistence Panel (2026-05)
Context: BOSS wanted the xianyu-hunter Web UI to support login like the original project, saving cookies for reuse. A plain “open login window” button was not enough; the Web UI needed a visible login/cookie panel with status detection.
## Implemented shape
Files in `/Users/chick/.Hermes/workspace/research/xianyu-hunter`:
- `lib/browser.mjs`
- Added `getOpenPagesInfo()` to report whether the persistent Playwright browser context is open and list page title/URL.
- `lib/login.mjs`
- Added `openLoginPage(emitLog)`.
- Added `getLoginStatus(existingPage = null)`.
- `checkLogin()` now uses stronger login status detection and logs cookie count + `browser-data/` persistence.
- `server.mjs`
- Imports `checkLogin`, `getLoginStatus`, `openLoginPage`, `closeBrowser`, `getOpenPagesInfo`.
- Added APIs:
- `GET /api/login/status`
- `POST /api/login/open`
- `POST /api/login/wait`
- `POST /api/login/close-browser`
- `public/index.html`
- Header button: `账号登录 / Cookie`.
- Login modal: status card, actions, help text, log panel.
- `public/app.js`
- Added `showLoginModal`, `hideLoginModal`, `renderLoginStatus`, `appendLoginLog`, `checkLoginStatus`, `openLoginWindow`, `waitLoginStatus`, `closeLoginBrowser`.
- `public/style.css`
- Added login modal styles (`.login-modal`, `.login-status-card`, `.login-actions`, `.login-help`, `.login-log`).
## Login detection rule
Do not trust homepage-only checks. In this session, CLI `login` said OK while the real search page still showed:
- `登录后可以更懂你,推荐你喜欢的商品!`
- `立即登录`
Use a real search probe such as:
```text
https://www.goofish.com/search?q=e3%201245%20v3
```
Treat status as logged in only if:
- login wall text is absent, and
- either product cards load or enough Goofish/Taobao/Alibaba cookies exist.
`nick--` and avatar elements alone are false-positive prone on login-wall pages.
## Verification commands
```bash
cd /Users/chick/.Hermes/workspace/research/xianyu-hunter
node --check server.mjs
node --check lib/browser.mjs
node --check lib/login.mjs
node --check public/app.js
# Restart if needed
lsof -tiTCP:3000 -sTCP:LISTEN | xargs -r kill
PORT=3000 node server.mjs
# Verify UI markers and API
curl -sS http://127.0.0.1:3000/ | grep -E '账号登录 / Cookie|闲鱼账号登录 / Cookie'
curl -sS http://127.0.0.1:3000/api/login/status | python3 -m json.tool
lsof -nP -iTCP:3000 -sTCP:LISTEN
```
Expected LAN URL for BOSSs Mac in this environment:
```text
http://192.168.2.69:3000
```
## Operational notes
- Cookies are stored in `browser-data/` via Playwright persistent context. Do not print raw cookies.
- `POST /api/login/open` opens a visible Chromium window on the Mac desktop. BOSS must use remote control/screen sharing to scan QR or complete captcha there.
- `POST /api/login/close-browser` should not delete cookies; it only closes the context.
- If a stale server process keeps old routes, kill the listener on port 3000 before starting the new server.
@@ -0,0 +1,84 @@
# Web login button + LAN exposure notes (2026-05)
Session learning from BOSS testing xianyu-hunter login from a LAN browser.
## Symptoms
- Dashboard was reachable from LAN at `http://192.168.2.69:3000`, because `lsof -nP -iTCP:3000 -sTCP:LISTEN` showed `TCP *:3000 (LISTEN)`.
- BOSS could not find a login entry in the Web UI because `public/index.html` only had `+ 新建任务`.
- `node tools/xianyu_ops.mjs stop` can report `no-recorded-server` / lose `.runtime` state while an old `node server.mjs` is still listening on port 3000. In that case new code is not active until the old PID is killed.
## Fix pattern
1. Add server imports:
```js
import { checkLogin } from './lib/login.mjs';
import { closeBrowser } from './lib/browser.mjs';
```
2. Add API routes before `server.listen`:
```js
app.post('/api/login/open', async (req, res) => {
try {
const logs = [];
const ok = await checkLogin(msg => logs.push(msg));
res.json({ ok, loggedIn: ok, logs });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
app.post('/api/login/close-browser', async (req, res) => {
await closeBrowser();
res.json({ ok: true });
});
```
3. Add a Web UI header button:
```html
<button class="btn" onclick="openLoginWindow()">打开闲鱼登录窗口</button>
```
4. Add `openLoginWindow()` in `public/app.js` near `apiPost()`:
```js
async function openLoginWindow() {
alert('即将在 Mac 桌面打开一个 Chromium 闲鱼窗口;请通过远程桌面/屏幕共享在那个窗口里完成扫码或验证码登录。');
try {
const result = await apiPost('/api/login/open', {});
const logs = (result.logs || []).join('\n');
if (result.loggedIn) {
alert(`登录状态正常。\n\n${logs}`);
} else {
alert(`还未检测到登录完成,请在弹出的 Chromium 窗口里登录后再试。\n\n${logs}`);
}
} catch (err) {
alert(`打开登录窗口失败:${err.message}`);
}
}
```
## Verification
```bash
cd /Users/chick/.Hermes/workspace/research/xianyu-hunter
node --check server.mjs
node --check public/app.js
lsof -nP -iTCP:3000 -sTCP:LISTEN
curl -sS http://127.0.0.1:3000/ | grep -o '打开闲鱼登录窗口' | head -1
curl -sS -X POST http://127.0.0.1:3000/api/login/close-browser
```
If `curl -X POST /api/login/close-browser` returns an HTML `Cannot POST` page, the stale server is still running. Kill the actual listening PID from `lsof` and restart:
```bash
kill -TERM <pid>
sleep 2
kill -KILL <pid> 2>/dev/null || true
PORT=3000 node server.mjs
```
Run the final server as a tracked background process in Hermes rather than using shell-level `nohup` wrappers.
@@ -0,0 +1,44 @@
# Web QR Login Simplification — 2026-05
Session learning from BOSS refining the xianyu-hunter Web login UX.
## User preference
BOSS found the login panel too busy after adding QR login and account status. For this project, the preferred Web login UX is deliberately minimal:
- Keep only **获取/刷新登录二维码** and **刷新账号状态** in the visible login modal.
- Do not expose legacy/backup actions as primary UI buttons:
- 打开闲鱼登录窗口
- 等待并检测
- 上传 Cookie JSON
- 关闭登录浏览器/关闭登录会话
- Keep backup APIs/functions available if useful for troubleshooting, but hide them from the normal Web UI unless BOSS explicitly asks for advanced controls.
- Simplify help text to the QR-first flow: get QR → scan → refresh status → cookies persist in `browser-data/`.
## Implementation shape used
In `/Users/chick/.Hermes/workspace/research/xianyu-hunter/public/index.html`, the login actions block should be:
```html
<div class="login-actions">
<button class="btn btn-primary" onclick="fetchLoginQr()">获取/刷新登录二维码</button>
<button class="btn" onclick="checkLoginStatus()">刷新账号状态</button>
</div>
```
The help text should avoid mentioning JSON import or manual browser opening in the normal flow.
`public/app.js` may still contain `openLoginWindow()`, `waitLoginStatus()`, `importCookieFile()`, and `closeLoginBrowser()` as hidden troubleshooting utilities, but no visible element should call them in the standard modal.
## Verification commands
```bash
cd /Users/chick/.Hermes/workspace/research/xianyu-hunter
node --check public/app.js
node --check server.mjs
node --check lib/login.mjs
PORT=3000 node server.mjs
curl -sS http://127.0.0.1:3000/ | grep -E '获取/刷新登录二维码|刷新账号状态|上传 Cookie JSON|打开闲鱼登录窗口|等待并检测|关闭登录会话'
```
Expected UI marker result: only the two kept buttons and QR/status help text should appear; no legacy button labels should appear.
@@ -0,0 +1,67 @@
# Web 后台二维码登录与账号状态面板(2026-05)
## Trigger
BOSS 反馈 Cookie JSON/外部截图仍不够方便,希望登录流程直接植入 xianyu-hunter Web 后台:点击登录即可获取真实登录二维码,并在后台看到当前账号基础状态。
## Implemented pattern
Files in local clone `/Users/chick/.Hermes/workspace/research/xianyu-hunter`:
- `lib/login.mjs`
- Add `getLoginQr(emitLog)`.
- It opens `https://www.goofish.com/` through the persistent Playwright profile, clicks the visible login entry when present, tries QR-login selectors, and returns a `data:image/png;base64,...` screenshot as `qrImage` when not logged in.
- It reuses `getLoginStatus(page)` so already-logged-in accounts return status and no QR image.
- `getLoginStatus()` now also returns account basics: `accountName`, `nickCandidates`, `avatarUrl`.
- `server.mjs`
- Import `getLoginQr` and expose `POST /api/login/qr`.
- `public/index.html`
- In the login modal, add account row (`login-account-row`) and QR panel (`login-qr-panel`).
- Primary action is now **获取登录二维码**; **打开闲鱼登录窗口** remains a fallback for captcha/expired QR/manual verification.
- `public/app.js`
- Add `fetchLoginQr()`, `renderLoginQr()`, `clearLoginQr()`.
- Extend `renderLoginStatus()` to show nickname/avatar/cookie count/account state.
- `public/style.css`
- Add `.login-qr-panel`, `.login-qr-img`, `.login-account-row`, `.login-avatar` styles.
## Important workflow corrections learned
1. Do **not** manually fabricate a Taobao login URL and send it to BOSS as “闲鱼二维码”. Goofish may use Taobao/Alibaba unified login under the hood, but the user experience must start from the Goofish Web login entry to avoid confusion.
2. Prefer an in-dashboard QR panel over sending QR screenshots through chat. BOSS explicitly wants the backend page to own login, QR display, and account status.
3. Keep visible Chromium login as fallback only. QR/captcha/verification may still require the desktop browser.
4. Avoid concurrent Playwright profiles. Temporary QR scripts and the main service can conflict on `browser-data/SingletonLock`; close/kill temporary QR browser processes before running tasks or status probes.
5. If `POST /api/login/qr` is called when already logged in, return status and no QR image; frontend should clear/hide the QR panel and show account info instead.
## Verification recipe
```bash
cd /Users/chick/.Hermes/workspace/research/xianyu-hunter
node --check lib/login.mjs
node --check server.mjs
node --check public/app.js
# restart exact listener if needed
for p in $(lsof -tiTCP:3000 -sTCP:LISTEN 2>/dev/null); do kill "$p" 2>/dev/null || true; done
PORT=3000 node server.mjs
```
API checks:
```bash
curl -sS -m 60 http://127.0.0.1:3000/api/login/status | python3 -m json.tool
curl -sS -m 90 -X POST http://127.0.0.1:3000/api/login/qr \
-H 'Content-Type: application/json' -d '{}' > /tmp/xianyu_qr_api.json
python3 - <<'PY'
import json
j=json.load(open('/tmp/xianyu_qr_api.json'))
print(j.get('ok'), j.get('loggedIn'), j.get('cookieCount'), j.get('accountName'), bool(j.get('qrImage')))
PY
curl -sS -m 10 http://127.0.0.1:3000/ | grep -E '获取登录二维码|扫码登录|login-qr-panel'
```
Expected logged-in example from the session:
- `loggedIn: true`
- `cookieCount: 27`
- `accountName: 札幌打滚的风筝鱼`
- `qrImage: false` because QR is unnecessary while logged in.
@@ -0,0 +1,133 @@
# Web Task Editing, Scheduling, and AI-Optimize UI (2026-05)
Session learning from extending BOSS's local `xianyu-hunter` fork at:
```text
/Users/chick/.Hermes/workspace/research/xianyu-hunter
```
## Feature shape implemented
When BOSS asks for Web后台 task management instead of Hermes-only CLI control, keep the product shape focused on monitoring/analysis-only:
- Web task modal supports both create and edit.
- Existing tasks can be edited only when not running.
- Task form supports:
- search keywords (`queries`)
- price min/max
- region
- personal seller toggle
- max pages
- `maxItems` / 单次最大商品寻找量
- custom requirements
- coarse/fine prompts
- schedule enabled + interval minutes
- AI optimize/current-form generation
- Save modes:
- `autoStart:false` = 手动新增任务但不立即采集
- save + run = create and start immediately
- edit + reset run = save then rerun with old products cleared
- Keep seller chat disabled. Scheduled/background runs must still stop at collection + local prefilter/detail collection and never auto-contact sellers.
## Backend/API pattern
Good API additions:
```text
PUT /api/tasks/:id edit task config (reject while running)
POST /api/tasks/optimize generate optimized config from current form
POST /api/tasks accepts autoStart:false
POST /api/tasks/:id/start accepts { reset:true }
```
Task config should normalize legacy and new schedule shapes:
```js
const existingSchedule = config.schedule || {};
const scheduleEnabled = config.scheduleEnabled ?? existingSchedule.enabled ?? false;
const scheduleIntervalMinutes = config.scheduleIntervalMinutes ?? existingSchedule.intervalMinutes ?? 0;
```
`maxItems` should be enforced after each query's dedupe and before fine detail fetch:
```js
const remaining = Math.max(0, (task.config.maxItems || 60) - task.products.raw.length);
const accepted = deduped.slice(0, remaining);
task.products.raw.push(...accepted);
if (task.products.raw.length >= (task.config.maxItems || 60)) break;
await fineFilter(task.products.coarseFiltered.slice(0, task.config.maxItems || 60), ...)
```
Scheduler pattern:
- `TaskManager` starts an interval timer in constructor after `_loadFromDisk()`.
- Tick every ~30s.
- For enabled schedule with due `nextRunAt`, non-running task: push a log and call `startTask(id, { reset: true, scheduled: true })`.
- On scheduled run, set `lastRunAt` and normalize next run.
## AI optimize fallback
`POST /api/tasks/optimize` can try `chatCompletion()`, but must tolerate missing AI config. If it fails, return `{ ok:true, source:'local-fallback', task }` so the UI still works.
Local fallback should generate conservative monitoring defaults:
- `maxPages`: 2
- `maxItems`: 40
- `personalSeller`: true
- requirements explicitly include: no seller contact, exclude parts/repair/wanted/rental/recycling/commercial bulk/title spam.
## Frontend/UI notes
- Reuse the create modal for editing by tracking `editingTaskId`.
- Add buttons in pipeline controls: 编辑, 复制, 继续/启动, 重新采集, 删除.
- `重新采集` should confirm because it clears current round products.
- `fillTaskForm(taskOrConfig)` avoids duplicate create/edit/duplicate mapping bugs.
- Add cache-busting version bump for `style.css` and `app.js`; also serve static files with `Cache-Control: no-store` to avoid stale dashboard assets.
## Verification recipe
Before final response:
```bash
cd /Users/chick/.Hermes/workspace/research/xianyu-hunter
node --check server.mjs && node --check lib/task.mjs && node --check public/app.js
```
Restart the exact server on port 3000. If `node tools/xianyu_ops.mjs stop` says `no-recorded-server` but port is in use, kill both the listener and parent npm process:
```bash
lsof -nP -iTCP:3000 -sTCP:LISTEN
ps -p <listener_pid> -o pid,ppid,command
kill -TERM <parent_pid> <listener_pid>
```
Then start in background and verify API:
```bash
PORT=3000 npm start
curl -sS http://127.0.0.1:3000/api/tasks | python3 -m json.tool
```
Smoke-test the new APIs with a temporary task and delete it after:
```bash
python3 - <<'PY'
import json, urllib.request
base='http://127.0.0.1:3000'
def req(method,path,data=None):
body=json.dumps(data).encode() if data is not None else None
r=urllib.request.Request(base+path,data=body,method=method,headers={'Content-Type':'application/json'})
with urllib.request.urlopen(r,timeout=30) as resp:
return json.loads(resp.read().decode())
opt=req('POST','/api/tasks/optimize',{'product':'e3-1245v3 cpu','budget':'150','currentRequirements':'只要CPU本体,排除主板套装'})
created=req('POST','/api/tasks',{**opt['task'],'name':'__api_verify_temp__','autoStart':False,'scheduleEnabled':True,'scheduleIntervalMinutes':120,'maxItems':25})
updated=req('PUT',f"/api/tasks/{created['id']}",{'name':'__api_verify_temp_updated__','queries':['e3 1245 v3 verify'],'maxItems':30,'scheduleEnabled':False,'scheduleIntervalMinutes':0})
req('DELETE',f"/api/tasks/{created['id']}")
print('ok', opt.get('source'), created['id'], updated['config']['maxItems'])
PY
```
## Pitfall
Do not leave a final user response empty after tools. If a tool result shows a port conflict or failed restart, process it immediately, continue with the fix, and give BOSS the final verified state.
@@ -0,0 +1,66 @@
# Web Theme Toggle + Visual Polish — 2026-05
Session learning from BOSS's xianyu-hunter dashboard refinement request: when asked to make the UI more polished, at minimum provide explicit dark/light theme switching and make the result visible in the Web UI immediately.
## Implemented pattern
Files in `/Users/chick/.Hermes/workspace/research/xianyu-hunter`:
- `public/index.html`
- Add a header button before existing settings/login/create actions:
- `id="theme-toggle"`
- `onclick="toggleTheme()"`
- child spans `theme-icon` and `theme-label`
- Bump static asset query versions, e.g. `style.css?v=14`, `app.js?v=14`, to avoid stale browser cache.
- `public/app.js`
- Add `THEME_STORAGE_KEY = 'xianyu-hunter-theme'`.
- Add `getPreferredTheme()`:
- prefer saved `localStorage` value if `light`/`dark`.
- otherwise follow `window.matchMedia('(prefers-color-scheme: light)')`.
- Add `applyTheme(theme)`:
- sets `document.documentElement.dataset.theme`.
- updates `theme-icon` and `theme-label` when DOM elements exist.
- Add `toggleTheme()`:
- toggles `light`/`dark`, persists to `localStorage`, reapplies.
- Call `applyTheme(getPreferredTheme())` early so theme applies before app data rendering.
- `public/style.css`
- Use `:root` as default dark theme and `:root[data-theme="light"]` for light variables.
- Expand variables beyond the older basic palette: `--surface`, `--surface-strong`, `--border-strong`, `--muted`, `--accent3`, `--shadow`, `--shadow-soft`, `--glow`.
- Prefer real light-theme variables rather than simple inversion.
- Polish high-visibility surfaces: header, sidebar, content shell, task cards, buttons, pipeline, tabs, stat cards, product table wrapper, modal, log blocks.
- Use cache-safe CSS constructs with existing browser target: gradients, `backdrop-filter`, CSS variables, and `color-mix()`.
## Verification commands
```bash
cd /Users/chick/.Hermes/workspace/research/xianyu-hunter
node --check public/app.js
node --check server.mjs
python3 - <<'PY'
from pathlib import Path
html=Path('public/index.html').read_text()
css=Path('public/style.css').read_text()
js=Path('public/app.js').read_text()
for needle in ['style.css?v=', 'app.js?v=', 'theme-toggle']:
print(needle, needle in html)
print('light theme vars', ':root[data-theme="light"]' in css)
print('toggleTheme', 'function toggleTheme()' in js)
PY
node tools/xianyu_ops.mjs stop || true
node tools/xianyu_ops.mjs start --port 3000
node tools/xianyu_ops.mjs status --port 3000
python3 - <<'PY'
import urllib.request
html=urllib.request.urlopen('http://127.0.0.1:3000/', timeout=10).read().decode('utf-8')
print('theme button', 'theme-toggle' in html)
print('css v', 'style.css?v=' in html)
print('app v', 'app.js?v=' in html)
PY
```
## Pitfalls
- Do not stop after backend/CSS edits only; BOSS expects visible product UI changes and live verification.
- If CSS/JS filenames use query-string cache busting, bump both versions in `index.html`; otherwise the browser may keep the previous UI.
- `applyTheme()` may run before the header DOM exists, so DOM updates must guard missing `theme-icon`/`theme-label` elements.
- Preserve existing login/notification/task controls while adding the theme toggle; do not regress the simplified login UX.