Agent Skillslichtblick-suite/lichtblick › extensions-internals

extensions-internals

GitHub

提供扩展系统底层实现知识,涵盖IExtensionLoader接口、IndexedDB存储结构、版本缓存策略、贡献点注册及.sandbox沙箱机制。

.github/skills/extensions-internals/SKILL.md lichtblick-suite/lichtblick

Trigger Scenarios

查询扩展加载器接口定义与实现 了解IndexedDB扩展存储Schema 分析扩展安装与加载流程 确认命名空间隔离机制

Install

npx skills add lichtblick-suite/lichtblick --skill extensions-internals -g -y
More Options

Non-standard path

npx skills add https://github.com/lichtblick-suite/lichtblick/tree/develop/.github/skills/extensions-internals -g -y

Use without installing

npx skills use lichtblick-suite/lichtblick@extensions-internals

指定 Agent (Claude Code)

npx skills add lichtblick-suite/lichtblick --skill extensions-internals -a claude-code -g -y

安装 repo 全部 skill

npx skills add lichtblick-suite/lichtblick --all -g -y

预览 repo 内 skill

npx skills add lichtblick-suite/lichtblick --list

SKILL.md

Frontmatter
{
    "name": "extensions-internals",
    "description": "Deep extension system implementation knowledge: IExtensionLoader interface contracts, IndexedDB storage schema, version-compare cache strategy, contribution point registration, extension sandbox, and the .foxe packaging format."
}

Extensions Internals Skill

IExtensionLoader Interface

Defined in packages/suite-base/src/services/extension/IExtensionLoader.ts:

export type TypeExtensionLoader = "browser" | "server" | "filesystem";

export type LoadedExtension = {
  buffer?: Uint8Array;
  raw: string;
};

export type InstallExtensionProps = {
  foxeFileData: Uint8Array;
  file?: File;
  externalId?: string;
};

export interface IExtensionLoader {
  readonly namespace: Namespace;          // "org" | "local"
  readonly type: TypeExtensionLoader;     // "browser" | "server" | "filesystem"

  getExtension(id: string): Promise<ExtensionInfo | undefined>;
  getExtensions(): Promise<ExtensionInfo[]>;
  loadExtension(id: string): Promise<LoadedExtension>;
  installExtension(data: InstallExtensionProps): Promise<ExtensionInfo>;
  uninstallExtension(id: string): Promise<void>;
}

⚠️ installExtension takes an InstallExtensionProps object (with foxeFileData), not a URL string. The type union is "browser" | "server" | "filesystem" — there is no "indexeddb" or "remote" value.

Implementations:

Class File namespace type
IdbExtensionLoader services/extension/IdbExtensionLoader.ts constructor arg "browser"
RemoteExtensionLoader services/extension/RemoteExtensionLoader.ts constructor arg "server"
DesktopExtensionLoader suite-desktop/src/renderer/services/DesktopExtensionLoader.ts "local" "filesystem"

All loaders implement this interface — the catalog provider doesn't know the backing store.

IdbExtensionLoader + IdbExtensionStorage (IndexedDB)

IdbExtensionLoader delegates persistence to IdbExtensionStorage (packages/suite-base/src/services/extension/IdbExtensionStorage.ts).

Storage Schema

  • Database name: `${KEY_WORKSPACE_PREFIX}lichtblick-extensions-${namespace}` (one DB per namespace)
  • Database version: 1
  • Object store metadata — keyPath "id", value ExtensionInfo
  • Object store extensions — keyPath "info.id", value StoredExtension

Install Flow (installExtension)

1. Receive InstallExtensionProps { foxeFileData, file?, externalId? }
2. Decompress + extract package.json / dist entry from the .foxe
3. validatePackageInfo() builds ExtensionInfo
4. Persist ExtensionInfo to `metadata` + StoredExtension to `extensions`

Load Flow (loadExtension)

1. storage.get(id) → StoredExtension (throws "Extension not found" if missing)
2. decompressFile(content) → extractFoxeFileContent(ALLOWED_FILES.EXTENSION)
3. Return LoadedExtension { buffer?, raw }

Namespace Isolation

  • "org" extensions: managed by organization, auto-synced
  • "local" extensions: user-installed, never auto-removed
  • Each namespace gets its own IndexedDB database (suffix -{namespace})

DesktopExtensionLoader

packages/suite-desktop/src/renderer/services/DesktopExtensionLoader.ts \u2014 namespace = "local", type = "filesystem". Reads installed extensions from the desktop file system (via the preload bridge) rather than IndexedDB. Same IExtensionLoader contract.

RemoteExtensionLoader

packages/suite-base/src/services/extension/RemoteExtensionLoader.ts \u2014 type = "server" (the remote/org loader). The type value is "server", not "remote" \u2014 the only valid type values are "browser", "server", and "filesystem".

.foxe Package Format (Detail)

Structure

├── package.json         (required)
├── dist/
│   └── index.js         (required — bundled extension entry)
├── README.md            (optional)
└── CHANGELOG.md         (optional)

package.json Required Fields

{
  "name": "@publisher/extension-name",
  "version": "1.2.3",
  "displayName": "Human Readable Name",
  "description": "What this extension does",
  "publisher": "publisher-name",
  "main": "dist/index.js"
}

The package.json carries metadata only. Contributions (panels, converters, aliases, camera models) are registered at runtime in activate(ctx) \u2014 see "Contribution Registration" below.

Contribution Registration (Dynamic)

⚠️ Contributions are not declared statically in package.json. There is no lichtblick.panels / messageConverters contributions key. Instead the extension's bundled source is executed and registers contributions at runtime via the activate(ctx) callback.

buildContributionPoints.ts (packages/suite-base/src/providers/helpers/buildContributionPoints.ts) executes the extension source with new Function("module", "require", source), then calls module.exports.activate(ctx). The ExtensionContext (ctx) exposes:

const ctx: ExtensionContext = {
  mode,                          // "production" | "test" | "development"
  registerPanel(registration),   // panelId = `${qualifiedName}.${registration.name}`
  registerMessageConverter(args), // collects InstalledMessageConverter + panelSettings
  registerTopicAliases(aliasFn),  // note: registerTopicAliases (plural), not ...Function
  registerCameraModel({ name, modelBuilder }),
};

buildContributionPoints returns the accumulated { panels, messageConverters, topicAliasFunctions, panelSettings, cameraModels }.

ExtensionCatalogProvider (Zustand Store)

State Shape

interface ExtensionCatalogState {
  installedExtensions: ExtensionInfo[];
  installedPanels: Map<string, RegisteredPanel>;
  installedMessageConverters: MessageConverter[];
  installedTopicAliasFunctions: TopicAliasFunction[];
  installedCameraModels: CameraModel[];

  // Actions
  refreshExtensions: () => Promise<void>;
  installExtension: (loader: IExtensionLoader, url: string) => Promise<void>;
  uninstallExtension: (loader: IExtensionLoader, id: string) => Promise<void>;
}

Registration Flow

refreshExtensions() called
  → For each loader: getExtensions()
  → For each extension: loadExtension()
  → Execute extension code in sandbox
  → Extension calls activate(context)
  → context.registerPanel() / context.registerMessageConverter()
  → Zustand state updated with new contributions

Extension Sandbox

Extensions run in a restricted context built by buildContributionPoints.ts:

  • Source executed via new Function("module", "require", source)require only resolves react and react-dom
  • No direct DOM access (panels render via a React component)
  • Limited to the ExtensionContext API surface:
    interface ExtensionContext {
      mode: "production" | "test" | "development";
      registerPanel(registration: ExtensionPanelRegistration): void;
      registerMessageConverter<Src>(args: RegisterMessageConverterArgs<Src>): void;
      registerTopicAliases(aliasFunction: TopicAliasFunction): void;
      registerCameraModel(args: RegisterCameraModelArgs): void;
    }
    

Panel Registration

// Inside extension's activate():
export function activate(context: ExtensionContext) {
  context.registerPanel({
    name: "MyPanel",
    initPanel: (panelAPI: PanelExtensionContext) => {
      panelAPI.onRender = (renderState, done) => {
        // Render panel content
        done();
      };
      panelAPI.subscribe([{ topic: "/my_topic" }]);
    },
  });
}

Conflict Resolution

Priority order when same contribution exists in multiple sources:

  1. Local namespace (user-installed) — highest priority
  2. Org namespace (organization-managed)
  3. Built-in panels (always lowest priority)

Debugging Extensions

  1. Check browser DevTools console for extension load errors
  2. IndexedDB inspector shows cached extension data
  3. Extension catalog zustand devtools shows registration state
  4. Common issues: missing main field, incorrect contribution format, version string format

Version History

  • cab9317 Current 2026-07-24 12:16

Same Skill Collection

.github/skills/3d-rendering/SKILL.md
.github/skills/caching-internals/SKILL.md
.github/skills/deserialization/SKILL.md
.github/skills/e2e-playwright-mcp/SKILL.md
.github/skills/electron-internals/SKILL.md
.github/skills/layouts-internals/SKILL.md
.github/skills/mcap-format/SKILL.md
.github/skills/message-path/SKILL.md
.github/skills/message-pipeline/SKILL.md
.github/skills/panel-extension-api/SKILL.md
.github/skills/panel-image/SKILL.md
.github/skills/panel-log/SKILL.md
.github/skills/panel-map/SKILL.md
.github/skills/panel-raw-messages/SKILL.md
.github/skills/panel-state-transitions/SKILL.md
.github/skills/panel-user-scripts/SKILL.md
.github/skills/performance/SKILL.md
.github/skills/player-internals/SKILL.md
.github/skills/plot-internals/SKILL.md
.github/skills/remote-caching/SKILL.md
.github/skills/test-conventions/SKILL.md
.github/skills/theme/SKILL.md
.github/skills/unit-testing/SKILL.md
.github/skills/web-workers/SKILL.md
.github/skills/websocket-connection/SKILL.md

Metadata

Files
0
Version
cab9317
Hash
9cfff806
Indexed
2026-07-24 12:16

Accueil - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-08-08 15:56
浙ICP备14020137号-1 $Carte des visiteurs$