From d0ec8dcc5d9f53ef6caafce8b09c35594020b90f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E5=B8=86?= Date: Mon, 2 Nov 2026 10:42:21 +0800 Subject: [PATCH] feat(knowledge): add optional ingestion and status center (#7518) * feat(knowledge): add optional ingestion and status center * docs(knowledge): record upstream CI budget fix and regression results --------- Co-authored-by: Willem Jiang --- README.md | 6 + backend/app/gateway/AGENTS.md | 9 +- backend/app/gateway/auth_middleware.py | 8 + backend/tests/test_agent_guidance_check.py | 1 + .../tests/test_knowledge_center_extension.py | 77 +++ docs/full-stack-plugins.md | 12 + .../AGENTS.md | 16 + .../README.md | 56 ++ .../VALIDATION.md | 83 +++ .../__init__.py | 22 + .../static/center.mjs | 542 ++++++++++++++++++ .../static/copy.mjs | 161 ++++++ .../static/index.mjs | 20 + .../static/styles.css | 211 +++++++ .../ui_manifest.json | 10 + .../pyproject.toml | 17 + .../screenshots/dark.png | Bin 0 -> 48094 bytes .../screenshots/en-desktop.png | Bin 0 -> 51215 bytes .../screenshots/zh-mobile.png | Bin 0 -> 30150 bytes frontend/AGENTS.md | 10 +- .../components/workspace/plugin-surfaces.tsx | 8 + frontend/src/core/extensions/catalog.ts | 5 + frontend/src/core/extensions/contracts.ts | 9 + frontend/src/core/extensions/services.ts | 32 ++ frontend/src/core/extensions/surfaces.ts | 13 + frontend/src/core/i18n/locales/en-US.ts | 5 + frontend/src/core/i18n/locales/types.ts | 1 + frontend/src/core/i18n/locales/zh-CN.ts | 5 + frontend/tests/e2e/knowledge-center.spec.ts | 484 ++++++++++++++++ .../unit/core/extensions/services.test.ts | 62 ++ 30 files changed, 1879 insertions(+), 6 deletions(-) create mode 100644 backend/tests/test_knowledge_center_extension.py create mode 100644 examples/deerflow-extension-knowledge-center/AGENTS.md create mode 100644 examples/deerflow-extension-knowledge-center/README.md create mode 100644 examples/deerflow-extension-knowledge-center/VALIDATION.md create mode 100644 examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/__init__.py create mode 100644 examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/static/center.mjs create mode 100644 examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/static/copy.mjs create mode 100644 examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/static/index.mjs create mode 100644 examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/static/styles.css create mode 100644 examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/ui_manifest.json create mode 100644 examples/deerflow-extension-knowledge-center/pyproject.toml create mode 100644 examples/deerflow-extension-knowledge-center/screenshots/dark.png create mode 100644 examples/deerflow-extension-knowledge-center/screenshots/en-desktop.png create mode 100644 examples/deerflow-extension-knowledge-center/screenshots/zh-mobile.png create mode 100644 frontend/tests/e2e/knowledge-center.spec.ts diff --git a/README.md b/README.md index 5d5a5c4920..93856f54bc 100644 --- a/README.md +++ b/README.md @@ -1749,6 +1749,12 @@ adds connection policies, hierarchical spaces, members and explicit resource sha Space-routed management retains existing administrator and Provider grants. It is disabled by default and includes no management page or global chat retrieval ACL. +The optional [knowledge center extension](examples/deerflow-extension-knowledge-center/README.md) +adds an independently enabled workspace page for authorized spaces, file/URL/text +ingestion and document processing snapshots. It consumes the platform and management +APIs; it does not grant permissions or add entry editing, versions, graph browsing, +complete document/task directories or Trace. Retrieval works without it. + The optional [knowledge management extension](examples/deerflow-extension-knowledge-management/README.md) adds administrator-authorized file, URL and manual ingestion, processing status, reprocessing and cancellation through WeKnora. Parsing, indexing and optional diff --git a/backend/app/gateway/AGENTS.md b/backend/app/gateway/AGENTS.md index e1199f7948..af94fd6d16 100644 --- a/backend/app/gateway/AGENTS.md +++ b/backend/app/gateway/AGENTS.md @@ -2,7 +2,14 @@ Reject external `sandbox`, `thread_data`, `viewed_images`, `goal_outcome`. Non-null goals: valid text, int counters/limits >=0, no Overwrite; keep sparse data. -Studio retains sanitized creation metadata. +[Viewer fence](../../../docs/full-stack-plugins.md). + +Studio keeps sanitized creation metadata. + +Capability Center `business` validates credentials, creates MCP connections. +MCP allows only its isolated launcher/credential environment keys; reject +arbitrary commands/manifest-policy bypasses. Keep admin checks, masked +atomic edits, cache reloads; exclude credentials from tool schemas. HTTP and run metadata creation share `goal_thread_lock`. Lock in detached `_ensure_thread_metadata`; preserve await-free attachment and cancel/drain diff --git a/backend/app/gateway/auth_middleware.py b/backend/app/gateway/auth_middleware.py index b2c01e40b0..c588ea1c7e 100644 --- a/backend/app/gateway/auth_middleware.py +++ b/backend/app/gateway/auth_middleware.py @@ -207,6 +207,14 @@ class AuthMiddleware(BaseHTTPMiddleware): }, ) + # Browser extension pages are bound to the account that loaded their + # descriptor. Cookies can change in another tab before React observes it. + # This is a stale-view fence, not an authorization grant; CLI callers may + # omit it and all existing route/resource checks still apply. + expected_viewer = request.headers.get("x-deerflow-plugin-viewer") + if get_request_route_path(request).startswith("/api/extensions/") and expected_viewer is not None and expected_viewer != str(user.id): + return JSONResponse(status_code=409, content={"detail": "Account changed; reload this extension view."}) + # Stamp both request.state.user (for the contextvar pattern) # and request.state.auth (so @require_permission's "auth is # None" branch short-circuits instead of running the entire diff --git a/backend/tests/test_agent_guidance_check.py b/backend/tests/test_agent_guidance_check.py index c3af1c716d..b8e7eee6a7 100644 --- a/backend/tests/test_agent_guidance_check.py +++ b/backend/tests/test_agent_guidance_check.py @@ -66,6 +66,7 @@ EXPECTED_GUIDANCE_PATHS = { "examples/deerflow-extension-weknora/AGENTS.md", "examples/deerflow-extension-knowledge-management/AGENTS.md", "examples/deerflow-extension-knowledge-platform/AGENTS.md", + "examples/deerflow-extension-knowledge-center/AGENTS.md", "examples/deerflow-extension-lightrag/AGENTS.md", } diff --git a/backend/tests/test_knowledge_center_extension.py b/backend/tests/test_knowledge_center_extension.py new file mode 100644 index 0000000000..23025bc196 --- /dev/null +++ b/backend/tests/test_knowledge_center_extension.py @@ -0,0 +1,77 @@ +"""The optional page must ship real assets without installing a second backend.""" + +from pathlib import Path + +import pytest + +from deerflow.extensions.loader import ExtensionSpec, load_extensions + + +@pytest.fixture +def load_center(monkeypatch): + monkeypatch.syspath_prepend(str(Path(__file__).resolve().parents[2] / "examples/deerflow-extension-knowledge-center")) + + def load(config): + return load_extensions([ExtensionSpec(use="deerflow_extension_knowledge_center:install", config=config)]) + + return load + + +@pytest.mark.parametrize("enabled", [False, True]) +def test_optional_center_has_packaged_page_and_no_management_backend(load_center, enabled): + loaded, diagnostics = load_center({"enabled": enabled}) + assert not diagnostics + ((_, plugin),) = loaded.plugins + assert plugin.namespace == "knowledge-center" + assert plugin.enabled is enabled + assert not plugin.backend and not plugin.tools and not loaded.routers + assets = plugin.frontend + assert assets.entry == "static/index.mjs" + assert set(assets.files) == {"static/index.mjs", "static/center.mjs", "static/styles.css", "static/copy.mjs"} + assert b"requestBackend" in assets.files["static/center.mjs"].content + + +@pytest.mark.parametrize("config", [{"enabled": "true"}, {"endpoint": "https://example.invalid"}, {"enabled": True, "token": "secret"}]) +def test_page_config_cannot_introduce_credentials_or_provider_configuration(load_center, config): + loaded, diagnostics = load_center(config) + assert not loaded.plugins + assert diagnostics + + +@pytest.mark.asyncio +@pytest.mark.parametrize("method", ["GET", "POST"]) +@pytest.mark.parametrize("root_path", ["", "/proxy"]) +async def test_stale_browser_viewer_never_reaches_contributed_routes(monkeypatch, method, root_path): + from types import SimpleNamespace + + import httpx + from fastapi import FastAPI + + from app.gateway.auth_middleware import AuthMiddleware + + async def user(request): + return SimpleNamespace(id="bob", system_role="admin") + + async def permissions(*args, **kwargs): + return [] + + monkeypatch.setattr("app.gateway.auth_middleware.is_auth_disabled", lambda: False) + monkeypatch.setattr("app.gateway.deps.get_current_user_from_request", user) + monkeypatch.setattr("app.gateway.auth_middleware.resolve_route_permissions", permissions) + app = FastAPI() + app.add_middleware(AuthMiddleware) + calls = [] + + @app.api_route("/api/extensions/example/documents", methods=["GET", "POST"]) + async def contributed(): + calls.append("provider") + return {"ok": True} + + async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app, root_path=root_path), base_url="http://test", cookies={"access_token": "session-b"}) as client: + path = root_path + "/api/extensions/example/documents" + stale = await client.request(method, path, headers={"X-Deerflow-Plugin-Viewer": "alice"}) + assert stale.status_code == 409 + assert calls == [] + for header in ({"X-Deerflow-Plugin-Viewer": "bob"}, {}): + assert (await client.request(method, path, headers=header)).status_code == 200 + assert calls == ["provider", "provider"] diff --git a/docs/full-stack-plugins.md b/docs/full-stack-plugins.md index 494df3946a..fc1b3d9cff 100644 --- a/docs/full-stack-plugins.md +++ b/docs/full-stack-plugins.md @@ -1,5 +1,17 @@ # Full-stack plugin contributions +Page surfaces may use optional `context.requestBackend(path, options)` for +contributed `/api/extensions/` routes. It returns the HTTP Response and supports +method, headers, body (including File) and an optional signal. The host supplies +the configured Gateway URL, credentialed CSRF fetcher, descriptor-bound viewer +header and page abort signal. Foreign URLs, traversal and redirects are rejected; +writes are never retried. Gateway returns 409 for stale viewer headers before +extension dispatch. This is not an authorization grant: contributed routes must +retain all principal, plugin and resource checks. Older hosts omit this service; +modules must render an unavailable state. The independent +[knowledge center](../examples/deerflow-extension-knowledge-center/README.md) +demonstrates capability-aware ingestion and status views using existing APIs. + A deployment-installed Python extension can register a `PluginContribution` with optional browser code, authenticated backend actions and model tools. This extends the existing `install(registry, config)` workflow. MCP and Skills keep their existing diff --git a/examples/deerflow-extension-knowledge-center/AGENTS.md b/examples/deerflow-extension-knowledge-center/AGENTS.md new file mode 100644 index 0000000000..718e79b839 --- /dev/null +++ b/examples/deerflow-extension-knowledge-center/AGENTS.md @@ -0,0 +1,16 @@ +# Knowledge center + +Keep this browser-only package independent of management/backend activation. +Consume platform routes through host `requestBackend`; never import host modules, +duplicate Provider logic or store credentials/content in browser storage. +Follow semantic tokens, system UI type, 8px controls, en/zh copy and 375px layouts. + +Capability is not authorization or observed execution. Preserve real resource +identities, unknown states and explicit false options. Never invent directories, +task IDs, percentages or Trace. Missing entry/version/graph APIs stay unavailable. + +Observe page/selection cancellation, keep one selector listener, order document +reads and invalidate them before writes. Use textContent for external text. +Never retry writes; failed submitted writes require explicit reconciliation. +Update ui_manifest.json for every asset and verify wheel contents. Tests belong +in backend/tests and frontend/tests/e2e/knowledge-center.spec.ts. diff --git a/examples/deerflow-extension-knowledge-center/README.md b/examples/deerflow-extension-knowledge-center/README.md new file mode 100644 index 0000000000..397fae9149 --- /dev/null +++ b/examples/deerflow-extension-knowledge-center/README.md @@ -0,0 +1,56 @@ +# Optional knowledge center extension + +First usable UI slice of phase 4 in [#6269's five-stage plan](https://github.com/bytedance/deer-flow/issues/6269#issuecomment-6281785410), consuming the platform and management APIs merged in #7417 and #7293. This browser-only Full-Stack Plugin registers an independently enabled page. It adds no Provider, credentials, parser, document store, model tool or authorization bypass. + +## Installation + +From the repository root: + +```sh +make extension-install SOURCE=./examples/deerflow-extension-knowledge-center +``` + +Install and configure the [knowledge platform](../deerflow-extension-knowledge-platform/README.md) and [knowledge management](../deerflow-extension-knowledge-management/README.md) separately. The management namespace must remain enabled; its direct routes may be disabled with `routes_enabled: false`. Add this entry alongside those entries in operator-owned `config.yaml`, then restart Gateway: + +```yaml +plugins: + # Keep the separately configured management and platform entries here. + - use: deerflow_extension_knowledge_center:install + config: + enabled: true +``` + +The only page setting is boolean `enabled` (default `false`). Endpoints, keys, factories and identities are rejected. The package requires the host's optional `SurfaceContext.requestBackend` introduced with this example; older hosts display an update message. Open **Knowledge center / 知识中心** in the workspace sidebar, or `/workspace/extensions/knowledge-center/library`. + +Set `enabled: false`, disable the entry point, or uninstall and restart Gateway to remove the page entry. Reload browser tabs to refresh discovery. This does not disable the platform/management API, delete policy data or upstream documents, or affect chat retrieval. Page hiding is not authorization. Installing the UI without the platform displays an unavailable state. + +## Use and behavior + +1. Choose an accessible space and mapped knowledge base. The current projection supplies native base IDs rather than human base titles; the selector preserves them and distinguishes connections. Empty membership/resource sets have explicit next steps. +2. The server intersects space policy with actual Provider capabilities. Non-admin members can browse effective resources but see no management controls when management authorization fails. Capability discovery is not a write grant: every server request still checks the real user, both plugin namespaces, space policy and unchanged Provider grants. +3. Import text, an HTTP(S) URL or a nonempty file up to 16 MiB using supported operations. Supported processing options preserve explicit `false` and validate paired chunk fields. Omitted new-document options inherit platform defaults. Files use the existing raw upload route, without base64 or plugin-action size limits. +4. Inspect returned document snapshots or look up an existing document by upstream ID. Refresh is explicit. Reprocess/cancel require inline confirmation; reprocessing sends empty options to preserve upstream settings. Document read ordering prevents an older status response from replacing a newer processing response. + +Requests use the configured Gateway URL, host credentialed CSRF fetcher, page cancellation signal and descriptor-bound viewer header. Gateway rejects stale viewers before contributed routes run, including when another tab switches accounts. Redirects are rejected; writes are never automatically retried. + +After **any failed submitted write**, the page warns that the Provider may already have accepted it and disables further writes until the user explicitly acknowledges upstream reconciliation. Check the upstream directory or query status first. Capability rejection before submission does not submit a write. Leaving the page can abort the browser wait but cannot cancel an already accepted upstream operation. + +## Delivery boundary + +- This is the first phase-4 slice, not the whole phase. The current management contract has no complete document/task listing, independent entry browsing/editing/versions or graph browsing API. The page explains unavailable features without inventing directories, graphs or editing endpoints. These require follow-up reviewed Provider contracts. +- At most 50 snapshots are held **for the selected resource during this visit**. They are not a durable task ledger. Reload, space/resource changes, unmount and account changes discard state. No user content is stored in localStorage/sessionStorage. +- Unknown stays unknown; `202` is not completion. `searchable` never implies all Wiki/graph/summary steps succeeded. No percentages, step timelines, elapsed times or Trace events are fabricated. Phase 5 still needs real events. +- This page does not edit platform policy/membership, create upstream bases, change chat scope, grant retrieval access or impersonate upstream users. Existing retrieval works independently. + +## Verification + +```sh +cd backend +uv run --no-sync pytest tests/test_knowledge_center_extension.py tests/test_auth_middleware.py tests/test_knowledge_platform_extension.py -q +cd ../frontend +pnpm check +pnpm exec rstest run tests/unit/core/extensions +pnpm exec playwright test tests/e2e/knowledge-center.spec.ts +``` + +Tests load the real extension/static registry, exercise Gateway authentication middleware over HTTP (including root paths and stale viewers), and load shipped native browser modules through the real host with mocked management HTTP. This is synthetic contract coverage, not a live WeKnora or production multi-user acceptance claim. Results are recorded in [VALIDATION.md](VALIDATION.md). diff --git a/examples/deerflow-extension-knowledge-center/VALIDATION.md b/examples/deerflow-extension-knowledge-center/VALIDATION.md new file mode 100644 index 0000000000..f60846f2c5 --- /dev/null +++ b/examples/deerflow-extension-knowledge-center/VALIDATION.md @@ -0,0 +1,83 @@ +# Phase-4 knowledge center validation + +## CI follow-up on 2026-10-26 + +The initial GitHub run passed frontend unit tests, all four browser shards, +both replay-contract layers, package checks, blocking-I/O, lint, and backend +shards 1/2/4. Backend shard 3 reported 9,607 passed and one guidance-budget +failure: the merged runtime chain was 99,004 bytes against the 98,304-byte limit. +Upstream reproduced and fixed that same failure in #7522. Main at +`3d5107dc0dd13a1f83b696b6682483aea64da896` was merged into this branch; +the guidance checker now reports **0 errors and 0 warnings**. The combined +guidance/extension/auth/platform/bookmark regression passes **139 tests**; +backend Ruff formatting and lint pass. Frontend extension unit tests remain +**100 passed**. This records local follow-up evidence; +the new head's remote CI result must be checked separately. + +Validated on 2026-10-25 on Windows with Python 3.12.15, Node 24.16.0 and pnpm 10.26.2, based on `885062e197d53abb771e6c3cd1fb1223e45e9bbb` (including phase 3, #7417). + +## Passing checks + +| Check | Actual result | +| --- | --- | +| Backend extension, authentication, platform, bookmark and platform blocking-I/O tests | 125 passed | +| Complete strict blocking-I/O suite | 277 passed, 8 skipped | +| Guidance and new knowledge-center tests after registering the new guide | 23 passed | +| Frontend extension unit tests (11 files) | 100 passed | +| Frontend API unit tests | 87 passed | +| `pnpm check` | Passed: ESLint and TypeScript | +| Changed TypeScript and shipped native-module/style formatting | Passed | +| Backend `ruff format .` and `ruff check .`, plus new package | Passed | +| `next build --webpack`, then `node scripts/build-search-index.mjs` | Passed; search index scanned 137 pages | +| Knowledge-center Chromium tests against production server | 11 passed | +| Existing bookmark, business-plugin and plugin-icon browser regressions | 19 passed | +| `scripts/check_agent_guidance.py` | 0 errors, 7 soft-limit warnings | +| `git diff --check` | Passed | +| Wheel and sdist build; isolated wheel installation and entry-point registration | Passed; all four manifest assets included and compared with source | + +The new browser suite covers English desktop and Chinese 375px mobile, dark theme, manual/URL/raw-file imports, explicit false processing fields, invalid inputs, status lookup, confirmed reprocess/cancel, revoked capabilities, browse-only membership, uncertain writes without replay, delayed read/write ordering, resource switching, disabled/empty/unavailable states and recovery. Screenshots in `screenshots/` show synthetic data rendered through the actual host. + +Backend HTTP tests prove mismatched descriptor viewers cannot reach contributed GET/POST handlers, including a proxy root path. Same-viewer and headerless clients retain existing behavior. The initial extension tests and stale-viewer tests failed before their corresponding implementation; the delayed-status browser regression also failed with the read-order guard removed. + +## Broader-suite limitations + +The whole local test run is **not green**: + +- Backend offline attempt: **4,224 passed, 172 skipped, 14 deselected, 10 failed**, stopped by `--maxfail=10`. One failure was the newly added guide missing from the approved guidance inventory; fixed and verified by the 23-test run above. Other failures were an atomic worker timeout, a quantized-clock benchmark precondition and seven credential-staging cases using unsupported Windows `os.utime(..., follow_symlinks=False)`. +- The worker timeout and credential-staging failures were reproduced in a separate, unchanged worktree at the exact base commit. The quantized-clock case passed alone on this branch and on the unchanged base. These results do not establish that the rest of the unexecuted backend suite passes. +- Frontend full unit run: **3,544 passed, 5 failed** from five-second timeouts in the unchanged scheduled-task schedule-input and task-form-dialog tests. Rerunning those two files gave **45 passed, 1 timeout**; that remaining task-form case passed when run alone. No scheduled-task production or test files were changed. The complete suite was not rerun to claim an all-green result. +- The first production-build attempt used dependencies from an older worktree and failed on missing `@headlessui/react`. After a local `pnpm install --frozen-lockfile`, the production Webpack build and `pnpm check` both passed. No dependency manifests or locks changed. + +Native Windows SWC is blocked by this machine's application-control policy, so the production build uses the supported Webpack option. `make` is unavailable locally; the equivalent formatter, linter and test commands were run directly. + +## Reproduction + +From `backend/`: + +```sh +uv run --no-sync pytest tests/test_knowledge_center_extension.py tests/test_auth_middleware.py tests/test_knowledge_platform_extension.py tests/test_bookmark_plugin.py tests/blocking_io/test_knowledge_platform_extension.py -q +uv run --no-sync pytest tests/blocking_io -q +uv run --no-sync pytest tests/test_agent_guidance_check.py tests/test_knowledge_center_extension.py -q +uv run --no-sync ruff format . +uv run --no-sync ruff check . +``` + +From `frontend/`: + +```sh +pnpm check +pnpm exec rstest run tests/unit/core/extensions +pnpm exec rstest run tests/unit/core/api +# Set BETTER_AUTH_SECRET=local-dev-secret, SKIP_ENV_VALIDATION=1, +# DEER_FLOW_AUTH_DISABLED=1 in the shell before building/starting. +pnpm exec next build --webpack +node scripts/build-search-index.mjs +pnpm exec next start --hostname 127.0.0.1 --port 3016 +# In another shell, set PLAYWRIGHT_BASE_URL=http://127.0.0.1:3016 +# and PLAYWRIGHT_SKIP_WEB_SERVER=1. Direct Node avoids a local Windows +# pnpm argument-forwarding issue with Playwright filename filters. +node node_modules/@playwright/test/cli.js test tests/e2e/knowledge-center.spec.ts --workers=2 --reporter=line +node node_modules/@playwright/test/cli.js test tests/e2e/bookmark-plugin.spec.ts tests/e2e/business-plugins.spec.ts tests/e2e/plugin-icons.spec.ts --workers=2 --reporter=line +``` + +From this package directory, `uv build` builds the independently installable wheel and sdist. Tests exercise the real extension loader/static registry, authentication middleware and native browser modules; management HTTP responses in the browser suite are mocked. No live WeKnora connection, production deployment, persistent task ledger, multi-user acceptance, complete phase-4 implementation or phase-5 Trace is claimed. diff --git a/examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/__init__.py b/examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/__init__.py new file mode 100644 index 0000000000..094fe5e537 --- /dev/null +++ b/examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/__init__.py @@ -0,0 +1,22 @@ +"""Browser-only client of the independently authorized platform API.""" + +from pathlib import Path + +from deerflow_extension_api import extension +from deerflow_extension_api.plugins import BrowserAssets, PluginContribution + + +@extension(api="0.2.9", name="knowledge-center") +def install(registry, config): + if set(config) - {"enabled"} or type(config.get("enabled", False)) is not bool: + raise ValueError("Knowledge center accepts only a boolean enabled setting") + if not registry.plugin( + PluginContribution( + namespace="knowledge-center", + title="知识中心 / Knowledge center", + description="浏览授权空间,导入知识并查询处理状态 / Browse authorized spaces, import knowledge and inspect processing status", + enabled=config.get("enabled", False), + frontend=BrowserAssets("knowledge-center.v1", Path(__file__).parent), + ) + ): + raise RuntimeError("Knowledge center requires the full-stack plugin host") diff --git a/examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/static/center.mjs b/examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/static/center.mjs new file mode 100644 index 0000000000..617a461569 --- /dev/null +++ b/examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/static/center.mjs @@ -0,0 +1,542 @@ +import { copy } from "./copy.mjs"; + +const API = "/api/extensions/knowledge-platform"; +const enc = encodeURIComponent; +const MAX_FILE = 16 * 1024 * 1024; + +// No host imports, provider credentials, persistence or automatic write retries. +export function mountCenter(root, context) { + const w = copy[context.locale.startsWith("zh") ? "zh" : "en"]; + const make = (tag, text = "", attrs = {}) => { + const node = document.createElement(tag); + node.textContent = text; + for (const [key, value] of Object.entries(attrs)) + node.setAttribute(key, value); + return node; + }; + const listen = (node, event, handler) => + node.addEventListener(event, handler, { signal: context.signal }); + const button = (text, handler) => { + const node = make("button", text, { type: "button" }); + listen(node, "click", handler); + return node; + }; + const field = (label, node) => { + const wrapper = make("label", label); + wrapper.append(node); + return wrapper; + }; + const select = (label, items) => { + const node = make("select", "", { "aria-label": label }); + for (const [value, text] of items) + node.append(make("option", text, { value })); + return node; + }; + const style = make("link", "", { + rel: "stylesheet", + crossorigin: "use-credentials", + href: new URL("./styles.css", import.meta.url).href, + }); + const shell = make("div", "", { class: "center" }); + const status = make("p", "", { role: "status" }); + const error = make("p", "", { role: "alert", class: "error" }); + const spaces = select(w.space, []); + const bases = select(w.base, []); + const content = make("div"); + const reload = button(w.reload, () => void run(loadSpaces)); + const selectors = make("div", "", { class: "selectors" }); + selectors.append(field(w.space, spaces), field(w.base, bases), reload); + shell.append( + make("h2", w.heading), + make("p", w.intro, { class: "intro" }), + selectors, + status, + error, + content, + ); + root.append(style, shell); + let disposed = false; + let generation = 0; + let controller = new AbortController(); + let busy = false; + let latestResources = []; + // Selection-scoped only. No browser storage across accounts or visits. + let records = new Map(); + const active = (g) => + !disposed && !context.signal.aborted && g === generation; + const message = (cause) => + ({ + 401: w.denied, + 403: w.denied, + 404: w.unavailable, + 409: w.conflict, + 422: w.invalid, + })[cause.status] ?? w.failed; + async function request(path, options = {}) { + const response = await context.requestBackend(path, { + ...options, + signal: controller.signal, + }); + if (!response.ok) + throw Object.assign(new Error("Knowledge request failed"), { + status: response.status, + }); + return response.json(); + } + async function run(operation) { + error.textContent = ""; + const pending = operation(); + const g = generation; + try { + await pending; + } catch (cause) { + if (active(g) && cause.name !== "AbortError") { + status.textContent = ""; + error.textContent = message(cause); + } + } + } + function reset() { + controller.abort(); + controller = new AbortController(); + generation++; + records = new Map(); + error.textContent = ""; + status.textContent = ""; + content.replaceChildren(); + } + async function loadSpaces() { + reset(); + const g = generation; + spaces.disabled = bases.disabled = true; + spaces.replaceChildren(); + bases.replaceChildren(); + status.textContent = w.loading; + const data = await request(`${API}/spaces`); + if (!active(g)) return; + for (const item of data.items) + spaces.append(make("option", item.title, { value: item.space_id })); + spaces.disabled = data.items.length === 0; + if (!data.items.length) { + status.textContent = w.noSpaces; + return; + } + await run(loadSpace); + } + async function loadSpace() { + reset(); + const g = generation; + bases.disabled = true; + bases.replaceChildren(); + status.textContent = w.loading; + const data = await request(`${API}/spaces/${enc(spaces.value)}`); + if (!active(g)) return; + for (const item of data.resources) { + // The current platform projection has no human knowledge-base title. + // Keep the exact native identifier visible instead of inventing one. + bases.append( + make("option", `${item.native_base_id} · ${item.connection_id}`, { + value: item.resource_id, + }), + ); + } + bases.disabled = data.resources.length === 0; + latestResources = data.resources; + if (!data.resources.length) { + status.textContent = w.noBases; + return; + } + await loadResource(data.resources); + } + async function loadResource(resources) { + const item = resources.find((r) => r.resource_id === bases.value); + if (!item) return; + reset(); + const g = generation; + const prefix = `${API}/spaces/${enc(spaces.value)}/resources/${enc(item.resource_id)}`; + status.textContent = w.loading; + const details = make("details"); + details.append( + make("summary", w.details), + make( + "pre", + JSON.stringify( + { + provider: item.provider, + connection: item.connection_id, + knowledge_base: item.native_base_id, + dataset: item.dataset_id, + processing: item.processing, + }, + null, + 2, + ), + ), + ); + content.append(details); + const future = make("section", "", { class: "future" }); + future.append(make("h3", w.future), make("p", w.futureText)); + let capabilities; + try { + capabilities = await request(`${prefix}/capabilities`); + } catch (cause) { + if (!active(g)) return; + status.textContent = ""; + content.append( + make("p", cause.status === 403 ? w.browseOnly : message(cause)), + future, + ); + return; + } + if (!active(g)) return; + status.textContent = ""; + const operations = new Set(capabilities.operations); + const supported = new Set(capabilities.processing_options); + const section = make("section"); + const taskSection = make("section"); + let uncertain = false; + let readEpoch = 0; + const readVersions = new Map(); + const warning = make("div", "", { class: "warning" }); + warning.hidden = true; + const acknowledge = button(w.reconcile, () => { + uncertain = false; + warning.hidden = true; + controls(); + }); + warning.append(make("p", w.uncertain), acknowledge); + const mutationControls = []; + const controls = () => { + for (const node of mutationControls) node.disabled = busy || uncertain; + spaces.disabled = bases.disabled = reload.disabled = busy; + }; + const write = async (action, operation) => { + if (busy || uncertain || !active(g)) return; + readEpoch++; + busy = true; + controls(); + error.textContent = ""; + let submitted = false; + try { + // Refresh capabilities before every explicit write; server admission is authoritative. + const fresh = await request(`${prefix}/capabilities`); + if (!active(g)) return; + if (!fresh.operations.includes(action)) + throw Object.assign(new Error("Unsupported"), { status: 422 }); + submitted = true; + const result = await operation(); + if (!active(g)) return; + record(result); + status.textContent = w.accepted; + } catch (cause) { + if (active(g) && cause.name !== "AbortError") { + error.textContent = message(cause); + if (submitted) { + uncertain = true; + warning.hidden = false; + } + } + } finally { + busy = false; + if (active(g)) controls(); + } + }; + const jsonWrite = (path, body) => + request(path, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + section.append(make("h3", w.import), warning); + const kinds = ["manual", "url", "upload"].filter((kind) => + operations.has(kind), + ); + if (kinds.length) { + const form = make("form"); + const kind = select( + w.import, + kinds.map((k) => [k, w[k]]), + ); + const title = make("input", "", { maxlength: "500" }); + const text = make("textarea", "", { rows: "7", maxlength: "1000000" }); + const url = make("input", "", { type: "url" }); + const file = make("input", "", { type: "file" }); + const titleField = field(w.title, title), + textField = field(w.content, text), + urlField = field(w.address, url), + fileField = field(w.file, file); + form.append( + field(w.import, kind), + titleField, + textField, + urlField, + fileField, + ); + const updateKind = () => { + titleField.hidden = kind.value === "upload"; + textField.hidden = kind.value !== "manual"; + urlField.hidden = kind.value !== "url"; + fileField.hidden = kind.value !== "upload"; + }; + listen(kind, "change", updateKind); + updateKind(); + const optionFields = new Map(); + const options = make("details"); + options.append(make("summary", w.options), make("p", w.defaults)); + for (const [key, label] of [ + ["summary_enabled", w.summary], + ["graph_enabled", w.graph], + ]) { + if (!supported.has(key)) continue; + const node = select(label, [ + ["", w.inherit], + ["true", w.yes], + ["false", w.no], + ]); + options.append(field(label, node)); + optionFields.set(key, node); + } + if (supported.has("chunk_size") && supported.has("chunk_overlap")) { + for (const [key, label, min, max] of [ + ["chunk_size", w.chunkSize, 64, 8192], + ["chunk_overlap", w.overlap, 0, 4096], + ]) { + const node = make("input", "", { + type: "number", + min, + max, + step: "1", + }); + options.append(field(label, node)); + optionFields.set(key, node); + } + } + form.append(options); + const validation = make("p", "", { role: "alert", class: "error" }); + const submit = make("button", w.submit, { + type: "submit", + class: "primary", + }); + mutationControls.push(submit); + form.append(validation, submit); + section.append(form); + listen(form, "submit", (event) => { + event.preventDefault(); + validation.textContent = ""; + const processing = {}; + for (const [key, node] of optionFields) { + if (node.value !== "") + processing[key] = key.startsWith("chunk_") + ? Number(node.value) + : node.value === "true"; + } + const size = processing.chunk_size, + overlap = processing.chunk_overlap; + if ( + (size === undefined) !== (overlap === undefined) || + (size !== undefined && + (!Number.isInteger(size) || + size < 64 || + size > 8192 || + !Number.isInteger(overlap) || + overlap < 0 || + overlap > 4096 || + overlap >= size)) + ) { + validation.textContent = w.badChunks; + return; + } + const mode = kind.value; + if (mode === "manual" && (!title.value.trim() || !text.value.trim())) { + validation.textContent = w.required; + return; + } + if (mode === "url") { + try { + const parsed = new URL(url.value); + if ( + !["http:", "https:"].includes(parsed.protocol) || + parsed.username || + parsed.password || + parsed.hash + ) + throw new Error(); + } catch { + validation.textContent = w.badUrl; + return; + } + } + const selected = file.files?.[0]; + if ( + mode === "upload" && + (!selected || !selected.size || selected.size > MAX_FILE) + ) { + validation.textContent = w.fileSize; + return; + } + const body = { + kind: mode, + title: title.value, + processing, + ...(mode === "manual" ? { content: text.value } : { url: url.value }), + }; + // Serialize now, before async admission, so edits cannot alter the admitted request. + const encoded = JSON.stringify(body); + void write(mode, () => + mode === "upload" + ? request(`${prefix}/files?filename=${enc(selected.name)}`, { + method: "POST", + headers: { + "Content-Type": "application/octet-stream", + "X-Processing-Options": JSON.stringify(processing), + }, + body: selected, + }) + : request(`${prefix}/documents`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: encoded, + }), + ); + }); + } else section.append(make("p", w.noImport)); + taskSection.append(make("h3", w.tasks), make("p", w.taskIntro)); + const rows = make("div", "", { class: "records" }); + const record = (result) => { + if ( + !result || + typeof result.document_id !== "string" || + result.provider !== item.provider || + result.connection_id !== item.connection_id || + result.dataset_id !== item.dataset_id + ) + throw new Error("Invalid resource response"); + records.set(result.document_id, result); + if (records.size > 50) records.delete(records.keys().next().value); + renderRecords(); + }; + const lookup = async (id) => { + if (busy) return; + const epoch = readEpoch; + const version = (readVersions.get(id) ?? 0) + 1; + readVersions.set(id, version); + const result = await request(`${prefix}/documents/${enc(id)}`); + if ( + active(g) && + epoch === readEpoch && + version === readVersions.get(id) && + !busy + ) + record(result); + }; + if (operations.has("status")) { + const form = make("form", "", { class: "lookup" }); + const id = make("input", "", { required: "", maxlength: "256" }); + const submit = make("button", w.lookup, { type: "submit" }); + form.append(field(w.document, id), submit); + taskSection.append(form); + listen(form, "submit", (event) => { + event.preventDefault(); + if (!id.value.trim()) return; + submit.disabled = true; + void run(() => lookup(id.value.trim())).finally(() => { + if (active(g)) submit.disabled = false; + }); + }); + } + function renderRecords() { + rows.replaceChildren(); + mutationControls.splice( + 0, + mutationControls.length, + ...mutationControls.filter((node) => content.contains(node)), + ); + for (const result of records.values()) { + const row = make("article"); + const stateKey = + result.state === "failed" ? "failedState" : result.state; + const state = [ + "unknown", + "queued", + "running", + "completed", + "failedState", + "cancelled", + ].includes(stateKey) + ? w[stateKey] + : w.unknown; + row.append( + make("h4", result.document_id), + make( + "p", + `${w.state}: ${state} · ${result.searchable === true ? w.searchable : w.notSearchable}`, + ), + ); + const raw = make("details"); + raw.append( + make("summary", w.providerState), + make("pre", String(result.provider_state)), + ); + row.append(raw); + const actions = make("div", "", { class: "actions" }); + if (operations.has("status")) + actions.append( + button(w.refresh, () => void run(() => lookup(result.document_id))), + ); + for (const action of ["reprocess", "cancel"]) { + if (!operations.has(action)) continue; + const confirm = make("span", "", { class: "confirmation" }); + confirm.hidden = true; + const trigger = button(w[action], () => { + confirm.hidden = false; + }); + const yes = button(w.confirm, () => { + confirm.hidden = true; + void write(action, () => + jsonWrite( + `${prefix}/documents/${enc(result.document_id)}/${action}`, + {}, + ), + ); + }); + mutationControls.push(trigger, yes); + confirm.append( + make( + "span", + action === "cancel" ? w.confirmCancel : w.confirmReprocess, + ), + yes, + button(w.back, () => { + confirm.hidden = true; + }), + ); + actions.append(trigger, confirm); + } + row.append(actions); + rows.append(row); + } + controls(); + } + taskSection.append(rows, make("p", w.snapshots, { class: "note" })); + content.append(section, taskSection, future); + controls(); + } + // One current-resource list; avoid accumulating handlers on repeated reloads. + listen(spaces, "change", () => { + if (!busy) void run(loadSpace); + }); + listen(bases, "change", () => { + if (!busy) void run(() => loadResource(latestResources)); + }); + if (!context.requestBackend) error.textContent = w.oldHost; + else void run(loadSpaces); + return { + dispose() { + disposed = true; + controller.abort(); + generation++; + records.clear(); + root.replaceChildren(); + }, + }; +} diff --git a/examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/static/copy.mjs b/examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/static/copy.mjs new file mode 100644 index 0000000000..e9a5085f86 --- /dev/null +++ b/examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/static/copy.mjs @@ -0,0 +1,161 @@ +export const copy = { + en: { + heading: "Your knowledge, ready to work", + intro: + "Choose a space and knowledge base to import documents or check processing. Access follows your account and the connected provider.", + space: "Space", + base: "Knowledge base", + reload: "Reload spaces", + loading: "Loading…", + noSpaces: + "No accessible spaces. Ask your administrator to configure space membership and resources.", + noBases: + "This space has no available knowledge bases. Ask your administrator to check its resource policy.", + oldHost: + "This host does not support extension HTTP requests. Update DeerFlow to use the knowledge center.", + browseOnly: + "You can browse this space. Document management requires separate administrator and provider permissions.", + denied: + "Access denied. Ask your administrator to check space, plugin and provider permissions.", + unavailable: + "Knowledge platform unavailable. Check that the platform extension is installed, enabled and configured.", + conflict: + "The account, policy or resource changed. Reload and reconcile before trying again.", + invalid: + "The provider does not support this request, or the input is invalid. Check the selected capability and fields.", + failed: + "The request could not be completed. Check the connection and try a fresh read.", + uncertain: + "The provider may have accepted this write. Check its document directory or query the document status before submitting again. This page never retries writes automatically.", + reconcile: "I checked the upstream result", + import: "Import document", + manual: "Write text", + url: "Import URL", + upload: "Upload file", + title: "Title", + content: "Document text", + address: "Document URL", + file: "File (up to 16 MiB)", + submit: "Submit document", + required: "Complete the required fields.", + badUrl: "Use an HTTP or HTTPS URL without credentials or a fragment.", + fileSize: "Select a nonempty file no larger than 16 MiB.", + options: "Processing options", + inherit: "Use inherited setting", + yes: "Enabled", + no: "Disabled", + summary: "Summary", + graph: "Graph extraction", + chunkSize: "Chunk size (64–8192)", + overlap: "Chunk overlap (0–4096)", + badChunks: + "Supply both chunk fields, with overlap smaller than size, or leave both empty.", + defaults: + "Empty options use the space defaults for new documents. Reprocessing preserves upstream settings.", + tasks: "Document processing", + taskIntro: + "Look up a document by its upstream ID. Results below belong to this visit and this knowledge base; they are not a complete document or task directory.", + document: "Document ID", + lookup: "Check status", + refresh: "Refresh status", + reprocess: "Reprocess", + cancel: "Cancel processing", + confirm: "Confirm", + back: "Keep unchanged", + confirmReprocess: + "Submit reprocessing for this document using its upstream settings?", + confirmCancel: "Request cancellation for this document?", + state: "Processing state", + searchable: "Searchable", + notSearchable: "Not searchable", + details: "Resource details", + providerState: "Provider state", + accepted: + "Submission returned. Check the document state below; acceptance does not mean processing completed.", + unknown: "Unknown", + queued: "Queued", + running: "Running", + completed: "Completed", + failedState: "Failed", + cancelled: "Cancelled", + snapshots: + "These are provider status snapshots. Searchable does not mean every summary, Wiki or graph step succeeded.", + future: "Knowledge entries and graph browsing", + futureText: + "Entry editing, version history and graph browsing are not available for this connection. You can still search knowledge through the existing chat tools.", + noImport: "No import operation is available for this resource.", + }, + zh: { + heading: "让知识随时可用", + intro: + "选择空间和知识库,导入文档或查询处理状态。访问范围由当前账号和上游服务共同校验。", + space: "知识空间", + base: "知识库", + reload: "重新加载空间", + loading: "正在加载…", + noSpaces: "暂无可访问的空间。请管理员配置空间成员和知识资源。", + noBases: "此空间暂无可用知识库。请管理员检查资源策略。", + oldHost: "当前宿主不支持扩展 HTTP 请求,请更新 DeerFlow 后使用知识中心。", + browseOnly: "你可以浏览此空间。文档管理还需要单独的管理员权限与上游授权。", + denied: "无权访问。请管理员检查空间、插件和上游服务的授权。", + unavailable: "知识平台不可用,请检查平台扩展是否已安装、启用并配置。", + conflict: "账号、策略或资源已发生变化,请重新加载并核对结果后再操作。", + invalid: "上游不支持此请求,或输入无效。请检查当前能力和输入内容。", + failed: "请求未能完成,请检查连接后重新查询。", + uncertain: + "上游可能已经接受本次写入。再次提交前,请核对上游文档目录或查询文档状态。页面不会自动重试写操作。", + reconcile: "我已核对上游结果", + import: "导入文档", + manual: "手工录入", + url: "导入网址", + upload: "上传文件", + title: "标题", + content: "文档内容", + address: "文档网址", + file: "文件(最大 16 MiB)", + submit: "提交文档", + required: "请填写必填内容。", + badUrl: "请使用 HTTP 或 HTTPS 网址,不包含账号密码或片段标识。", + fileSize: "请选择非空且不超过 16 MiB 的文件。", + options: "处理选项", + inherit: "使用继承配置", + yes: "开启", + no: "关闭", + summary: "摘要", + graph: "图谱抽取", + chunkSize: "分块大小(64–8192)", + overlap: "分块重叠(0–4096)", + badChunks: "请同时填写两个分块参数,重叠量小于分块大小;或同时留空。", + defaults: "新文档的空选项使用空间默认值。重新处理保留上游配置。", + tasks: "文档处理", + taskIntro: + "使用上游文档 ID 查询状态。下方仅展示本次访问中当前知识库的查询记录,不代表完整文档或任务目录。", + document: "文档 ID", + lookup: "查询状态", + refresh: "刷新状态", + reprocess: "重新处理", + cancel: "取消处理", + confirm: "确认操作", + back: "保持不变", + confirmReprocess: "确认按上游现有配置重新处理此文档?", + confirmCancel: "确认请求取消此文档的处理?", + state: "处理状态", + searchable: "可检索", + notSearchable: "暂不可检索", + details: "资源详情", + providerState: "上游状态", + accepted: "提交已返回,请以下方文档状态为准;请求被接受不代表处理完成。", + unknown: "未知", + queued: "排队中", + running: "处理中", + completed: "已完成", + failedState: "失败", + cancelled: "已取消", + snapshots: + "这里展示上游状态快照。可检索不代表摘要、Wiki 或图谱的每一步都已成功。", + future: "知识条目与图谱浏览", + futureText: + "此连接暂不支持条目编辑、版本记录和图谱浏览。你仍可通过已有聊天工具检索知识。", + noImport: "当前资源没有可用的导入操作。", + }, +}; diff --git a/examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/static/index.mjs b/examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/static/index.mjs new file mode 100644 index 0000000000..0ddd14fd6c --- /dev/null +++ b/examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/static/index.mjs @@ -0,0 +1,20 @@ +import { mountCenter } from "./center.mjs"; + +export default { + apiVersion: 1, + module: "knowledge-center.v1", + icon: "book-open", + surfaces: [ + { + id: "library", + slot: "page", + title: "Knowledge center", + navigation: { + label: "Knowledge center", + labelZh: "知识中心", + icon: "book-open", + }, + mount: mountCenter, + }, + ], +}; diff --git a/examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/static/styles.css b/examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/static/styles.css new file mode 100644 index 0000000000..d2e7aea62c --- /dev/null +++ b/examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/static/styles.css @@ -0,0 +1,211 @@ +:host { + color: var(--foreground); + font: 14px/1.6 var(--font-sans, ui-sans-serif, system-ui); +} +* { + box-sizing: border-box; +} +[hidden] { + display: none !important; +} +.center { + max-width: 960px; + margin: 0 auto; + padding: 24px 16px 64px; +} +h2 { + font-size: 24px; + line-height: 1.3; + letter-spacing: -0.012em; + margin: 0 0 12px; + font-weight: 600; + text-wrap: balance; +} +h3 { + font-size: 18px; + margin: 0 0 12px; + font-weight: 600; +} +h4 { + font-size: 14px; + margin: 0; + overflow-wrap: anywhere; +} +p { + margin: 8px 0 16px; + text-wrap: pretty; +} +.intro { + max-width: 640px; +} +.intro, +.note, +.future, +summary { + color: var(--muted-foreground); +} +.selectors { + display: grid; + grid-template-columns: 1fr 1fr auto; + gap: 16px; + align-items: end; + margin: 28px 0 16px; +} +label { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 16px; + min-width: 0; + font-weight: 500; +} +.selectors label { + margin: 0; +} +input, +select, +textarea, +button { + font: inherit; + color: inherit; + border: 1px solid var(--input, var(--border)); + border-radius: 8px; + background: var(--background); + min-width: 0; +} +input, +select, +textarea { + padding: 8px 12px; + width: 100%; +} +textarea { + resize: vertical; +} +button { + padding: 8px 16px; + cursor: pointer; + font-weight: 500; + min-height: 40px; + transition: + background-color 150ms, + transform 150ms; +} +button:hover { + background: var(--accent); +} +button:active { + transform: scale(0.97); +} +button:disabled { + opacity: 0.5; + cursor: default; +} +.primary { + background: var(--primary); + color: var(--primary-foreground); + border-color: var(--primary); +} +.primary:hover { + background: color-mix(in oklch, var(--primary) 85%, var(--background)); +} +:is(button, input, select, textarea, summary):focus-visible { + outline: 2px solid var(--ring); + outline-offset: 3px; +} +section { + margin-top: 28px; + padding-top: 24px; + border-top: 1px solid var(--border); +} +form { + max-width: 680px; +} +details { + margin: 12px 0 20px; +} +summary { + cursor: pointer; +} +details label { + margin-top: 12px; +} +pre { + white-space: pre-wrap; + overflow-wrap: anywhere; + font: + 13px/1.6 ui-monospace, + monospace; +} +.error { + color: var(--destructive); +} +.error:empty, +[role="status"]:empty { + display: none; +} +.warning { + padding: 16px; + border: 1px solid var(--border); + background: var(--secondary); + border-radius: 10px; + margin-bottom: 20px; +} +.lookup { + display: flex; + align-items: end; + gap: 12px; +} +.lookup label { + flex: 1; + margin-bottom: 0; +} +.lookup button { + flex-shrink: 0; +} +article { + padding: 20px 0; + border-bottom: 1px solid var(--border); +} +.actions, +.confirmation { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} +.confirmation { + flex-basis: 100%; + padding: 12px; + background: var(--secondary); + border-radius: 8px; +} +.confirmation span { + flex-basis: 100%; +} +@media (max-width: 640px) { + .center { + padding: 16px 4px 40px; + } + .selectors { + grid-template-columns: 1fr; + gap: 12px; + } + .selectors button { + justify-self: start; + } + .lookup { + flex-direction: column; + align-items: stretch; + } + input, + select, + button { + min-height: 44px; + } +} +@media (prefers-reduced-motion: reduce) { + button { + transition: none; + } +} diff --git a/examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/ui_manifest.json b/examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/ui_manifest.json new file mode 100644 index 0000000000..67029fd933 --- /dev/null +++ b/examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center/ui_manifest.json @@ -0,0 +1,10 @@ +{ + "schema_version": 1, + "entry": "static/index.mjs", + "files": [ + "static/index.mjs", + "static/center.mjs", + "static/copy.mjs", + "static/styles.css" + ] +} diff --git a/examples/deerflow-extension-knowledge-center/pyproject.toml b/examples/deerflow-extension-knowledge-center/pyproject.toml new file mode 100644 index 0000000000..25c2ecaae6 --- /dev/null +++ b/examples/deerflow-extension-knowledge-center/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "deerflow-extension-knowledge-center" +version = "0.1.0" +description = "Optional capability-aware knowledge center pages for DeerFlow" +requires-python = ">=3.12" +license = "MIT" +dependencies = ["deerflow-extension-api>=0.2.9,<0.3"] + +[project.entry-points."deerflow.extensions"] +knowledge-center = "deerflow_extension_knowledge_center:install" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["deerflow_extension_knowledge_center"] diff --git a/examples/deerflow-extension-knowledge-center/screenshots/dark.png b/examples/deerflow-extension-knowledge-center/screenshots/dark.png new file mode 100644 index 0000000000000000000000000000000000000000..ebd7bc5cdb4ab4cda8a899def27e21a45382fe13 GIT binary patch literal 48094 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 literal 0 HcmV?d00001 diff --git a/examples/deerflow-extension-knowledge-center/screenshots/zh-mobile.png b/examples/deerflow-extension-knowledge-center/screenshots/zh-mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..0542571e6f1088397b408b76cda80945ffc76f0a GIT binary patch literal 30150 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 + requestExtensionRoute( + path, + options, + abort.signal, + entry.viewer_id ?? "", + ), openConversation: (id, signal) => openConversation(id, (path) => router.push(path), signal), callBackend: bindFrontendServices( diff --git a/frontend/src/core/extensions/catalog.ts b/frontend/src/core/extensions/catalog.ts index 84c7125331..025d85c139 100644 --- a/frontend/src/core/extensions/catalog.ts +++ b/frontend/src/core/extensions/catalog.ts @@ -60,6 +60,11 @@ export const extensionCatalog = [ package: "deerflow-extension-knowledge-platform", namespace: "knowledge-platform", }, + { + key: "knowledgeCenter", + package: "deerflow-extension-knowledge-center", + namespace: "knowledge-center", + }, { key: "lightrag", package: "deerflow-extension-lightrag", namespace: null }, { key: "weknora", package: "deerflow-extension-weknora", namespace: null }, { key: "example", package: "deerflow-extension-example", namespace: null }, diff --git a/frontend/src/core/extensions/contracts.ts b/frontend/src/core/extensions/contracts.ts index 3da8a4f444..2059b40afc 100644 --- a/frontend/src/core/extensions/contracts.ts +++ b/frontend/src/core/extensions/contracts.ts @@ -7,6 +7,10 @@ export type ExtensionSettings = Readonly< Record >; export type SurfaceSlot = "page"; +export type ExtensionRequestOptions = Pick< + RequestInit, + "method" | "headers" | "body" | "signal" +>; export type SurfaceContext = { namespace: string; locale: string; @@ -14,6 +18,11 @@ export type SurfaceContext = { threadId?: string; signal: AbortSignal; callBackend: FrontendServices["callBackend"]; + /** Authenticated HTTP to contributed /api/extensions/ routes. Routes own authorization. */ + requestBackend?: ( + path: string, + options?: ExtensionRequestOptions, + ) => Promise; /** Host resolves current thread ownership before navigating; unavailable on older hosts. */ openConversation?: (threadId: string) => Promise; }; diff --git a/frontend/src/core/extensions/services.ts b/frontend/src/core/extensions/services.ts index ee303694e2..bca33e7fd7 100644 --- a/frontend/src/core/extensions/services.ts +++ b/frontend/src/core/extensions/services.ts @@ -9,8 +9,40 @@ import type { ConversationActionContext, FrontendContribution, FrontendServices, + ExtensionRequestOptions, } from "./contracts"; +/** Same Gateway and CSRF transport as host calls; never replay an HTTP mutation. */ +export async function requestExtensionRoute( + path: string, + options: ExtensionRequestOptions = {}, + signal: AbortSignal, + viewerId: string, +): Promise { + signal.throwIfAborted(); + if (!viewerId) throw new Error("Reload this extension to bind its viewer"); + // Decode once for validation only; forward the original encoded resource IDs. + const decoded = decodeURIComponent(path.split("?")[0]!); + if ( + !decoded.startsWith("/api/extensions/") || + /[\\\\#\u0000-\u0020]/.test(path) || + decoded.split("/").some((part) => part === "." || part === "..") || + decoded.includes("\\") || + decoded.includes("%") + ) + throw new Error("Expected a contributed extension route"); + const headers = new Headers(options.headers); + headers.set("X-Deerflow-Plugin-Viewer", viewerId); + const response = await fetch(`${getBackendBaseURL()}${path}`, { + ...options, + headers, + signal: options.signal ? AbortSignal.any([signal, options.signal]) : signal, + redirect: "error", + }); + signal.throwIfAborted(); + return response; +} + export type HostServices = Omit; /** Namespace comes from the installed page snapshot, never from action input. */ diff --git a/frontend/src/core/extensions/surfaces.ts b/frontend/src/core/extensions/surfaces.ts index 3696f914a3..efa96fef17 100644 --- a/frontend/src/core/extensions/surfaces.ts +++ b/frontend/src/core/extensions/surfaces.ts @@ -29,6 +29,19 @@ export function mountSurface( controller = surface.mount(root, { ...context, signal: abort.signal, + requestBackend: context.requestBackend + ? async (path, options) => { + abort.signal.throwIfAborted(); + const response = await context.requestBackend!(path, { + ...options, + signal: options?.signal + ? AbortSignal.any([abort.signal, options.signal]) + : abort.signal, + }); + abort.signal.throwIfAborted(); + return response; + } + : undefined, openConversation: context.openConversation ? async (threadId) => { abort.signal.throwIfAborted(); diff --git a/frontend/src/core/i18n/locales/en-US.ts b/frontend/src/core/i18n/locales/en-US.ts index 7f55424fe8..7563be7631 100644 --- a/frontend/src/core/i18n/locales/en-US.ts +++ b/frontend/src/core/i18n/locales/en-US.ts @@ -95,6 +95,11 @@ export const enUS: Translations = { description: "Manage hierarchical spaces, resource sharing and inherited policies with explicit administrator grants. Requires installation and configuration; no management page or global retrieval ACL is included.", }, + knowledgeCenter: { + title: "Knowledge center", + description: + "Browse authorized spaces, import files, URLs and text, and inspect document processing. Requires the separately configured knowledge platform and management extensions.", + }, knowledgeManagement: { title: "Knowledge ingestion and processing", description: diff --git a/frontend/src/core/i18n/locales/types.ts b/frontend/src/core/i18n/locales/types.ts index 517d05606e..bff434ae17 100644 --- a/frontend/src/core/i18n/locales/types.ts +++ b/frontend/src/core/i18n/locales/types.ts @@ -38,6 +38,7 @@ export interface Translations { | "knowledge" | "knowledgeManagement" | "knowledgePlatform" + | "knowledgeCenter" | "weknora" | "lightrag" | "example", diff --git a/frontend/src/core/i18n/locales/zh-CN.ts b/frontend/src/core/i18n/locales/zh-CN.ts index 65ebcd0424..b7f3495f26 100644 --- a/frontend/src/core/i18n/locales/zh-CN.ts +++ b/frontend/src/core/i18n/locales/zh-CN.ts @@ -89,6 +89,11 @@ export const zhCN: Translations = { description: "在管理员明确授权后管理多层级空间、资源共享与继承策略。需要安装和配置,不包含管理页面,也不改变全局检索权限。", }, + knowledgeCenter: { + title: "知识中心", + description: + "浏览授权空间,导入文件、网址和文本,并查询文档处理状态。需要单独配置知识平台与知识管理扩展。", + }, knowledgeManagement: { title: "知识入库与处理", description: diff --git a/frontend/tests/e2e/knowledge-center.spec.ts b/frontend/tests/e2e/knowledge-center.spec.ts new file mode 100644 index 0000000000..ce59f0ed58 --- /dev/null +++ b/frontend/tests/e2e/knowledge-center.spec.ts @@ -0,0 +1,484 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; + +import { expect, test, type Page, type Request } from "@playwright/test"; + +import { mockLangGraphAPI } from "./utils/mock-api"; + +const pageURL = "/workspace/extensions/knowledge-center/library"; +const api = "/api/extensions/knowledge-platform"; +const assetRoot = path.resolve( + process.cwd(), + "../examples/deerflow-extension-knowledge-center/deerflow_extension_knowledge_center", +); +const resource = { + resource_id: "resource-a", + provider: "weknora", + connection_id: "local", + native_base_id: "Team handbook", + dataset_id: "wk1_fixture", + operations: [ + "capabilities", + "manual", + "url", + "upload", + "status", + "reprocess", + "cancel", + ], + tools: [], + processing: {}, +}; +const result = { + version: 1, + provider: "weknora", + connection_id: "local", + dataset_id: "wk1_fixture", + document_id: "doc-1", + state: "unknown", + provider_state: "new-provider-state", + searchable: false, +}; + +async function setup( + page: Page, + options: { + locale?: string; + capabilitiesStatus?: number; + writeStatus?: number; + disabled?: boolean; + empty?: boolean; + } = {}, +) { + mockLangGraphAPI(page); + const calls: Request[] = []; + let operations = [...resource.operations]; + await page.route("**/api/plugins**", async (route) => { + const pathname = new URL(route.request().url()).pathname; + const entry = `/api/plugins/knowledge-center/assets/${"a".repeat(64)}/static/index.mjs`; + if (pathname.endsWith("/api/plugins")) + return route.fulfill({ + json: [ + { + namespace: "knowledge-center", + viewer_id: "default", + title: "Knowledge center", + description: "", + module: "knowledge-center.v1", + entry, + transport: "assets-v1", + settings: { enabled: !options.disabled }, + backend_actions: [], + }, + ], + }); + const filename = pathname.split("/").pop()!; + if ( + !["index.mjs", "center.mjs", "copy.mjs", "styles.css"].includes(filename) + ) + return route.fulfill({ status: 404 }); + return route.fulfill({ + body: await readFile(path.join(assetRoot, "static", filename)), + contentType: filename.endsWith("css") ? "text/css" : "text/javascript", + }); + }); + await page.route(`**${api}/**`, async (route) => { + const req = route.request(); + calls.push(req); + const pathname = new URL(req.url()).pathname; + if (pathname.endsWith("/spaces")) + return route.fulfill({ + json: { + revision: 1, + items: options.empty + ? [] + : [{ space_id: "team", title: "Research team", role: "manager" }], + }, + }); + if (pathname.endsWith("/spaces/team")) + return route.fulfill({ + json: { + revision: 1, + title: "Research team", + resources: [ + resource, + { + ...resource, + resource_id: "resource-b", + native_base_id: "Other handbook", + }, + ], + }, + }); + if (pathname.endsWith("/capabilities")) + return route.fulfill({ + status: options.capabilitiesStatus ?? 200, + json: { + version: 1, + operations, + processing_options: [ + "summary_enabled", + "chunk_size", + "chunk_overlap", + ], + pipeline: {}, + }, + }); + if (req.method() === "POST") + return route.fulfill({ + status: options.writeStatus ?? 202, + json: result, + }); + return route.fulfill({ + json: { + ...result, + state: "completed", + provider_state: "done", + searchable: true, + }, + }); + }); + await page.context().addCookies([ + { + name: "locale", + value: options.locale ?? "en-US", + url: test.info().project.use.baseURL!, + }, + { + name: "csrf_token", + value: "knowledge-test-token", + url: test.info().project.use.baseURL!, + }, + ]); + return { + calls, + revoke: () => { + operations = ["capabilities", "status"]; + }, + }; +} + +for (const zh of [false, true]) { + test(`knowledge center import, status and explicit processing (${zh ? "zh mobile" : "en desktop"})`, async ({ + page, + }) => { + if (zh) await page.setViewportSize({ width: 375, height: 812 }); + const { calls } = await setup(page, { locale: zh ? "zh-CN" : "en-US" }); + await page.goto(pageURL, { waitUntil: "domcontentloaded" }); + await expect( + page.getByRole("heading", { + name: zh ? "让知识随时可用" : "Your knowledge, ready to work", + }), + ).toBeVisible(); + await page + .getByLabel(zh ? "标题" : "Title", { exact: true }) + .fill("A source worth keeping"); + await page + .getByLabel(zh ? "文档内容" : "Document text", { exact: true }) + .fill("Synthetic knowledge for a browser contract test."); + await page + .getByRole("button", { name: zh ? "提交文档" : "Submit document" }) + .click(); + const row = page + .getByRole("article") + .filter({ has: page.getByRole("heading", { name: "doc-1" }) }); + await expect(row).toContainText( + zh ? "处理状态: 未知" : "Processing state: Unknown", + ); + expect(calls.filter((r) => r.method() === "POST")).toHaveLength(1); + expect( + calls.find((r) => r.method() === "POST")!.headers()["x-csrf-token"], + ).toBe("knowledge-test-token"); + await row + .getByRole("button", { name: zh ? "刷新状态" : "Refresh status" }) + .click(); + await expect(row).toContainText( + zh ? "处理状态: 已完成" : "Processing state: Completed", + ); + await row + .getByRole("button", { name: zh ? "重新处理" : "Reprocess", exact: true }) + .click(); + expect(calls.filter((r) => r.method() === "POST")).toHaveLength(1); + await row + .getByRole("button", { name: zh ? "确认操作" : "Confirm", exact: true }) + .click(); + await expect + .poll(() => calls.filter((r) => r.method() === "POST").length) + .toBe(2); + await expect( + page.getByRole("button", { name: zh ? "提交文档" : "Submit document" }), + ).toBeEnabled(); + await row + .getByRole("button", { + name: zh ? "取消处理" : "Cancel processing", + exact: true, + }) + .click(); + await row + .getByRole("button", { name: zh ? "确认操作" : "Confirm", exact: true }) + .click(); + await expect + .poll(() => calls.filter((r) => r.method() === "POST").length) + .toBe(3); + await expect( + page.getByText(zh ? /此连接暂不支持/ : /Entry editing, version history/), + ).toBeVisible(); + expect( + await page.evaluate( + () => document.documentElement.scrollWidth <= innerWidth, + ), + ).toBe(true); + if (process.env.EXTENSION_SCREENSHOT_DIR) { + await page + .getByRole("heading", { + name: zh ? "让知识随时可用" : "Your knowledge, ready to work", + }) + .scrollIntoViewIfNeeded(); + await page.screenshot({ + path: path.join( + process.env.EXTENSION_SCREENSHOT_DIR, + `knowledge-${zh ? "zh-mobile" : "en-desktop"}.png`, + ), + fullPage: true, + }); + } + }); +} + +test("URL and raw file submission preserve processing fields", async ({ + page, +}) => { + const { calls } = await setup(page); + await page.goto(pageURL, { waitUntil: "domcontentloaded" }); + await page.getByLabel("Import document", { exact: true }).selectOption("url"); + await page.getByLabel("Document URL").fill("https://example.com/document"); + await page.getByText("Processing options", { exact: true }).click(); + await page.getByLabel("Summary", { exact: true }).selectOption("false"); + await page.getByRole("button", { name: "Submit document" }).click(); + await expect + .poll(() => calls.filter((r) => r.method() === "POST").length) + .toBe(1); + expect( + calls.find((r) => r.method() === "POST")!.postDataJSON(), + ).toMatchObject({ kind: "url", processing: { summary_enabled: false } }); + await expect( + page.getByRole("button", { name: "Submit document" }), + ).toBeEnabled(); + await page + .getByLabel("Import document", { exact: true }) + .selectOption("upload"); + await page.getByLabel("File (up to 16 MiB)").setInputFiles({ + name: "notes.txt", + mimeType: "text/plain", + buffer: Buffer.from("synthetic upload"), + }); + await page.getByRole("button", { name: "Submit document" }).click(); + await expect + .poll(() => calls.filter((r) => r.method() === "POST").length) + .toBe(2); + const upload = calls.filter((r) => r.method() === "POST")[1]!; + expect(upload.url()).toContain("/files?filename=notes.txt"); + expect(upload.postData()).toBe("synthetic upload"); + expect(upload.headers()["x-processing-options"]).toBe( + '{"summary_enabled":false}', + ); +}); + +test("uncertain writes require reconciliation and never replay", async ({ + page, +}) => { + const { calls } = await setup(page, { writeStatus: 504 }); + await page.goto(pageURL, { waitUntil: "domcontentloaded" }); + await page.getByLabel("Title", { exact: true }).fill("Uncertain write"); + await page.getByLabel("Document text").fill("Synthetic text"); + await page.getByRole("button", { name: "Submit document" }).click(); + await expect(page.getByText(/The provider may have accepted/)).toBeVisible(); + await expect( + page.getByRole("button", { name: "Submit document" }), + ).toBeDisabled(); + expect(calls.filter((r) => r.method() === "POST")).toHaveLength(1); + await page + .getByRole("button", { name: "I checked the upstream result" }) + .click(); + await expect( + page.getByRole("button", { name: "Submit document" }), + ).toBeEnabled(); + expect(calls.filter((r) => r.method() === "POST")).toHaveLength(1); +}); + +test("invalid URL, chunk pair and oversized upload never submit", async ({ + page, +}) => { + const { calls } = await setup(page); + await page.goto(pageURL, { waitUntil: "domcontentloaded" }); + const mode = page.getByLabel("Import document", { exact: true }); + const submit = page.getByRole("button", { name: "Submit document" }); + await mode.selectOption("url"); + await page + .getByLabel("Document URL") + .fill("https://user:password@example.com/document"); + await submit.click(); + await expect( + page.getByRole("alert").filter({ hasText: /without credentials/ }), + ).toBeVisible(); + await page.getByLabel("Document URL").fill("https://example.com/document"); + await page.getByText("Processing options", { exact: true }).click(); + await page.getByLabel("Chunk size (64–8192)").fill("256"); + await submit.click(); + await expect( + page.getByRole("alert").filter({ hasText: /Supply both chunk fields/ }), + ).toBeVisible(); + await page.getByLabel("Chunk size (64–8192)").fill(""); + await mode.selectOption("upload"); + await page.getByLabel("File (up to 16 MiB)").setInputFiles({ + name: "large.txt", + mimeType: "text/plain", + buffer: Buffer.alloc(16 * 1024 * 1024 + 1), + }); + await submit.click(); + await expect( + page.getByRole("alert").filter({ hasText: /no larger than 16 MiB/ }), + ).toBeVisible(); + expect(calls.filter((r) => r.method() === "POST")).toHaveLength(0); +}); + +test("revoked capabilities stop submission before any write", async ({ + page, +}) => { + const fixture = await setup(page); + await page.goto(pageURL, { waitUntil: "domcontentloaded" }); + await page.getByLabel("Title", { exact: true }).fill("Revoked write"); + await page.getByLabel("Document text").fill("Synthetic text"); + fixture.revoke(); + await page.getByRole("button", { name: "Submit document" }).click(); + await expect( + page.getByRole("alert").filter({ hasText: /does not support/ }), + ).toBeVisible(); + expect(fixture.calls.filter((r) => r.method() === "POST")).toHaveLength(0); +}); + +test("browse-only members see no management controls", async ({ page }) => { + await setup(page, { capabilitiesStatus: 403 }); + await page.goto(pageURL, { waitUntil: "domcontentloaded" }); + await expect(page.getByText(/You can browse this space/)).toBeVisible(); + await expect( + page.getByRole("button", { name: "Submit document" }), + ).toHaveCount(0); + await expect( + page.getByLabel("Knowledge base", { exact: true }), + ).toBeEnabled(); +}); + +test("a late status read cannot overwrite a confirmed cancellation", async ({ + page, +}) => { + await setup(page); + await page.goto(pageURL, { waitUntil: "domcontentloaded" }); + await page.getByLabel("Document ID", { exact: true }).fill("doc-1"); + await page.getByRole("button", { name: "Check status", exact: true }).click(); + const row = page + .getByRole("article") + .filter({ has: page.getByRole("heading", { name: "doc-1" }) }); + await expect(row).toContainText("Completed"); + let release!: () => void; + const delayed = new Promise((resolve) => { + release = resolve; + }); + let readStarted = false; + await page.route(`**${api}/**/documents/doc-1`, async (route) => { + readStarted = true; + await delayed; + await route.fulfill({ json: { ...result, state: "running" } }); + }); + await page.route(`**${api}/**/documents/doc-1/cancel`, (route) => + route.fulfill({ status: 202, json: { ...result, state: "cancelled" } }), + ); + await row.getByRole("button", { name: "Refresh status" }).click(); + await expect.poll(() => readStarted).toBe(true); + await row + .getByRole("button", { name: "Cancel processing", exact: true }) + .click(); + await row.getByRole("button", { name: "Confirm", exact: true }).click(); + await expect(row).toContainText("Cancelled"); + const response = page.waitForResponse((r) => + r.url().endsWith("/documents/doc-1"), + ); + release(); + await (await response).finished(); + await page.evaluate( + () => + new Promise((resolve) => + requestAnimationFrame(() => { + requestAnimationFrame(() => resolve()); + }), + ), + ); + await expect(row).toContainText("Cancelled"); +}); + +test("dark theme follows host tokens and unavailable platform has a recovery control", async ({ + page, +}) => { + await page.addInitScript(() => localStorage.setItem("theme", "dark")); + await setup(page); + await page.goto(pageURL, { waitUntil: "domcontentloaded" }); + await expect(page.getByLabel("Title", { exact: true })).toBeVisible(); + if (process.env.EXTENSION_SCREENSHOT_DIR) + await page.screenshot({ + path: path.join( + process.env.EXTENSION_SCREENSHOT_DIR, + "knowledge-dark.png", + ), + fullPage: true, + }); + await page.route(`**${api}/spaces`, (route) => + route.fulfill({ status: 503, json: {} }), + ); + await page.getByRole("button", { name: "Reload spaces" }).click(); + await expect( + page + .getByRole("alert") + .filter({ hasText: /request could not be completed/ }), + ).toBeVisible(); + await expect( + page.getByRole("button", { name: "Reload spaces" }), + ).toBeEnabled(); +}); + +test("resource changes clear snapshots and do not accumulate listeners", async ({ + page, +}) => { + const { calls } = await setup(page); + await page.goto(pageURL, { waitUntil: "domcontentloaded" }); + await page.getByLabel("Document ID", { exact: true }).fill("doc-1"); + await page.getByRole("button", { name: "Check status", exact: true }).click(); + await expect(page.getByRole("heading", { name: "doc-1" })).toBeVisible(); + const selector = page.getByLabel("Knowledge base", { exact: true }); + await selector.selectOption("resource-b"); + await expect(page.getByRole("heading", { name: "doc-1" })).toHaveCount(0); + await expect(page.getByLabel("Document ID", { exact: true })).toBeVisible(); + const before = calls.filter((r) => r.url().endsWith("/capabilities")).length; + await page.getByRole("button", { name: "Reload spaces" }).click(); + await expect(page.getByLabel("Document ID", { exact: true })).toBeVisible(); + await selector.selectOption("resource-b"); + await expect(page.getByLabel("Document ID", { exact: true })).toBeVisible(); + expect( + calls.filter((r) => r.url().endsWith("/capabilities")).length - before, + ).toBe(2); +}); + +test("disabled page and empty membership have explicit states", async ({ + page, +}) => { + await setup(page, { empty: true }); + await page.goto(pageURL, { waitUntil: "domcontentloaded" }); + await expect(page.getByText(/No accessible spaces/)).toBeVisible(); + await setup(page, { disabled: true }); + await page.reload(); + await expect( + page.getByRole("heading", { name: "Extension page unavailable" }), + ).toBeVisible(); + await expect( + page.getByRole("link", { name: "Knowledge center", exact: true }), + ).toHaveCount(0); +}); diff --git a/frontend/tests/unit/core/extensions/services.test.ts b/frontend/tests/unit/core/extensions/services.test.ts index ae03b95760..a3babf111c 100644 --- a/frontend/tests/unit/core/extensions/services.test.ts +++ b/frontend/tests/unit/core/extensions/services.test.ts @@ -6,6 +6,7 @@ import { conversationText, latestVisibleAnswer, openConversation, + requestExtensionRoute, } from "@/core/extensions/services"; import type { AgentThread } from "@/core/threads/types"; @@ -24,6 +25,67 @@ beforeEach(() => { rs.clearAllMocks(); }); +test("extension HTTP transport uses host auth and abort without replaying writes", async () => { + const signal = new AbortController().signal; + const response = { ok: false, status: 409 }; + request.mockResolvedValue(response); + expect( + await requestExtensionRoute( + "/api/extensions/example/files?filename=a.txt", + { + method: "POST", + body: "data", + headers: { "x-deerflow-plugin-viewer": "forged" }, + }, + signal, + "alice", + ), + ).toBe(response); + expect(request).toHaveBeenCalledWith( + "/api/extensions/example/files?filename=a.txt", + expect.objectContaining({ + method: "POST", + body: "data", + signal, + redirect: "error", + }), + ); + expect(request).toHaveBeenCalledTimes(1); + expect( + request.mock.calls[0]![1].headers.get("X-Deerflow-Plugin-Viewer"), + ).toBe("alice"); +}); + +test("extension transport rejects foreign URLs, traversal and late calls", async () => { + const signal = new AbortController().signal; + for (const path of [ + "https://evil.test/api/extensions/a", + "//evil.test/api/extensions/a", + "/api/plugins", + "/api/extensions/../auth/me", + "/api/extensions/a/%2e%2e/%2e%2e/auth/me", + "/api/extensions/a\\b", + "/api/extensions/a#fragment", + ]) { + await expect( + requestExtensionRoute(path, {}, signal, "alice"), + ).rejects.toThrow(); + } + const aborted = AbortSignal.abort(); + await expect( + requestExtensionRoute( + "/api/extensions/example/spaces", + {}, + aborted, + "alice", + ), + ).rejects.toThrow(); + expect(request).not.toHaveBeenCalled(); + await expect( + requestExtensionRoute("/api/extensions/example/spaces", {}, signal, ""), + ).rejects.toThrow(); +}); + test("bookmark receives only the last visible assistant answer", async () => { const thread = { thread_id: "t",