Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,28 @@ jobs:
- run: npm run typecheck
- run: npm test

plugin-sdk:
runs-on: ubuntu-latest
defaults:
run:
working-directory: web/packages/plugin-sdk
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: pnpm/action-setup@v4
with:
version: 10.4.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: web/packages/plugin-sdk/pnpm-lock.yaml
- run: pnpm install --frozen-lockfile
- run: pnpm run typecheck
- run: pnpm test
- run: pnpm run build

gate:
runs-on: ubuntu-latest
steps:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Console is a Go backend-for-frontend with a React UI compiled into the same bina
- **OpenAI-compatible router.** Programs call `/v1` with API keys that administrators issue — with expiry and optional per-model scope. Keys are stored hashed; usage is exported as Prometheus metrics.
- **Gateway discovery.** The inference entrypoint, routes and gateway key are read from the cluster (Swiss's site profile or the route ConfigMap) and followed as they change — no URLs or keys copied into configuration.
- **One `helm install`.** The image and chart are published together to GHCR; point Console at Swiss and the models deployed there are in the Playground and `/v1`.
- **Optional frontend plugins.** Declare trusted frontend services and menus in configuration; load their pages through the ModelSphere Plugin SDK without rebuilding Console. See the [plugin guide](docs/development/plugins.md).
- **Upgrade path to Rise Global.** Identity is wire-compatible with Rise Global (same CRDs, same token claims), so upgrading is a configuration change, not a data migration.

## Quick start
Expand Down
35 changes: 31 additions & 4 deletions docs/console-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ unchanged. Moving either one moves both.

## Modules

The console is a shell plus compile-time modules. A module is a feature area
The console is a shell plus built-in and installed runtime modules. A module is a feature area
(iam, inferences, playground, router, docs, later container management); the shell owns everything
around it.

Expand Down Expand Up @@ -132,9 +132,11 @@ Rules:
- `validateModules` rejects duplicate ids/basePaths, shell-reserved paths and ids (`common`, `shell`: they are i18n namespaces) and pages declared twice at startup.
- Text goes through `useT()` (see "i18n"); the declaration's `title` and menu labels stay Chinese.

Deliberately not built: runtime loading, micro-frontends, cross-module slots. The
earlier Cordis design (branch `docs/console-plugin-design`) is parked; revisit it
when one of those is actually needed.
Runtime plugins use `@modelsphere/plugin-sdk`, maintained in `web/packages/plugin-sdk`. Configuration declares a trusted resource service and one menu; SDK registration supplies components and internal routes. `/plugins/index.json` returns descriptors, and `/plugins/<id>/*` proxies assets without Console credentials. Registration validates identity and collisions before adding a module. Empty configuration adds no external module.

Development uses the SDK Vite preset, same-origin WebSocket proxy and the host's React Refresh runtime. Deployment uses a built bundle served by a static-resource image. Configuration changes require a backend restart and browser refresh; compatible component edits update through Fast Refresh. This integration adds no menu/page permission policy.

See the [authoring and deployment guide](development/plugins.md) and [design](plans/2026-11-05-global-sdk-design.md).

### i18n

Expand Down Expand Up @@ -699,3 +701,28 @@ requests carry the same headers Global's apiserver sets.
| P7 | Container management modules, moved over from Rise Global. |

Each phase is independently committable and verifiable.

## Configured frontend navigation

The startup YAML tree selects built-ins and remote SDK plugins, their order, and nested menu folders.

| Concern | Decision |
| --- | --- |
| Default order | Embedded `internal/config/default-plugins.yaml` |
| Compiled registry | `web/src/modules/index.ts` maps IDs to available modules; configuration determines selection/order |
| Explicit tree | Built-in and remote leaves share sibling ordering; folder nodes contain `folder`, `title`, `children` |
| Folder state | Stable folder IDs identify expandable directories; navigation opens active ancestors, including detail routes |
| Permissions | Existing page checks run before grouping; empty folders are omitted |
| Discovery | `/plugins/index.json` returns the tree and same-origin remote URLs |
| Failure | Discovery errors do not enable the registry; remote failures preserve other valid configured modules |
| Compatibility | Unmarked legacy remote lists append to default built-ins; omitted/null uses defaults; [] explicitly disables modules |

```text
startup YAML -> validation/default resolution -> discovery tree
|
compiled registry + SDK loader -> ordered modules with folder ancestry
|
existing permissions -> visible sidebar tree -> expand/collapse
```

Configuration examples and lifecycle are in [the plugin guide](development/plugins.md#order-modules-and-group-menus-into-folders); the proposal is [configured navigation](plans/2026-11-06-configured-navigation-design.md).
148 changes: 148 additions & 0 deletions docs/development/plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Frontend plugins

Console loads trusted, separately maintained frontend plugins through `@modelsphere/plugin-sdk`.

## Prepare the SDK

SDK source lives in `web/packages/plugin-sdk`. The package is currently built from a pinned Console checkout; it has not been published to a registry.

```sh
export CONSOLE_SOURCE_DIR=/path/to/console
pnpm --dir "$CONSOLE_SOURCE_DIR/web/packages/plugin-sdk" install --frozen-lockfile
pnpm --dir "$CONSOLE_SOURCE_DIR/web/packages/plugin-sdk" run typecheck
pnpm --dir "$CONSOLE_SOURCE_DIR/web/packages/plugin-sdk" test
pnpm --dir "$CONSOLE_SOURCE_DIR/web/packages/plugin-sdk" run build
```

A consuming repository can prepare a generated local `file:` dependency from the built SDK. Keep generated package files out of Git; prepare them before dependency installation and record the Console revision with the build. Available exports are the runtime, `/build`, `/dev`, `/react-shim` and `/testing`.

## Register pages

```tsx
import { createPlugin } from "@modelsphere/plugin-sdk";
import { ExamplePage, ItemPage } from "./pages";
declare const __PLUGIN_VERSION__: string;
createPlugin({
name: "example",
version: __PLUGIN_VERSION__,
components: { ExamplePage },
routes: { "items/[id]": ItemPage },
});
```

The presets inject `__PLUGIN_VERSION__` from the frontend's `Chart.yaml`. Configuration selects the menu and root component. `[id]` routes become `:id`; `[...name]` becomes a catch-all. Use named imports for shared `react-router`, `@tanstack/react-query` and `@modelsphere/ui` modules. React and these imports use actual host instances. Authenticated `apiFetch` and i18n are exposed through `getBusiness<ConsoleServices>().console`. Optional auth metadata creates no new menu/page policy.

## Develop with Fast Refresh

Create `vite.config.mjs` in the plugin frontend:

```js
import { definePluginDevConfig } from "@modelsphere/plugin-sdk/dev";
export default definePluginDevConfig({
name: "example", entry: "src/runtime.tsx",
hostSource: process.env.CONSOLE_SOURCE_DIR,
hostOrigin: process.env.CONSOLE_ORIGIN,
});
```

Run one Console frontend and Go backend using the normal development instructions. Preserve existing cluster/auth/backend settings and add:

```yaml
plugins:
- id: example
url: http://127.0.0.1:4181
development: true
entry: __modelsphere_entry.ts
menu:
title: Example
label: Example
path: /example
component: ExamplePage
icon: puzzle
```

Restart the existing backend. From the plugin frontend directory:

```sh
export CONSOLE_SOURCE_DIR=/path/to/console
export CONSOLE_ORIGIN=http://127.0.0.1:5173
pnpm exec vite --config vite.config.mjs
```

Refresh Console once to discover the development entry. Compatible React component edits then update automatically and preserve local state. Entry/registration changes reload the page. New shared named imports regenerate their exports while Vite runs. The SDK compiles styles against the host theme and plugin source.

HTTP and WebSocket requests use `/plugins/<id>/` through both Console proxies. The plugin reuses the host Vite server's `/@react-refresh` runtime. This development mode requires the Console Vite frontend; a production frontend does not provide that runtime.

## Deploy a static-resource image

Production `tsup.config.mjs`:

```js
import { definePluginConfig } from "@modelsphere/plugin-sdk/build";
export default definePluginConfig({ name: "example", entry: "src/runtime.tsx", outDir: "dist" });
```

Run `pnpm exec tsup --config tsup.config.mjs`. It emits `index.umd.js` and Tailwind `candidates.json`. The image build must compile CSS using those candidates and the pinned Console theme. Current stylesheet integration supports one optional frontend; arbitrary multi-plugin runtime CSS merging is outside its scope.

Serve the built files with a static-resource image. Change the descriptor to `url: http://example-frontend:8080`, `entry: index.umd.js`, `styles: [index.css]` and omit `development`. Keep the menu declaration, restart the existing backend and refresh Console. Deployment loads the classic built bundle and rejects WebSocket upgrades.

## Verify and recover

| Check | Observable result |
| --- | --- |
| `GET /plugins/index.json` | Identity, same-origin URLs and menu; no upstream URL. |
| Entry and stylesheet requests | HTTP 200. |
| `Object.keys(window.__MODELSPHERE_SDK__.getPluginComponents("example"))` | Contains `ExamplePage`. |
| Menu/root and `items/<id>` | Render under host Router/Query/UI providers. |
| Compatible component edit | Text changes with unchanged document and local state. |
| Service unavailable | Visible plugin loading error; built-ins remain usable. Restore the service and refresh. |
| Set `plugins: []`, restart and refresh | Optional menu/routes disappear; backend business records remain. |

Configuration is read at backend startup. Open tabs retain their registry until refreshed. Plugin JavaScript runs with Console frontend privileges, so operators must select trusted services. The resource proxy strips Cookie/Authorization and upstream Set-Cookie; existing business API authentication/authorization still applies.

`web/test/plugin-runtime.html` is a development browser test fixture using configured plugins and actual host providers. It is excluded from the production entry and does not disable application authentication.

## Order modules and group menus into folders

Console loads the frontend tree from `plugins` at backend startup. Siblings appear in YAML order. Built-in IDs resolve against the compiled registry; remote entries register their pages through the SDK.

```yaml
plugins:
- folder: model-services
title: 模型与实验
children:
- id: inferences
source: builtin
- folder: experiments
title: 实验平台
children:
- id: canary
source: remote
url: http://canary-frontend:8080
entry: index.umd.js
styles: [index.css]
menu:
title: 北极星
label: 实验
path: /canary
component: Canary
icon: flask-conical
- id: playground
source: builtin
- id: docs
source: builtin
```

| Declaration | Behavior |
| --- | --- |
| Omitted `plugins` or `plugins: null` | Load `internal/config/default-plugins.yaml` with the default built-ins |
| `plugins: []` | Load no modules; shell/login and overview remain |
| Explicit `source: builtin` / `source: remote` entries, or folders | Load only the listed tree |
| Legacy remote entries without `source` | Keep default built-ins, append the configured remotes |
| `folder`, `title`, `children` | Nested expandable directory; no URL, route, or permission grant |

The available built-ins are `inferences`, `playground`, `router`, `iam` and `docs`. Built-ins keep their existing page routes, labels and permission checks. A folder disappears when none of its descendants are visible. Navigating directly to a detail page expands its ancestors; collapsing the entire sidebar leaves authorized descendant links accessible.

Duplicate plugin IDs/folder IDs, unknown built-ins, empty folders, and mixed folder/source declarations fail configuration validation. The browser checks the complete set of configured mount paths before executing remote entries. Discovery failure shows a loading error without enabling unconfigured built-ins. A failed remote preserves other valid configured modules.

After editing YAML, restart the existing Console backend and refresh the browser. The plugin image does not need rebuilding to change its position or folder.
5 changes: 5 additions & 0 deletions docs/installed-plugins-proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Configured frontend plugins

The current design is tracked in [issue #113](https://github.com/modelsphere/console/issues/113), with the [SDK architecture](plans/2026-11-05-global-sdk-design.md) and [development/deployment guide](development/plugins.md).

Menus come from Console configuration. Pages register through `@modelsphere/plugin-sdk`. Development uses Vite source modules and Fast Refresh; deployment uses a static-resource image.
Loading
Loading