Koee DocsKoee 文档

Native Kit Development

This is the canonical development guide for native Kits.

Koee has two kinds of Kits:

  • WebView Kits load a web app you host, inside the app's Kit container. Anyone can create one in the Developer Console and submit it for review. See WebView Kit Development.
  • Native Kits (this guide) are reviewed Dart modules compiled into the app binary, for official modules and source-review partners.

Native Kits are interactive product modules for workflows that need a real application surface rather than a chat transcript. A native Kit is not a bot, a chat, or a remotely downloaded mini program, and it does not load remote web content; if your product is a web app, build a WebView Kit instead.

Status

Native Kit development is a Private Developer Preview limited to official modules and explicitly approved source-review partners. There is no public enrollment or self-service submission for native Kits (the Developer Console's self-service flow is for WebView Kits), no published native Kit SDK, and no dynamic package installation.

Official Kits that appear in a released app are compiled into that binary and reviewed with the app release. The server cannot deliver or activate a Kit that the binary did not compile.

The current kit_api package is a repository-local contract at version 0.0.1. It may change together with official Kits in one reviewed app release. Do not treat the Preview API as a stable public compatibility promise.

When To Build A Kit

Use a Kit when the experience needs continuous, stateful interaction:

  • calendars, task boards, trackers, editors, or study sessions;
  • native forms, multi-step navigation, local interaction, and dense layouts;
  • app-owned capabilities such as controlled attachments or local persistence;
  • a feature users Add once and reopen from Chats.

Use the Bot API when the experience is primarily conversational, message-driven, or hosted by an external agent. A bot may later deep-link to an added Kit through an explicitly reviewed integration, but it must not automate the Kit UI or write Kit storage directly.

Core Concepts

ConceptMeaning
KitA user-visible interactive feature compiled into the app.
Kit moduleThe Dart package that implements one Kit's UI and client behavior.
Kit hostApp-owned code that controls registration, routing, identity, platform access, and capabilities.
Kit server moduleKit-owned server service and route adapters composed into the shared authenticated Worker.
DescriptorCompiled metadata declared by the module, including version and supported platforms.
CatalogServer-controlled metadata, audience, platform rollout, status, and kill switch.
AddPut a Kit entry in the current user's Chats list. It does not download code.
RemoveRemove the Chats entry while preserving local cache and server business data.
Clear dataA separate, explicit destructive action owned by the Kit.

Non-Negotiable Rules

  • Native Kit source is reviewed and compiled into a normal app release.
  • Never download or execute Dart, native code, templates, or unrestricted bridge definitions from the catalog.
  • A Kit must not import app/lib, another Kit, chat internals, global providers, raw platform plugins, or app-wide service locators.
  • A Kit never receives the session bearer token, unrestricted filesystem paths, picker objects, or another Kit's local data.
  • The app registry and server catalog must both permit a Kit before it opens.
  • Every Kit server request is authenticated and authorized again on the server.
  • Client-supplied user or owner identifiers are never authorization input.
  • Add, Remove, Clear local cache, and Clear server data are distinct operations.
  • A Kit is not represented as a fake chat or SpaceSummary.
  • Expected failures use typed results or stable error codes. Do not rely on parsing human-readable messages.

Repository Layout

Each Kit is a vertical package with its own UI, state, tests, assets, and server business module:

app/
  lib/features/kits/
    kit_host.dart
    kit_route_screen.dart
    kit_catalog_store.dart

packages/
  kit_api/

kits/
  example_kit/
    assets/
    lib/
      example_kit.dart
      src/
    server/
    test/
    pubspec.yaml

workers/server/
  src/kits/
    kit_catalog.ts
    kit_server_api.ts
    routes.ts
  migrations/

kits/<kit_id> is a local Dart package. app and the Kit both depend on packages/kit_api by path. Shared UI or utility packages are extracted only after more than one real Kit proves the shared boundary; do not create a broad foundation layer in advance.

Server code remains part of one deployment and one ordered migration history. A Kit owns its business service, but the shared Worker owns public routing, authentication, catalog policy, rate limits, idempotency primitives, and audit boundaries.

Create A Module

Package Setup

A minimal package is private and depends only on Flutter, kit_api, and its own reviewed pure-Dart dependencies:

name: example_kit
description: Example reviewed Kit module.
publish_to: none
version: 0.1.0

environment:
  sdk: ^3.12.2

dependencies:
  flutter:
    sdk: flutter
  kit_api:
    path: ../../packages/kit_api

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^6.0.0

Do not add camera, media picker, FFmpeg, notification, location, or other native plugins directly to a Kit. Request a narrow host capability when a real feature requires one.

Module Contract

Every compiled module implements KitModule:

abstract interface class KitModule {
  KitDescriptor get descriptor;

  Widget build(KitContext context);
}

Minimal implementation:

import 'package:flutter/material.dart';
import 'package:kit_api/kit_api.dart';

class ExampleKitModule implements KitModule {
  static const kitId = 'example-kit';

  @override
  KitDescriptor get descriptor => const KitDescriptor(
    id: kitId,
    compiledVersion: '1.0.0',
    displayName: 'Example',
    description: 'A short user-visible description.',
    supportedPlatforms: {KitPlatform.ios, KitPlatform.macos},
  );

  @override
  Widget build(KitContext context) => ExampleKitPage(context: context);
}

The host registers modules explicitly. There is no reflection, filesystem discovery, dynamic dependency resolution, or remote module loading.

Descriptor Contract

KitDescriptor is immutable compiled metadata:

FieldRequirement
idStable protocol and package identity, such as next-up. Never localized or reused.
compiledVersionExact module version expected by the server catalog for the current rollout.
displayNameShort fallback display name. Server-localized catalog content may override presentation text.
descriptionShort fallback description. Do not put secrets or policy in it.
supportedPlatformsPlatforms implemented and tested by this compiled package.
avatarBuilderOptional package-owned avatar builder using bundled, optimized assets.

Supported wire platform names are:

ios, android, macos, web, windows, linux

Platform support is declared twice:

  1. The package declares what its compiled implementation supports through KitDescriptor.supportedPlatforms.
  2. The server catalog declares where the current rollout is enabled.

Effective support is the intersection. Catalog policy may immediately narrow a rollout, but it can never enable a platform omitted by the compiled descriptor. A Kit must not appear in search, Plaza, Add, Open, or business APIs on an unsupported platform.

Identity And Handle

kit_id and handle have different purposes:

IdentifierPurpose
kit_idImmutable protocol/package identity used in registry, routes, storage namespaces, and migrations.
handlePublic search identity shown with @, stored without @.

Kit handles are lowercase, immutable, and globally unique across users, bots, groups, channels, and Kits. A disabled, retired, or removed Kit retains its handle so another identity cannot impersonate it. Display names may be localized; handles are never localized.

KitContext

The host builds a namespaced KitContext for one authenticated profile and one Kit:

class KitContext {
  final KitHttpClient http;
  final KitLocalStore localStore;
  final KitAttachmentCapability? attachments;
  final KitEmbeddedWebRuntimeCapability? embeddedWebRuntime;
  final Future<String> Function() timeZoneId;
  final VoidCallback onBack;
}

These are the capabilities available today. Planned capabilities are not part of the contract until they exist in kit_api and have host implementations, tests, and catalog policy.

Every Kit root app bar must expose onBack with a familiar back icon. The callback is owned by the host so wide macOS layouts keep the app navigation rail and return to Chats without a Kit creating its own product Navigator.

Authenticated HTTP

KitHttpClient sends requests only inside the current Kit's server namespace:

final response = await context.http.request(
  KitHttpMethod.post,
  'items',
  body: {
    'title': 'Prepare release notes',
    'operation_id': operationId,
  },
);

if (!response.ok) {
  final code = response.jsonMap['code'];
  // Map the stable code to a localized, actionable UI state.
}

The host adds authentication, the Kit id, and the current platform. A Kit must pass a relative path and must not construct the public API base URL, add an authorization header, or use a separate networking client to bypass the host.

KitHttpResponse.data is intentionally untyped at the transport boundary. Parse and validate it into Kit-owned DTOs before state reaches the UI.

Local Store

KitLocalStore is a small profile-and-Kit-isolated JSON string store:

await context.localStore.write('snapshot.v1', jsonEncode(snapshot));
final cached = await context.localStore.read('snapshot.v1');

Use it for recoverable cache, local preferences, and local-first snapshots. Do not use it as the only copy of durable server business data. Keys and values must not contain credentials. clear() affects only the current profile and current Kit namespace.

Bundled Embedded Runtime

embeddedWebRuntime is a platform-controlled capability for a very small set of reviewed official native Kits. It is not a browser, a remote Web Kit, or a general extension point. A module can request only a runtime id already compiled into the App; the host owns the WebView and may run only the exact signed-bundle asset manifest registered for that id.

The host verifies asset hashes before serving, denies remote network access, navigation, popups, downloads, permissions, cookies, and persistent Web storage, and exposes only a closed lifecycle/persistence bridge. The runtime receives no session token, generic HTTP client, user identifier, contacts, or chat data. Generated JavaScript and WebAssembly remain untrusted even when their build is reproducible, so containment is the security boundary.

This capability is unavailable to partner and third-party Kits without a separate platform security review. A Kit must render its typed unavailable state rather than constructing its own WebView or falling back to remote content.

Time Zone

Call timeZoneId() when calendar semantics need an IANA zone:

final zone = await context.timeZoneId();

Do not use a fixed UTC offset as calendar authority. The host may return UTC when a platform cannot provide a native IANA identifier, so server contracts must define a safe fallback.

Attachments

attachments is optional. Check both presence and availability before showing attachment controls:

final capability = context.attachments;
if (capability == null ||
    await capability.availability() != KitAttachmentAvailability.available) {
  // Hide the picker or render a typed unsupported state.
  return;
}

final picked = await capability.pickImages(limit: 4);
switch (picked) {
  case KitAttachmentSuccess<List<KitAttachmentSelection>>(
    value: final selections,
  ):
    // Queue the selected opaque handles for upload.
  case KitAttachmentError<List<KitAttachmentSelection>>(
    failure: final failure,
  ):
    // Render the appropriate cancelled, denied, or unavailable state.
}

Attachment selection ids are opaque and short-lived. A Kit never receives raw paths or picker objects. Upload and load return observable, cancellable operations:

final operation = capability.upload(selection);
final subscription = operation.progress.listen(updateProgress);
final result = await operation.result;
await subscription.cancel();
await capability.releaseSelection(selection.id);

Always release selections after success, cancellation, or failure. Handle every KitAttachmentFailure, including cancellation, permission denial, size limits, unsupported platforms, storage pressure, network failure, server rejection, and temporary unavailability.

Remote attachments must be represented by KitRemoteAttachment. Loading, sharing, and saving continue to pass through the host so authentication, local archive isolation, and platform behavior remain centralized.

Client State And UI

Routing

Each Kit opens at a stable host-owned route:

/kits/:kitId

The host performs catalog, audience, Add state, platform, registry, and exact compiled-version checks before calling module.build(context). A module must not create a second product router or mutate the app root navigator.

The Kit owns navigation inside its feature surface only where the host contract permits it. Product-level back, close, wide-layout presentation, and deep-link handling remain host responsibilities.

Chats Integration

An added Kit appears in the same Chats timeline as conversations. It uses the ordinary row, pin, selection, and remove interactions, with a Kit badge as its identity distinction. It remains a typed Kit item internally and is not placed in SpacesStore or converted to a fake SpaceSummary.

Opening updates last_opened_at; pinning is per user. Neither operation changes Kit business data.

Add, Remove, And Data Deletion

  • Add creates a per-user Chats entry and makes Kit business APIs available.
  • Remove deletes only that Chats entry. Re-adding restores existing server data and local cache.
  • Clear local cache clears only the current device profile's Kit cache.
  • Clear my data is an explicit, confirmed Kit-owned operation that removes the authenticated user's server business data and relevant local state.

Never label Add or Remove as install or uninstall. No executable package is installed at runtime.

UI Requirements

  • Support app light/dark theme behavior and text scaling.
  • Localize user-visible strings; never localize protocol ids or handles.
  • Keep fixed-format controls responsive without viewport-based font scaling.
  • Use app-consistent navigation, dialogs, loading, error, and destructive confirmation patterns.
  • Render explicit loading, empty, unavailable, conflict, retry, and offline states. Do not leave a blank feature surface.
  • Optimize bundled avatars and media for their rendered sizes. Use decode size hints for large raster assets.
  • Supply Kit avatars as square, edge-to-edge artwork. Do not bake rounded corners into the source image: the app host applies the canonical rounded square clip on Chats, search, Plaza, and other identity surfaces. Keep key artwork inside a conservative safe area so it remains legible at 48 px.
  • Test narrow and wide layouts for every declared desktop/mobile platform.

Server Module

Ownership Boundary

One Kit has one business service. UI routes and any future reviewed adapters must call that service rather than duplicate validation or write D1 directly.

Kit server modules may import only the narrow kit_server_api surface and approved pure utilities. They must not import another Kit or unrelated Worker business domains.

The shared Worker owns:

  • bearer authentication and server-derived actor identity;
  • catalog, audience, status, platform, and Add checks;
  • public route dispatch;
  • rate limits, operation replay primitives, and audit boundaries;
  • the single deployment and ordered D1 migration history.

The Kit owns:

  • business schemas and validation;
  • owner-scoped queries and state transitions;
  • conflict snapshots and stable business error codes;
  • Kit-specific R2 object semantics and deletion reconciliation;
  • service and route-adapter tests.

Route Convention

Kit business endpoints live below:

/kits/:kitId/...

The host authenticates the request and calls assertKitAccess before the Kit handler. Access requires an enabled native catalog row, admitted audience, supported platform, and an existing Add record. Restricted Kits should return the same not-found response for unknown and unauthorized callers so catalog existence is not leaked.

Do not accept owner_sub, actor role, catalog status, or capability grants from request JSON. Derive them from the authenticated request context and catalog.

Validation And Errors

Validate all request bodies and query parameters at the route boundary with a schema parser. Return a stable JSON envelope:

{
  "ok": false,
  "code": "ITEM_CONFLICT"
}

Use appropriate HTTP status codes. Examples:

StatusMeaning
400Invalid validated input or required confirmation missing.
401Missing or invalid authenticated session.
403Kit not added, platform unsupported, maintenance, or unavailable.
404Unknown/inaccessible Kit or owner-scoped business entity.
409Revision conflict or another deterministic state conflict.
413Kit-owned bounded storage or payload limit exceeded.
429Rate limited.
503Retryable service or finalization failure.

Never expose stack traces, storage keys, raw internal user ids belonging to other users, or internal exception messages.

Idempotency And Conflicts

Every retriable mutation carries an operation_id of at most 128 characters. The server checks operation replay before checking the current revision. This ordering is required when a mutation succeeded but its response was lost.

Updates to mutable records should use expected_revision and return a safe, owner-scoped current snapshot on conflict:

{
  "ok": false,
  "code": "ITEM_CONFLICT",
  "item": {
    "id": "item_opaque",
    "revision": 7
  }
}

The client retains the same operation id after transport failure or a retryable response. It removes the pending operation after a definitive rejection such as 400, 403, 404, or 409.

D1 And R2

Kit tables live in the shared D1 database but use Kit-specific names and owner-scoped indexes. Migrations join the repository's single forward-only sequence; a Kit does not create an independent migration stream.

R2 namespaces require an explicit lifecycle policy:

  • temporary staging objects use a short TTL and a reconciler;
  • ordinary messaging retention must not accidentally match Kit data prefixes;
  • retained Kit objects without TTL require a hard quota, explicit user deletion, retryable physical deletion, and orphan reconciliation;
  • an R2 key is never sufficient authorization to read an object.

R2 and D1 are not one transaction. Model upload, commit, logical deletion, and physical deletion as explicit retryable states rather than pretending they are atomic.

Catalog And Availability

The server catalog is declarative metadata and policy, not executable content. It includes fields such as:

{
  "kit_id": "example-kit",
  "handle": "example",
  "kind": "native",
  "display_name": "Example",
  "description": "A short catalog description.",
  "category": "Utilities",
  "compiled_version": "1.0.0",
  "supported_platforms": ["ios", "macos"],
  "status": "disabled"
}

Status meanings:

StatusBehavior
disabledHidden from discovery; Add, Open, and business APIs reject access.
enabledEligible users may discover, Add, open, and use the Kit.
maintenanceExisting entry renders unavailable; business mutations reject.
retiredNo new Adds; existing users can see retirement and Remove the entry.

A Kit is effectively available only when:

catalog status is enabled
AND catalog kind is native
AND audience admits the authenticated user
AND client build meets the catalog minimum for the current platform
AND app registry contains kit_id
AND compiled versions match exactly
AND current platform appears in both declarations

The minimum build is only a compatibility and presentation filter. Platform and build values are reported by the client and must never authorize access. The signed binary's compiled registry determines whether the client contains a Kit, while authenticated server routes independently enforce audience and business permissions.

Ship new native code disabled or with an internal audience first. Enable it only after a compatible binary is available and declared platforms pass their test gates. The catalog kill switch must be able to disable a Kit without an app release.

Security And Privacy Checklist

  • [ ] Module source is reviewed and compiled into the app.
  • [ ] Package does not import app/lib, another Kit, or raw native plugins.
  • [ ] Descriptor id, version, and supported platforms match catalog policy.
  • [ ] Server derives actor identity and checks assertKitAccess on every route.
  • [ ] Every business query is owner-scoped or has an explicit shared-data policy.
  • [ ] Request bodies and query parameters use schema validation.
  • [ ] Mutations are retry-safe with operation replay before revision checks.
  • [ ] Restricted audience membership and object keys are not leaked.
  • [ ] Logs, analytics, and crash reports exclude user business content and secrets.
  • [ ] Local state is isolated by app profile and Kit id.
  • [ ] R2 prefixes have explicit TTL or bounded-retention policy.
  • [ ] Clear-data behavior covers D1, R2, local cache, retries, and failure recovery.
  • [ ] Unsupported platforms cannot discover, Add, open, or call the Kit.
  • [ ] Any bundled runtime is platform-registered, hash-verified, offline-only, ephemeral, and covered by a dedicated security review.

Testing Requirements

Every Kit must provide automated coverage at the lowest appropriate layer.

Package Tests

  • descriptor identity, compiled version, and supported platform declarations;
  • controller/state transitions using fake KitHttpClient and KitLocalStore;
  • local-first cache, refresh, offline, retry, and conflict behavior;
  • typed capability denial, cancellation, and failure behavior;
  • widget tests for loading, empty, data, conflict, unavailable, and destructive confirmation states;
  • narrow and wide layout behavior for each declared platform.

Server Tests

  • unknown, disabled, unsupported, and unauthorized Kit access;
  • audience/allowlist enforcement without existence leakage;
  • actor identity cannot be forged in request JSON;
  • owner isolation for every read and mutation;
  • operation replay, lost-response retry, and revision conflict ordering;
  • migration, deletion, quota, upload finalization, and reconciliation behavior;
  • R2 object reads require live business authorization.

Host Integration Tests

  • registry and catalog version mismatch never opens a module;
  • package and catalog platform declarations are intersected;
  • search, Add, opening, pinning, Remove, and re-Add preserve expected data;
  • Kit list items do not enter SpacesStore or acquire fake unread/message state;
  • profile switching never exposes another profile's cache or attachments;
  • emergency disable blocks the route and server APIs.
  • bundled runtime tests deny external navigation/network, verify exact asset hashes, and release listeners, WebViews, audio, and orientation on teardown.

Manual Release Gate

Run the complete user flow on every declared platform: discovery or search, Add, cold launch, primary mutations, offline/retry, wide/narrow navigation, Remove, re-Add, Clear local cache, and Clear server data. A successful compile alone is not a platform support claim.

Review And Release Process

Official Kit development follows this sequence:

  1. Write or update the product and security RFC.
  2. Create the independent package, tests, server service, and migrations.
  3. Add an explicit host registry entry and disabled catalog record.
  4. Declare only platforms with complete implementation and validation.
  5. Pass package, server, host integration, migration, and platform build gates.
  6. Ship the binary while the Kit remains disabled or audience-restricted.
  7. Run real-device validation, then enable catalog visibility.
  8. Monitor errors and retain a server-side emergency disable path.

Approved partners will follow a private source-intake and review process. They will not upload precompiled binaries to user devices. KYC, contracts, dependency and license review, SBOM, secret scanning, forbidden API checks, capability review, signing, and app-store release remain platform-controlled.

The partner intake workflow and standalone integration test app do not exist yet. Contact the platform team before beginning an external Kit. Do not infer a submission API from this Preview guide.

Current Limitations

  • No public submission portal or package registry for native Kits.
  • No standalone developer integration app or scaffold command.
  • No dynamic Dart/native package download.
  • No generic Kit-to-bot action gateway.
  • No unrestricted app navigation, contacts, chat history, microphone, camera, transcoding, notification, payment, or arbitrary network capabilities.
  • WebView Kits use a separate container and security model; see WebView Kit Development.
  • Bundled Web runtimes are reviewed official exceptions, not a public Kit API.

These limitations are intentional. New capabilities are added only for a real, reviewed Kit through a narrow typed interface with availability, cancellation, failure, lifecycle, privacy, and fake-test semantics defined together.

原生 Kit 开发

这是原生 Kit 开发的权威指南。

Koee 提供两种 Kit:

  • WebView Kit 在 App 的 Kit 容器中加载你托管的网页应用。开发者可在控制台创建并提交审核,见 WebView Kit 开发。
  • 原生 Kit(本指南)是经审核、编译进 App 二进制的 Dart 模块,面向官方模块和接受源码审核的合作方。

原生 Kit 是交互式产品模块,适合需要实际应用界面而非聊天记录的流程。它不是 Bot、聊天或远程下载的小程序,也不加载远程网页内容;若产品是网页应用,请开发 WebView Kit。

状态

原生 Kit 开发处于私有开发者预览,仅限官方模块和明确获准接受源码审核的合作方。不提供公开报名、自助提交(控制台自助流程用于 WebView Kit)、公开原生 Kit SDK 或动态包安装。

已发布 App 中的官方 Kit 随该二进制编译并随 App 发布审核。服务端无法投递或激活二进制未编译的 Kit。

当前 kit_api 是仓库内本地约定,版本为 0.0.1,可以在一次经审核的 App 发布中与官方 Kit 一起变更。预览 API 不构成稳定的公开兼容性承诺。

何时开发 Kit

当体验需要持续、有状态的交互时,可使用 Kit:

  • 日历、任务看板、追踪工具、编辑器或学习会话;
  • 原生表单、多步骤导航、本地交互与密集布局;
  • App 提供的受控附件或本地存储能力;
  • 用户添加一次、之后从聊天列表反复打开的功能。

主要以对话、消息或外部 agent 为中心的体验使用 Bot API。Bot 可在明确审核的接入中通过深链打开已添加 Kit,但不得自动操作 Kit 界面或直接写入 Kit 存储。

核心概念

概念含义
Kit编译进 App、面向用户的交互功能。
Kit 模块实现某个 Kit 界面和客户端行为的 Dart 包。
Kit 宿主控制登记、路由、身份、平台访问和能力的 App 代码。
Kit 服务端模块Kit 自有业务服务及路由适配器,组合进共享的已认证 Worker。
描述符模块声明的编译元数据,包括版本和支持平台。
目录服务端控制的元数据、受众、平台放量、状态与紧急停用开关。
添加将 Kit 入口加入当前用户的聊天列表,不下载代码。
移除移除聊天入口,保留本地缓存与服务端业务数据。
清除数据Kit 负责的另一项需明确执行的破坏性操作。

必须遵守的规则

  • 原生 Kit 源码经过审核,并编译进正常 App 发布。
  • 不得从目录下载或执行 Dart、原生代码、模板或不受限的桥接定义。
  • Kit 不得导入 app/lib、其他 Kit、聊天内部实现、全局 provider、原始平台插件或 App 全局服务定位器。
  • Kit 不接收会话 bearer token、不受限的文件路径、选择器对象或其他 Kit 的本地数据。
  • 打开前,App 注册表与服务端目录均须允许该 Kit。
  • 每个 Kit 服务端请求都须重新认证和授权。
  • 客户端提交的用户或所有者标识不得作为授权依据。
  • 添加、移除、清除本地缓存、清除服务端数据是不同操作。
  • 不得将 Kit 表示为伪造聊天或 SpaceSummary。
  • 预期失败使用有类型的结果或稳定错误码,不解析人类可读消息来判断。

仓库结构

每个 Kit 是独立的纵向功能包,拥有自己的界面、状态、测试、资源和服务端业务模块:

app/
  lib/features/kits/
    kit_host.dart
    kit_route_screen.dart
    kit_catalog_store.dart

packages/
  kit_api/

kits/
  example_kit/
    assets/
    lib/
      example_kit.dart
      src/
    server/
    test/
    pubspec.yaml

workers/server/
  src/kits/
    kit_catalog.ts
    kit_server_api.ts
    routes.ts
  migrations/

kits/<kit_id> 是本地 Dart 包。app 与 Kit 都以 path 依赖 packages/kit_api。只有多个真实 Kit 证明共享边界后才提取共享 UI 或工具包,不预先构建宽泛的基础层。

服务端代码仍属于同一部署和一条有序迁移历史。Kit 负责自己的业务服务,共享 Worker 负责公开路由、认证、目录策略、限流、幂等基础机制及审计边界。

创建模块

包设置

最小包为私有包,仅依赖 Flutter、kit_api 及自身经过审核的纯 Dart 依赖:

name: example_kit
description: Example reviewed Kit module.
publish_to: none
version: 0.1.0

environment:
  sdk: ^3.12.2

dependencies:
  flutter:
    sdk: flutter
  kit_api:
    path: ../../packages/kit_api

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^6.0.0

不要直接向 Kit 添加相机、媒体选择、FFmpeg、通知、定位或其他原生插件。真实功能需要时,应申请范围明确的宿主能力。

模块接口

每个编译模块实现 KitModule:

abstract interface class KitModule {
  KitDescriptor get descriptor;

  Widget build(KitContext context);
}

最小实现:

import 'package:flutter/material.dart';
import 'package:kit_api/kit_api.dart';

class ExampleKitModule implements KitModule {
  static const kitId = 'example-kit';

  @override
  KitDescriptor get descriptor => const KitDescriptor(
    id: kitId,
    compiledVersion: '1.0.0',
    displayName: 'Example',
    description: 'A short user-visible description.',
    supportedPlatforms: {KitPlatform.ios, KitPlatform.macos},
  );

  @override
  Widget build(KitContext context) => ExampleKitPage(context: context);
}

宿主显式登记模块。不使用反射、文件系统发现、动态依赖解析或远程模块加载。

描述符约定

KitDescriptor 是不可变的编译元数据:

字段要求
id稳定的协议与包身份,例如 next-up,永不本地化或复用。
compiledVersion当前放量时服务端目录预期的精确模块版本。
displayName简短的默认显示名称;服务端本地化目录内容可覆盖展示文字。
description简短默认描述,不放秘密或策略。
supportedPlatforms该编译包已实现并测试的平台。
avatarBuilder可选的包内头像构建器,使用打包并优化的资源。

协议支持的平台名称:

ios, android, macos, web, windows, linux

平台支持声明两次:

  1. 包通过 KitDescriptor.supportedPlatforms 声明编译实现支持的平台。
  2. 服务端目录声明当前放量启用的平台。

实际支持范围为两者交集。目录可立即缩小放量范围,但不能开启编译描述符未声明的平台。不支持的平台不得在搜索、广场、添加、打开或业务 API 中访问该 Kit。

身份与 handle

kit_id 与 handle 用途不同:

标识符用途
kit_id不可变的协议/包身份,用于注册表、路由、存储命名空间和迁移。
handle公开搜索身份,显示时带 @,存储时不带。

Kit handle 为小写、不可变,并在用户、Bot、群组、频道和 Kit 之间全局唯一。停用、永久下架或移除的 Kit 保留 handle,以免其他身份冒充。显示名称可本地化,handle 不可本地化。

KitContext

宿主为一个已认证用户配置和一个 Kit 创建隔离命名空间的 KitContext:

class KitContext {
  final KitHttpClient http;
  final KitLocalStore localStore;
  final KitAttachmentCapability? attachments;
  final KitEmbeddedWebRuntimeCapability? embeddedWebRuntime;
  final Future<String> Function() timeZoneId;
  final VoidCallback onBack;
}

以上是当前可用能力。计划中的能力须先存在于 kit_api,并完成宿主实现、测试和目录策略,才能成为接口约定的一部分。

每个 Kit 根页面的 App bar 都须提供带常见返回图标的 onBack。回调由宿主负责,让 macOS 宽布局保留 App 导航栏并返回聊天,Kit 不自行创建产品级 Navigator。

已认证 HTTP

KitHttpClient 仅在当前 Kit 服务端命名空间内发送请求:

final response = await context.http.request(
  KitHttpMethod.post,
  'items',
  body: {
    'title': 'Prepare release notes',
    'operation_id': operationId,
  },
);

if (!response.ok) {
  final code = response.jsonMap['code'];
  // Map the stable code to a localized, actionable UI state.
}

宿主添加认证、Kit ID 和当前平台。Kit 必须传相对路径,不得自行构建公开 API 基础 URL、添加授权头,或使用另一个网络客户端绕过宿主。

KitHttpResponse.data 在传输边界刻意不限定类型。状态进入 UI 前,解析并验证为 Kit 自有 DTO。

本地存储

KitLocalStore 是按用户配置和 Kit 隔离的小型 JSON 字符串存储:

await context.localStore.write('snapshot.v1', jsonEncode(snapshot));
final cached = await context.localStore.read('snapshot.v1');

用于可恢复缓存、本地偏好和本地优先快照,不得作为持久服务端业务数据的唯一副本。键和值不能包含凭据。clear() 仅影响当前用户配置与当前 Kit 命名空间。

打包的嵌入式运行时

embeddedWebRuntime 是平台控制的能力,仅供少数经审核的官方原生 Kit 使用。它不是浏览器、远程 Web Kit 或通用扩展点。模块只能请求已编译进 App 的 runtime ID;宿主拥有 WebView,仅运行该 ID 登记的精确签名包资源清单。

宿主在提供资源前验证哈希,禁止远程网络、导航、弹窗、下载、权限、Cookie 和持久 Web 存储,仅暴露封闭的生命周期/持久化桥接。运行时不接收会话 token、通用 HTTP 客户端、用户标识、联系人或聊天数据。即使构建可复现,生成的 JavaScript 与 WebAssembly 仍不可信,隔离容器才是安全边界。

合作方及第三方 Kit 未经过单独平台安全审核时不可使用该能力。Kit 应显示有类型的不可用状态,不得自行构建 WebView 或回退到远程内容。

时区

日历语义需要 IANA 时区时,调用 timeZoneId():

final zone = await context.timeZoneId();

不得将固定 UTC offset 作为日历权威。平台无法提供原生 IANA 标识时,宿主可能返回 UTC;服务端约定必须定义安全回退。

附件

attachments 为可选能力。显示附件控件前,检查其是否存在且可用:

final capability = context.attachments;
if (capability == null ||
    await capability.availability() != KitAttachmentAvailability.available) {
  // Hide the picker or render a typed unsupported state.
  return;
}

final picked = await capability.pickImages(limit: 4);
switch (picked) {
  case KitAttachmentSuccess<List<KitAttachmentSelection>>(
    value: final selections,
  ):
    // Queue the selected opaque handles for upload.
  case KitAttachmentError<List<KitAttachmentSelection>>(
    failure: final failure,
  ):
    // Render the appropriate cancelled, denied, or unavailable state.
}

附件选择 ID 不透明且短期有效。Kit 不获取原始路径或选择器对象。上传和加载返回可观察、可取消的操作:

final operation = capability.upload(selection);
final subscription = operation.progress.listen(updateProgress);
final result = await operation.result;
await subscription.cancel();
await capability.releaseSelection(selection.id);

成功、取消或失败后都须释放选择结果。处理每种 KitAttachmentFailure,包括取消、权限拒绝、大小限制、不支持的平台、存储压力、网络失败、服务端拒绝和暂时不可用。

远程附件必须使用 KitRemoteAttachment 表示。加载、分享和保存继续经宿主执行,以集中维护认证、本地归档隔离及平台行为。

客户端状态与界面

路由

每个 Kit 在稳定、由宿主负责的路由打开:

/kits/:kitId

调用 module.build(context) 前,宿主检查目录、受众、添加状态、平台、注册表及精确编译版本。模块不得创建第二个产品路由器或修改 App 根导航器。

Kit 仅在宿主约定允许的范围内负责功能内部导航。产品级返回、关闭、宽布局展示和深链处理仍由宿主负责。

聊天列表集成

已添加 Kit 与对话出现在同一聊天时间线中,使用普通列表行、置顶、选择和移除交互,以 Kit 标记区分身份。内部仍是有类型的 Kit 条目,不进入 SpacesStore,也不转换为伪造的 SpaceSummary。

打开更新 last_opened_at;置顶按用户保存。两者均不修改 Kit 业务数据。

添加、移除与删除数据

  • 添加创建当前用户的聊天入口,并使 Kit 业务 API 可用。
  • 移除仅删除该入口;重新添加恢复已有服务端数据和本地缓存。
  • 清除本地缓存仅清除当前设备用户配置的 Kit 缓存。
  • 清除我的数据是 Kit 负责的明确、需确认操作,删除已认证用户的服务端业务数据及相关本地状态。

不得将添加或移除称为安装或卸载。运行时不会安装可执行包。

界面要求

  • 支持 App 浅色/深色主题及文字缩放。
  • 用户可见文字须本地化;协议 ID 和 handle 不本地化。
  • 固定格式控件保持响应式,不按视口缩放字体。
  • 使用与 App 一致的导航、对话框、加载、错误和破坏性操作确认模式。
  • 明确展示加载、空、不可用、冲突、重试与离线状态,不能留下空白界面。
  • 按显示尺寸优化打包头像和媒体,大型位图提供解码尺寸提示。
  • Kit 头像为铺满边缘的正方形图案。源图不要自带圆角;聊天、搜索、广场等身份界面由宿主应用标准圆角正方形裁剪。关键图案放在保守安全区内,确保 48 px 时清晰。
  • 对每个声明的桌面/移动平台测试窄布局和宽布局。

服务端模块

责任边界

一个 Kit 对应一个业务服务。界面路由及未来经过审核的适配器必须调用该服务,不重复校验或直接写 D1。

Kit 服务端模块仅可导入范围明确的 kit_server_api 和获准的纯工具函数,不得导入其他 Kit 或无关 Worker 业务域。

共享 Worker 负责:

  • bearer 认证及服务端推导的操作者身份;
  • 目录、受众、状态、平台与添加状态检查;
  • 公开路由分发;
  • 限流、操作重放基础机制及审计边界;
  • 单一部署和有序 D1 迁移历史。

Kit 负责:

  • 业务 schema 与校验;
  • 按所有者限定的查询和状态转换;
  • 冲突快照及稳定业务错误码;
  • Kit 专属 R2 对象语义及删除对账;
  • 服务和路由适配器测试。

路由约定

Kit 业务接口位于以下路径:

/kits/:kitId/...

宿主在 Kit handler 前认证请求并调用 assertKitAccess。访问要求启用的原生目录记录、获准受众、支持平台和已有添加记录。受限 Kit 对未知与未授权调用方应返回相同的不存在响应,以免泄露目录存在性。

不从请求 JSON 接受 owner_sub、操作者角色、目录状态或能力许可,应从已认证请求上下文和目录推导。

校验与错误

在路由边界用 schema parser 校验所有请求体和查询参数,返回稳定 JSON 结构:

{
  "ok": false,
  "code": "ITEM_CONFLICT"
}

使用合适的 HTTP 状态码,例如:

状态含义
400输入校验失败或缺少必需确认。
401缺少或无效的认证会话。
403Kit 未添加、平台不支持、维护中或不可用。
404Kit 未知/不可访问,或所有者范围内的业务实体不存在。
409revision 冲突或其他确定的状态冲突。
413超出 Kit 的受限存储或请求大小上限。
429被限流。
503可重试的服务或最终提交失败。

不得暴露堆栈、存储键、其他用户的原始内部 ID 或内部异常消息。

幂等与冲突

每次可重试修改携带最多 128 字符的 operation_id。服务端先检查操作重放,再检查当前 revision;当操作成功但响应丢失时,此顺序是必需的。

更新可变记录应使用 expected_revision,冲突时返回安全、限定所有者范围的当前快照:

{
  "ok": false,
  "code": "ITEM_CONFLICT",
  "item": {
    "id": "item_opaque",
    "revision": 7
  }
}

传输失败或可重试响应后,客户端保留同一个 operation ID。遇到 400、403、404 或 409 等明确拒绝后,移除待处理操作。

D1 与 R2

Kit 表位于共享 D1 数据库,但使用 Kit 专属表名及按所有者限定的索引。迁移加入仓库唯一的只向前迁移序列,Kit 不创建独立迁移流。

R2 命名空间需要明确生命周期策略:

  • 临时暂存对象使用较短 TTL 和对账清理器;
  • 普通消息保留规则不得误匹配 Kit 数据前缀;
  • 无 TTL 的持久 Kit 对象必须具有硬配额、用户显式删除、可重试物理删除和孤立对象对账;
  • R2 key 本身永远不足以授权读取对象。

R2 和 D1 不属于同一事务。上传、提交、逻辑删除与物理删除须建模为明确、可重试的状态,不能假装原子完成。

目录与可用性

服务端目录是声明式元数据和策略,不是可执行内容,包括以下字段:

{
  "kit_id": "example-kit",
  "handle": "example",
  "kind": "native",
  "display_name": "Example",
  "description": "A short catalog description.",
  "category": "Utilities",
  "compiled_version": "1.0.0",
  "supported_platforms": ["ios", "macos"],
  "status": "disabled"
}

状态含义:

状态行为
disabled不在发现入口展示;添加、打开和业务 API 拒绝访问。
enabled符合条件的用户可发现、添加、打开并使用。
maintenance已有入口显示不可用,业务修改被拒绝。
retired不允许新添加;已有用户可看到下架状态并移除入口。

Kit 只有在以下条件同时成立时才实际可用:

catalog status is enabled
AND catalog kind is native
AND audience admits the authenticated user
AND client build meets the catalog minimum for the current platform
AND app registry contains kit_id
AND compiled versions match exactly
AND current platform appears in both declarations

最低 build 仅用于兼容性和展示筛选。平台和 build 由客户端报告,绝不能作为访问授权依据。签名二进制的编译注册表决定客户端是否包含 Kit;已认证服务端路由独立执行受众和业务权限检查。

新原生代码先以停用或内部受众状态发布。兼容二进制可用且声明平台通过测试后才启用。目录紧急停用开关须能在不发布 App 的情况下停用 Kit。

安全与隐私检查清单

  • [ ] 模块源码经审核并编译进 App。
  • [ ] 包不导入 app/lib、其他 Kit 或原始原生插件。
  • [ ] 描述符 ID、版本和支持平台与目录策略一致。
  • [ ] 服务端推导操作者身份,每个路由都检查 assertKitAccess。
  • [ ] 每个业务查询限定所有者,或具有明确共享数据策略。
  • [ ] 请求体和查询参数经过 schema 校验。
  • [ ] 修改可安全重试,先检查操作重放再检查 revision。
  • [ ] 不泄露受限受众成员关系和对象键。
  • [ ] 日志、分析和崩溃报告不包含用户业务内容和秘密。
  • [ ] 本地状态按 App 用户配置和 Kit ID 隔离。
  • [ ] R2 前缀具有明确 TTL 或受限保留策略。
  • [ ] 清除数据涵盖 D1、R2、本地缓存、重试和失败恢复。
  • [ ] 不支持的平台无法发现、添加、打开或调用 Kit。
  • [ ] 打包运行时经平台登记、哈希验证、仅离线、使用临时状态,并经过专门安全审核。

测试要求

每个 Kit 应在合适的最低层提供自动化覆盖。

包测试

  • 描述符身份、编译版本与支持平台声明;
  • 使用 fake KitHttpClient 和 KitLocalStore 测试控制器/状态转换;
  • 本地优先缓存、刷新、离线、重试及冲突;
  • 有类型的能力拒绝、取消及失败;
  • 加载、空、数据、冲突、不可用及破坏性确认的 widget 测试;
  • 各声明平台的窄布局和宽布局。

服务端测试

  • 未知、停用、不支持平台及未授权 Kit 访问;
  • 执行受众/白名单规则,不泄露存在性;
  • 请求 JSON 无法伪造操作者身份;
  • 每次读取和修改的所有者隔离;
  • 操作重放、丢失响应后的重试及 revision 冲突检查顺序;
  • 迁移、删除、配额、上传最终提交与对账;
  • 读取 R2 对象需要当前有效的业务授权。

宿主集成测试

  • 注册表与目录版本不一致时不打开模块;
  • 包和目录的平台声明取交集;
  • 搜索、添加、打开、置顶、移除及重新添加按预期保留数据;
  • Kit 列表条目不进入 SpacesStore,不产生虚假未读/消息状态;
  • 切换用户配置不暴露其他配置的缓存或附件;
  • 紧急停用阻止路由与服务端 API;
  • 打包运行时禁止外部导航/网络、校验精确资源哈希,并在销毁时释放监听器、WebView、音频及屏幕方向。

手动发布验收

在每个声明平台运行完整用户流程:发现或搜索、添加、冷启动、主要修改、离线/重试、宽窄布局导航、移除、重新添加、清除本地缓存与清除服务端数据。编译成功不能单独证明平台支持。

审核与发布流程

官方 Kit 开发按以下流程:

  1. 编写或更新产品与安全 RFC。
  2. 创建独立包、测试、服务端服务和迁移。
  3. 添加显式宿主注册表条目与停用的目录记录。
  4. 仅声明完整实现并验证的平台。
  5. 通过包、服务端、宿主集成、迁移及平台构建检查。
  6. 在 Kit 停用或限制受众的状态下发布二进制。
  7. 完成真机验证,再启用目录可见性。
  8. 监测错误,并保留服务端紧急停用路径。

获准合作方使用私有源码接收与审核流程,不向用户设备上传预编译二进制。KYC、合同、依赖与许可审核、SBOM、秘密扫描、禁用 API 检查、能力审核、签名和应用商店发布均由平台控制。

合作方源码接收流程与独立集成测试 App 尚不存在。开展外部 Kit 前请联系平台团队,不得从本预览指南推断出提交 API。

当前限制

  • 原生 Kit 没有公开提交门户或包注册表。
  • 没有独立开发者集成 App 或 scaffold 命令。
  • 不动态下载 Dart/原生包。
  • 没有通用 Kit-to-bot 操作网关。
  • 不提供不受限的 App 导航、联系人、聊天历史、麦克风、相机、转码、通知、支付或任意网络能力。
  • WebView Kit 使用独立容器和安全模型,见 WebView Kit 开发。
  • 打包 Web 运行时是经审核的官方特例,不属于公开 Kit API。

这些限制是有意设计。只有真实、经审核的 Kit 需要时才新增能力,使用范围明确、有类型的接口,同时定义可用性、取消、失败、生命周期、隐私及 fake 测试语义。