fix: band-level windowed refine_layout + programmatic map_fields to prevent 91.5% content loss

Root cause: LLM receiving full 34k-char JRXML would regenerate from scratch
instead of modifying coordinates in-place, shrinking output to ~3k chars.

Solution (programmatic node control, not prompt engineering):

- New agent/jrxml_windower.py: decompose JRXML into header (never sent to
  LLM) + individual bands. Split bands >4000 chars at element boundaries.
  Reassemble with element count validation (>10% change = rollback).

- Rewrite refine_layout: per-band windowed LLM processing (~2-4k chars
  each). LLM cannot "reimagine" the entire report.

- Rewrite map_fields: 100% programmatic regex $F{field_N} -> real name
  replacement. Zero LLM calls, zero content loss.

- _sanitize_field_name: non-ASCII chars escaped to _uXXXX_ format for
  valid JRXML identifiers.

- Tests: 48 new unit tests (windower 28 + map_fields 20). All passing.
  Full suite 385 tests, zero regressions.
This commit is contained in:
2026-05-24 08:55:38 +08:00
parent bb6cc6e241
commit bd5bfbac2d
80 changed files with 39463 additions and 108 deletions
+152
View File
@@ -166,3 +166,155 @@ test.describe("Input UX", () => {
await expect(page.locator(".send-btn")).toBeEnabled();
});
});
// ── KB (Knowledge Base) API mocks ───────────────────────────────
function mockKbApi(page: any) {
mockApi(page);
page.route("**/api/users", (route: any) => {
if (route.request().method() === "POST") {
return route.fulfill({
json: { user_id: "u_e2e_test_001", name: "E2E用户", created_at: "2026-05-23T00:00:00Z" },
});
}
return route.fulfill({
json: { users: [{ user_id: "u_e2e_test_001", name: "E2E用户", created_at: "2026-05-23T00:00:00Z" }] },
});
});
page.route("**/api/users/*/kbs", (route: any) => {
if (route.request().method() === "POST") {
return route.fulfill({
json: {
kb_id: "kb_e2e_001", user_id: "u_e2e_test_001",
name: "E2E测试库", description: "",
created_at: "2026-05-23T00:00:00Z", updated_at: "2026-05-23T00:00:00Z",
fields: [], templates: [], file_count: 0, chunk_count: 0, parse_status: "empty",
},
});
}
return route.fulfill({
json: {
kbs: [{
kb_id: "kb_e2e_001", name: "E2E测试库", description: "",
created_at: "2026-05-23T00:00:00Z", updated_at: "2026-05-23T00:00:00Z",
field_count: 10, template_count: 3, file_count: 2, chunk_count: 50, parse_status: "ready",
}],
},
});
});
page.route("**/api/kbs/*/status", (route: any) =>
route.fulfill({ json: { parse_status: "ready", file_count: 2, chunk_count: 50 } })
);
page.route("**/api/kbs/*/fields", (route: any) =>
route.fulfill({ json: {
fields: [{ name: "billNo", description: "工单号", type: "String" }],
templates: [{ name: "结算单", file: "结算单.jrxml" }],
}})
);
page.route("**/api/kbs/*", (route: any) => {
if (route.request().method() === "DELETE") {
return route.fulfill({ json: { status: "deleted" } });
}
return route.fulfill({
json: {
kb_id: "kb_e2e_001", user_id: "u_e2e_test_001",
name: "E2E测试库", description: "",
fields: [], templates: [],
file_count: 2, chunk_count: 50, parse_status: "ready",
},
});
});
page.route("**/api/kbs/*/upload", (route: any) =>
route.fulfill({ json: { filename: "test.jrxml", type: "jrxml", error: null } })
);
page.route("**/api/sessions/*/kb", (route: any) => {
if (route.request().method() === "PUT") {
return route.fulfill({ json: { kb_id: "kb_e2e_001", kb_name: "E2E测试库" } });
}
return route.fulfill({ json: { kb_id: "kb_e2e_001", kb_name: "E2E测试库" } });
});
}
// ── KB feature tests ────────────────────────────────────────────
test.describe("KB selector", () => {
test("KB selector renders in chat interface", async ({ page }) => {
await mockKbApi(page);
await page.goto("/");
await expect(page.locator(".kb-selector")).toBeVisible({ timeout: 5000 });
await expect(page.locator(".kb-label")).toContainText("知识库");
await expect(page.locator(".kb-select")).toBeVisible();
await expect(page.locator(".kb-manage-btn")).toBeVisible();
});
test("can select a KB from dropdown", async ({ page }) => {
await mockKbApi(page);
await page.goto("/");
const select = page.locator(".kb-select");
await expect(select).toBeVisible({ timeout: 5000 });
await select.selectOption({ label: "E2E测试库 (10字段, 3模板)" });
await expect(page.locator(".kb-badge")).toContainText("E2E测试库");
});
});
test.describe("KB manager", () => {
test("opens KB manager overlay", async ({ page }) => {
await mockKbApi(page);
await page.goto("/");
await page.locator(".kb-manage-btn").click();
await expect(page.locator(".kb-manager")).toBeVisible({ timeout: 3000 });
await expect(page.locator(".kb-manager h3")).toContainText("知识库管理");
});
test("can close KB manager", async ({ page }) => {
await mockKbApi(page);
await page.goto("/");
await page.locator(".kb-manage-btn").click();
await expect(page.locator(".kb-manager")).toBeVisible({ timeout: 3000 });
await page.locator(".close-btn").click();
await expect(page.locator(".kb-manager")).not.toBeVisible({ timeout: 3000 });
});
test("create form has name input and create button", async ({ page }) => {
await mockKbApi(page);
await page.goto("/");
await page.locator(".kb-manage-btn").click();
await expect(page.locator(".kb-manager")).toBeVisible({ timeout: 3000 });
await expect(page.locator('.kb-manager .create-form .kb-input').first()).toBeVisible();
await expect(page.locator('.kb-manager .create-form button.primary')).toBeVisible();
});
test("KB cards show name, status, and actions", async ({ page }) => {
await mockKbApi(page);
await page.goto("/");
await page.locator(".kb-manage-btn").click();
await expect(page.locator(".kb-manager")).toBeVisible({ timeout: 3000 });
await expect(page.locator(".kb-card")).toBeVisible({ timeout: 3000 });
await expect(page.locator(".kb-card strong")).toContainText("E2E测试库");
await expect(page.locator(".kb-status.ready")).toContainText("就绪");
await expect(page.locator(".kb-actions button")).toHaveCount(3);
});
});
test.describe("JRXML upload in chat", () => {
test("file input accepts .jrxml extension", async ({ page }) => {
await mockKbApi(page);
await page.goto("/");
const input = page.locator('.unified-input input[type="file"]');
await expect(input).toHaveAttribute("accept", /\.jrxml/);
});
});