推进本地索引与 AI changed-files 审计

- 为本地工作区补充 search/backlinks/tags/resource 引用索引与 watcher 单文件刷新
- 在页面设置中增加索引页签,并展示本地反链、标签和 AI changed-files 审计
- 补充本地搜索与本地 AI changed-files 浏览器 smoke,并回填当前优先级 checklist
This commit is contained in:
lix-2026
2026-05-19 10:22:01 +08:00
parent 1b5d6a2a2d
commit fc4a47e597
8 changed files with 968 additions and 42 deletions
@@ -72,7 +72,14 @@ async function run() {
const debug = { root, baseUrl: BASE_URL };
writeWorkspaceManifest(root, "user_real");
fs.mkdirSync(path.join(root, "docs"), { recursive: true });
fs.writeFileSync(path.join(root, "README.md"), "# Search Smoke\n打开搜索 smoke。\n", "utf8");
const token = `LOCAL-SEARCH-${Date.now()}`;
const firstRelativePath = "docs/search-target.md";
const renamedRelativePath = "docs/search-renamed.md";
fs.writeFileSync(
path.join(root, "README.md"),
`---\ntitle: Search Smoke\ntags: [alpha]\n---\n# Search Smoke\n打开搜索 smoke。\n[Target](docs/search-target.md)\n`,
"utf8",
);
const browser = await chromium.launch({
headless: true,
@@ -97,12 +104,9 @@ async function run() {
timeout: UI_TIMEOUT_MS,
});
const token = `LOCAL-SEARCH-${Date.now()}`;
const firstRelativePath = "docs/search-target.md";
const renamedRelativePath = "docs/search-renamed.md";
fs.writeFileSync(
path.join(root, firstRelativePath),
`---\ntitle: Search Target\n---\n# Search Target\n${token}\n`,
`---\ntitle: Search Target\ntags: [alpha, beta]\n---\n# Search Target\n${token}\n`,
"utf8",
);
@@ -115,6 +119,30 @@ async function run() {
`新建页面应立即可搜索: ${JSON.stringify(firstResults)}`,
);
await page.goto(documentUrl(root, firstRelativePath), {
waitUntil: "domcontentloaded",
timeout: UI_TIMEOUT_MS,
});
await page.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"]').first().waitFor({
state: "visible",
timeout: UI_TIMEOUT_MS,
});
await page.locator('[data-testid="wolai-page-settings-trigger"]').click({ timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-settings-tab="index"]').click({ timeout: UI_TIMEOUT_MS });
await page.waitForFunction(() => {
const backlinks = document.querySelector('[data-testid="wolai-page-settings-local-index-backlinks"]');
const tags = document.querySelector('[data-testid="wolai-page-settings-local-index-tags"]');
return Boolean(
backlinks && backlinks.textContent && backlinks.textContent.includes("Search Smoke")
&& tags && tags.textContent && tags.textContent.includes("#alpha"),
);
}, null, { timeout: UI_TIMEOUT_MS });
debug.localIndexPanel = await page.evaluate(() => ({
backlinks: document.querySelector('[data-testid="wolai-page-settings-local-index-backlinks"]')?.textContent || "",
tags: document.querySelector('[data-testid="wolai-page-settings-local-index-tags"]')?.textContent || "",
status: document.querySelector('[data-testid="wolai-page-settings-local-index-status"]')?.textContent || "",
}));
fs.renameSync(path.join(root, firstRelativePath), path.join(root, renamedRelativePath));
const second = await browserSearch(page, root, token);
assert.equal(second.status, 200, `重命名后搜索应成功: ${JSON.stringify(second)}`);
@@ -135,6 +163,7 @@ async function run() {
token,
firstPath: firstRelativePath,
renamedPath: renamedRelativePath,
localIndexPanel: debug.localIndexPanel,
indexExists: fs.existsSync(path.join(root, ".mnote", "index", "search-index.json")),
};
fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8");
@@ -0,0 +1,243 @@
#!/usr/bin/env node
"use strict";
const assert = require("node:assert");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { chromium } = require("playwright");
const {
BASE_URL,
UI_TIMEOUT_MS,
} = require("./tree-shell-smoke-helpers");
const CHROMIUM_EXECUTABLE_PATH = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|| ["/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/snap/bin/chromium", "/usr/bin/chromium"]
.find((candidate) => fs.existsSync(candidate));
function fileUrl(localPath) {
return `file://${localPath}`;
}
function writeWorkspaceManifest(root, ownerId, workspaceId) {
fs.mkdirSync(path.join(root, ".mnote"), { recursive: true });
fs.writeFileSync(
path.join(root, ".mnote", "workspace.json"),
`${JSON.stringify({
workspaceId,
ownerId,
createdAt: new Date().toISOString(),
capabilities: ["local_files", "ai_sessions", "markdown_edit"],
}, null, 2)}\n`,
"utf8",
);
}
async function main() {
const suffix = Date.now().toString(36);
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-local-ai-changed-files-"));
const documentId = "local-md:README.md";
const actorId = "user_real";
const sessionId = `mnote_local_ai_changed_${suffix}`;
const runId = `run_local_ai_changed_${suffix}`;
const marker = `LOCAL-AI-CHANGED-FILES-${suffix}`;
const readmePath = path.join(root, "README.md");
const captured = [];
const browser = await chromium.launch({
headless: true,
...(CHROMIUM_EXECUTABLE_PATH ? { executablePath: CHROMIUM_EXECUTABLE_PATH } : {}),
});
const context = await browser.newContext({
viewport: { width: 1440, height: 960 },
extraHTTPHeaders: {
"x-mnote-actor-id": actorId,
"x-mnote-actor-type": "user",
},
});
const page = await context.newPage();
try {
const workspaceId = `local-ws:${actorId}:task453`;
writeWorkspaceManifest(root, actorId, workspaceId);
fs.writeFileSync(readmePath, `# Local AI Changed Files\n初始内容 ${suffix}\n`, "utf8");
const rootUri = fileUrl(root);
await page.route("**/api/ai-agent/run", async (route) => {
throw new Error(`页面 AI 不应请求旧 /api/ai-agent/run: ${route.request().url()}`);
});
await page.route("**/api/hermes/client/gateway/health**", async (route) => {
await route.fulfill({
status: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify({
ok: true,
gateway: { ok: true, status: "mocked" },
profile: { name: "reasonix", modelConfigured: true, apiKeyConfigured: true },
suggestions: [],
}),
});
});
await page.route("**/api/hermes/client/tools**", async (route) => {
await route.fulfill({
status: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify({ ok: true, tools: [] }),
});
});
await page.route("**/api/hermes/client/profiles", async (route) => {
await route.fulfill({
status: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify({
ok: true,
active: "reasonix",
profiles: [{ name: "reasonix", label: "Reasonix", modelConfigured: true, apiKeyConfigured: true }],
}),
});
});
await page.route("**/api/hermes/client/sessions", async (route) => {
captured.push({ kind: "session", method: route.request().method(), body: route.request().postData() || "" });
await route.fulfill({
status: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify({
ok: true,
sessionId,
title: "本地 changed files",
traceId: `trace_local_changed_${suffix}`,
persistence: "local_ai_session_jsonl",
sessionStorage: "local_private",
}),
});
});
await page.route(`**/api/hermes/client/sessions/${sessionId}/resume`, async (route) => {
captured.push({ kind: "session-resume", method: route.request().method(), body: "" });
await route.fulfill({
status: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify({
ok: true,
sessionId,
session: { sessionId, messages: [] },
runtime: {
sessionId,
runId,
status: "completed",
profile: "reasonix",
documentId,
traceId: `trace_local_changed_resume_${suffix}`,
},
}),
});
});
await page.route("**/api/hermes/client/runs", async (route) => {
captured.push({ kind: "run", method: route.request().method(), body: route.request().postData() || "" });
await route.fulfill({
status: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify({
ok: true,
sessionId,
runId,
events: [],
traceId: `trace_local_changed_run_${suffix}`,
persistence: "local_ai_session_jsonl",
sessionStorage: "local_private",
}),
});
});
await page.route(`**/api/hermes/client/events/${runId}`, async (route) => {
captured.push({ kind: "events", method: route.request().method(), body: "" });
fs.appendFileSync(readmePath, `\nAI 写入标记:${marker}\n`, "utf8");
await route.fulfill({
status: 200,
headers: { "content-type": "text/event-stream; charset=utf-8" },
body:
`data: ${JSON.stringify({ event: "message.delta", run_id: runId, session_id: sessionId, delta: "已修改本地 README。" })}\n\n` +
`data: ${JSON.stringify({
event: "run.completed",
run_id: runId,
session_id: sessionId,
output: "已修改本地 README。",
agentAudit: {
eventId: `audit_local_changed_${suffix}`,
rootUri,
diffSummary: "1 changed file(s)",
changedFiles: [
{
path: "README.md",
changeType: "modified",
summary: `追加 ${marker}`,
},
],
},
})}\n\n`,
});
});
const documentUrl = new URL(`${BASE_URL}/documents/${encodeURIComponent(documentId)}`);
documentUrl.searchParams.set("sourceKind", "local_folder");
documentUrl.searchParams.set("rootUri", rootUri);
await page.goto(documentUrl.toString(), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
() => (document.body.textContent || "").includes("Local AI Changed Files"),
null,
{ timeout: UI_TIMEOUT_MS },
);
await page.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"]').first().waitFor({
state: "attached",
timeout: UI_TIMEOUT_MS,
}).catch(() => undefined);
await page.getByTestId("wolai-floating-ai").click({ timeout: UI_TIMEOUT_MS });
await page.locator("[data-page-ai-input]").fill(`请修改 README 并记录 changed files ${marker}`, { timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-ai-action="send"]').click({ timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
() => {
const drawerText = document.querySelector('[data-testid="wolai-page-ai-drawer"]')?.textContent || "";
return drawerText.includes("agent.changed_files") && drawerText.includes("README.md");
},
null,
{ timeout: UI_TIMEOUT_MS },
);
const cards = await page.$$eval("[data-page-ai-tool-card]", (nodes) =>
nodes.map((node) => ({
status: node.getAttribute("data-page-ai-tool-status"),
text: node.textContent || "",
})),
);
assert(
cards.some((card) =>
card.status === "completed"
&& card.text.includes("agent.changed_files")
&& card.text.includes("README.md")
&& card.text.includes(marker)
),
`本地 AI changed files 工具卡未显示 README.md 与 diff 摘要: ${JSON.stringify(cards)}`,
);
assert(fs.readFileSync(readmePath, "utf8").includes(marker), "本地 README.md 未写入 smoke 标记");
assert(captured.some((entry) => entry.kind === "run"), "未捕获 page AI run 请求");
console.log(
JSON.stringify({
ok: true,
root,
documentId,
sessionId,
runId,
marker,
capturedKinds: captured.map((entry) => entry.kind),
}, null, 2),
);
} finally {
await context.close().catch(() => undefined);
await browser.close().catch(() => undefined);
fs.rmSync(root, { recursive: true, force: true });
}
}
main().catch((error) => {
console.error(error instanceof Error ? error.stack || error.message : String(error));
process.exit(1);
});