extensions-internals
GitHub提供扩展系统底层实现知识,涵盖IExtensionLoader接口、IndexedDB存储结构、版本缓存策略、贡献点注册及.sandbox沙箱机制。
触发场景
安装
npx skills add lichtblick-suite/lichtblick --skill extensions-internals -g -y
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>;
}
⚠️
installExtensiontakes anInstallExtensionPropsobject (withfoxeFileData), 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", valueExtensionInfo - Object store
extensions— keyPath"info.id", valueStoredExtension
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.jsoncarries metadata only. Contributions (panels, converters, aliases, camera models) are registered at runtime inactivate(ctx)\u2014 see "Contribution Registration" below.
Contribution Registration (Dynamic)
⚠️ Contributions are not declared statically in
package.json. There is nolichtblick.panels/messageConverterscontributions key. Instead the extension's bundled source is executed and registers contributions at runtime via theactivate(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)—requireonly resolvesreactandreact-dom - No direct DOM access (panels render via a React component)
- Limited to the
ExtensionContextAPI 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:
- Local namespace (user-installed) — highest priority
- Org namespace (organization-managed)
- Built-in panels (always lowest priority)
Debugging Extensions
- Check browser DevTools console for extension load errors
- IndexedDB inspector shows cached extension data
- Extension catalog zustand devtools shows registration state
- Common issues: missing
mainfield, incorrect contribution format, version string format
版本历史
- cab9317 当前 2026-07-24 12:16


