mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(wiki): 3 bugs caught by browser-driven chat e2e + 12-round trace
This commit is contained in:
parent
7c9db74886
commit
0ef735588a
@ -883,6 +883,135 @@ to-end recovery flow proven on live server.
|
||||
|
||||
---
|
||||
|
||||
## 14. Browser-driven chat e2e — 12 conversation rounds (2026-05-28)
|
||||
|
||||
Drove the full UI through the `gstack browse` headless Chromium, logged
|
||||
in as admin, exercised the wiki feature exclusively through chat
|
||||
conversations against the default "研究分析师" plan-execute agent (id
|
||||
`2056270363980120065`). 12 rounds. Two real bugs caught and fixed mid-run.
|
||||
|
||||
### 14.1 Conversation transcript (compressed)
|
||||
|
||||
| # | Prompt | Result |
|
||||
|---|---|---|
|
||||
| R1 | "列出当前所有知识库 KB" | ✅ Agent listed 3 KBs (E2E-LookupDemo 4p, E2E-RFC55-PostFix 7p, dev 3p) via injected `<wiki-context>` block; honest "wiki_list_kbs tool not enabled" note |
|
||||
| R2 | "详细介绍 E2E-LookupDemo KB 里的 react 页面" | ✅ Rich Markdown intro of the page (reasoning / action / observation / StateGraph relation / use cases). Used wiki_read_page tool |
|
||||
| R3 | "请直接调用 wiki_read_page... 把原文 markdown 完整返回" | ✅ Full raw markdown returned, including `[[stategraph]]` wikilinks rendered into `<a class="wiki-link">`. **🔴 Bug A discovered**: rendered link had `data-wiki-title="stategraph\|StateGraph"` (pipe alias bled into attribute) |
|
||||
| R4 | (click `[[StateGraph]]` in the chat bubble) | ✅ Global click delegator fired, lookup returned 1 hit, `router.push` navigated to `/wiki?kbId=2059795489877315586&slug=stategraph` |
|
||||
| R5 | (verify wiki view auto-opened the page) | **🔴 Bug B discovered**: URL changed but `.page-content` did not render — KB list still showing. Tracing: `Number("2059795489877315586")` truncated the Snowflake from §13's consumeQueryNavigation, then API returned 404 "Knowledge base not found" |
|
||||
| → fix mid-run | Two fixes applied + rebuild + restart | Bug A: legacy renderer regex now captures `slug` + `alias` separately; Bug B: `kbIdRaw` stays a string end-to-end; WikiWorkspace switches to 'pages' tab on currentPage assign |
|
||||
| R5 (retest) | Same flow on the fixed bundle | ✅ `.page-content` renders the StateGraph wiki content. URL is cleaned to `/wiki` after `router.replace`. KB selected, page open, tab switched |
|
||||
| R6 | "列出 E2E-LookupDemo 里的所有页面" | ✅ Agent returned `react — ReAct 模式` + `stategraph — StateGraph` (system pages correctly excluded) |
|
||||
| R7 | "读取 react 页面,告诉我里面有多少处 wikilink、各指向哪个 slug" | ✅ Agent identified 2 wikilinks, both pointing at `stategraph` |
|
||||
| R8 | "扫描 E2E-LookupDemo 这个 KB 现在有多少死链?" | ✅ Agent: 0 broken links across both content pages; 3 total wikilinks, all resolve |
|
||||
| R9 | "ReAct 和 StateGraph 有什么关系?" | ✅ Substantive synthesis grounded in the actual page content (3-stage reasoning/action/observation mapping to graph node types) |
|
||||
| R10 | "查最近的 wiki audit 事件" | ✅ Agent inspected the `log` wiki page and produced a 3-event table; honestly flagged that the wiki log page is *not* the audit-event table and pointed at the proper API |
|
||||
| R11 | "在 E2E-LookupDemo 创建新页 langgraph 引用 `[[stategraph]]`" | ✅ Agent created the page via wiki write tool. Post-API check: `langgraph` exists, `content_len=198`, `outgoing=["stategraph"]`, `broken=[]` |
|
||||
| R12 | "三个 KB 的死链总数" | ✅ Agent reports 0 / 0 / 0; summarises wikilink count and verifies all resolve |
|
||||
|
||||
### 14.2 Bug A — legacy renderer pipe handling
|
||||
|
||||
**Symptom**: The chat-side renderer is supposed to turn
|
||||
`[[stategraph|StateGraph]]` into a link whose `data-wiki-title` is the
|
||||
slug `stategraph` and whose visible text is the alias `StateGraph`.
|
||||
Instead it produced
|
||||
`<a data-wiki-title="stategraph|StateGraph">stategraph|StateGraph</a>`
|
||||
— the regex `\[\[([^\]]+)\]\]` captured the entire bracket interior
|
||||
including the `|` separator, and the replacement template copied it
|
||||
verbatim into both the attribute and the label.
|
||||
|
||||
**Impact**: when the global wikilink click delegator forwarded
|
||||
`data-wiki-title="stategraph|StateGraph"` to the cross-KB lookup, the
|
||||
backend matched against neither a slug `stategraph|StateGraph` nor a
|
||||
title with that literal — every aliased wikilink in chat resulted in a
|
||||
"未找到匹配的 wiki 页面" toast instead of navigating.
|
||||
|
||||
**Fix** (`useMarkdownRenderer.ts`):
|
||||
|
||||
```ts
|
||||
// before
|
||||
.replace(/\[\[([^\]]+)\]\]/g, '<a data-wiki-title="$1">$1</a>')
|
||||
|
||||
// after — alias-aware
|
||||
.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (_, slug, alias) => {
|
||||
const target = slug.trim()
|
||||
const visible = alias?.trim() || slug.trim()
|
||||
return `<a data-wiki-title="${target}">${visible}</a>`
|
||||
})
|
||||
```
|
||||
|
||||
The fix also HTML-escapes the captured strings (`"` → `"`,
|
||||
`'` → `\'`) before they enter the attribute and the inline onclick to
|
||||
plug the same XSS risk the page-viewer postprocess covers.
|
||||
|
||||
### 14.3 Bug B — Snowflake precision in consumeQueryNavigation
|
||||
|
||||
**Symptom**: Round 5 found the URL navigated correctly to
|
||||
`/wiki?kbId=2059795489877315586&slug=stategraph` but the KB never
|
||||
selected. Console showed `Error: Knowledge base not found` (HTTP 404).
|
||||
|
||||
**Root cause**: my §13 `consumeQueryNavigation` did
|
||||
`Number(route.query.kbId)` to satisfy `store.selectKB(id: number)`.
|
||||
But `2059795489877315586` exceeds `Number.MAX_SAFE_INTEGER` (2⁵³−1 =
|
||||
`9007199254740992`), so the coercion silently truncated the last few
|
||||
digits — exactly the bug class CLAUDE.md warns about in the "ID
|
||||
Handling — Snowflake Precision Convention" section. The backend
|
||||
correctly returned 404 because the truncated number doesn't match any
|
||||
real KB.
|
||||
|
||||
**Fix** (`Wiki/index.vue`): keep `kbId` as a string throughout. The
|
||||
store / api layer never reconstructs it as a number — it's interpolated
|
||||
straight into `/wiki/knowledge-bases/${id}`, so a string works fine at
|
||||
runtime. The TypeScript type signature `selectKB(id: number)` is
|
||||
satisfied with a localised `as unknown as number` cast plus a
|
||||
`// snowflake-precision-ok` comment so the lint script (`pnpm
|
||||
lint:precision`) doesn't flag it. The `Number()` call is gone.
|
||||
|
||||
### 14.4 Bug C — WikiWorkspace default tab hides the auto-opened page
|
||||
|
||||
**Symptom**: After the Snowflake fix, the KB selected correctly but
|
||||
`.page-content` still didn't render — the workspace defaults to the
|
||||
'raw' (raw materials) tab, and the WikiPageViewer only mounts inside
|
||||
the 'pages' tab.
|
||||
|
||||
**Fix** (`WikiWorkspace.vue`): watch `store.currentPage` and switch
|
||||
`activeTab` to `'pages'` whenever a page becomes current. Manual
|
||||
sidebar clicks already work because the user is on the pages tab
|
||||
when they click; the query-param auto-open bypassed that state, so the
|
||||
explicit watcher closes the gap.
|
||||
|
||||
### 14.5 Live verification after all fixes
|
||||
|
||||
After the three fixes landed + frontend rebuild + server restart, R5
|
||||
was rerun on the fresh bundle:
|
||||
|
||||
- URL after click: `http://localhost:18088/wiki?kbId=...&slug=stategraph` → router.replace cleans to `/wiki`
|
||||
- `.workspace-title` = `E2E-LookupDemo`
|
||||
- `.page-content` text starts with the StateGraph page's first paragraph: "驱动的节点类型 StateGraph 在标准 Agent 架构 中驱动以下三类核心节点循环执行:推理节点(Reasoning Node)..."
|
||||
- Console errors are historical only (from the broken bundle pre-fix); no new errors after the restart
|
||||
|
||||
### 14.6 What the agent could and couldn't do
|
||||
|
||||
| Capability | Result |
|
||||
|---|---|
|
||||
| Read wiki pages via `wiki_read_page` | ✅ works, returns full content |
|
||||
| Search across KBs (via `<wiki-context>` injection) | ✅ works, lists all 3 KBs accurately |
|
||||
| Create new pages (via `wiki_write_page` or similar) | ✅ works — R11 created `langgraph` with correct `[[stategraph]]` reference, content + outgoing_links + broken_links all populated |
|
||||
| Scan / report broken links across KBs | ✅ works — R8, R12 both accurate (0 broken refs) |
|
||||
| Detect and reason about wikilink target consistency | ✅ works — R7 listed exact occurrence count + targets |
|
||||
| Synthesize across multiple wiki pages | ✅ works — R9 connected ReAct's three stages to StateGraph node types |
|
||||
| Read structured audit events | ⚠️ agent confused "wiki log page" with "audit log table" in R10 — honest enough to flag the limitation. Not a bug of the wikilink overhaul; agent prompt could be tuned to know which "log" to use |
|
||||
|
||||
### 14.7 Bottom line for §14
|
||||
|
||||
12 rounds of real chat interaction. Two genuine bugs caught (legacy
|
||||
renderer pipe handling, Snowflake precision in consumeQueryNavigation)
|
||||
plus one UX gap (workspace default tab). All three fixed mid-run.
|
||||
After fixes: chat click → cross-KB lookup → wiki view auto-open with
|
||||
page content rendered, full flow proven from the user's perspective.
|
||||
|
||||
---
|
||||
|
||||
## 14. Seventh pass — post-restart full sweep (2026-05-28 09:09)
|
||||
|
||||
Server killed (`lsof -ti:18088 | kill -9`) and restarted via
|
||||
|
||||
@ -395,9 +395,25 @@ export function useMarkdownRenderer() {
|
||||
const withWikiLinks =
|
||||
wikilink === 'none'
|
||||
? withLatex
|
||||
: withLatex.replace(
|
||||
/\[\[([^\]]+)\]\]/g,
|
||||
'<a class="wiki-link" href="#" data-wiki-title="$1" onclick="window.dispatchEvent(new CustomEvent(\'wiki-link-click\',{detail:{title:\'$1\'}}));return false">$1</a>'
|
||||
: // Split `[[slug|display]]` into slug + display halves so the
|
||||
// `data-wiki-title` attribute carries the slug ALONE (the cross-KB
|
||||
// lookup keys off that) and the visible label is the display text
|
||||
// (the alias an author chose). The earlier single-capture regex
|
||||
// copied the whole bracket interior — including the literal `|` —
|
||||
// into both, producing `data-wiki-title="slug|display"` lookups
|
||||
// that the backend would never resolve.
|
||||
withLatex.replace(
|
||||
/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g,
|
||||
(_match, slug: string, alias?: string) => {
|
||||
const target = slug.trim().replace(/"/g, '"')
|
||||
const visible = (alias?.trim() || slug.trim()).replace(/"/g, '"')
|
||||
return (
|
||||
'<a class="wiki-link" href="#" data-wiki-title="' + target +
|
||||
'" onclick="window.dispatchEvent(new CustomEvent(\'wiki-link-click\',{detail:{title:\'' +
|
||||
target.replace(/'/g, "\\'") +
|
||||
'\'}}));return false">' + visible + '</a>'
|
||||
)
|
||||
},
|
||||
)
|
||||
// 3. Marked → 4. DOMPurify.
|
||||
const rawHtml = markedInstance.parse(withWikiLinks) as string
|
||||
|
||||
@ -90,6 +90,14 @@ const canManageWiki = computed(() => workspace.can('manage:wiki'))
|
||||
const activeTab = ref('raw')
|
||||
const brokenPanelOpen = ref(false)
|
||||
|
||||
// When a page becomes the currentPage (e.g. via the global wikilink click
|
||||
// handler that lands on /wiki?kbId=X&slug=Y), switch the tab to 'pages' so
|
||||
// the viewer is the thing the user sees. Without this, the workspace stays
|
||||
// on the default 'raw' tab and the page silently loads off-screen.
|
||||
watch(() => store.currentPage, (page) => {
|
||||
if (page) activeTab.value = 'pages'
|
||||
})
|
||||
|
||||
const tabs = computed(() => {
|
||||
const list = [
|
||||
{ key: 'raw', label: t('wiki.rawMaterials') },
|
||||
|
||||
@ -117,17 +117,27 @@ async function consumeQueryNavigation() {
|
||||
// ?kbId=X&slug=Y on click. Honour both: enter the KB then surface
|
||||
// the page directly. Strips the query immediately so a manual reload
|
||||
// doesn't keep re-opening the same page.
|
||||
//
|
||||
// **Snowflake precision** (per CLAUDE.md): the kbId is a 19-digit
|
||||
// Snowflake that exceeds Number.MAX_SAFE_INTEGER. NEVER coerce via
|
||||
// Number()/parseInt() — that truncates the last 2-3 digits and turns
|
||||
// a real lookup into a silent "KB not found". Keep the string and
|
||||
// pass it through to the store; the store / api layer treats kbId as
|
||||
// an opaque token interpolated into the request URL.
|
||||
const kbIdRaw = route.query.kbId
|
||||
const slugRaw = route.query.slug
|
||||
if (typeof kbIdRaw !== 'string' || typeof slugRaw !== 'string') return
|
||||
// Snowflake stays as a string end-to-end — store.selectKB accepts number,
|
||||
// so we coerce only at the call site (safe because Pinia stores routes
|
||||
// through to the backend as a string in the URL path).
|
||||
const kbIdNum = Number(kbIdRaw)
|
||||
if (!Number.isFinite(kbIdNum)) return
|
||||
await store.selectKB(kbIdNum)
|
||||
if (typeof kbIdRaw !== 'string' || !kbIdRaw) return
|
||||
if (typeof slugRaw !== 'string' || !slugRaw) return
|
||||
// Cast to number ONLY to satisfy the store's type signature — the
|
||||
// runtime value stays a string under the hood. TypeScript can't
|
||||
// express "number-or-Snowflake-string" without widening every signature,
|
||||
// so the cast is the localised, documented escape hatch.
|
||||
// snowflake-precision-ok: kbIdRaw is the URL-encoded string from the
|
||||
// global click delegator; never passed through Number()/parseInt().
|
||||
const kbId = kbIdRaw as unknown as number
|
||||
await store.selectKB(kbId)
|
||||
try {
|
||||
await store.loadPage(kbIdNum, slugRaw)
|
||||
await store.loadPage(kbId, slugRaw)
|
||||
} catch (e) {
|
||||
console.warn('[Wiki] auto-open page failed', e)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user