AgentSkillsCN

widget-data-architecture

在 StickerNest 中设计数据架构,并将小部件与数据源相连接。当用户询问有关数据绑定、数据架构、字段映射、外部桥接(Notion、Obsidian)、架构注册表,或小部件如何与数据对接时,请使用此技能。内容涵盖数据与视图分离模式、架构定义,以及数据源分层架构。

SKILL.md
--- frontmatter
name: widget-data-architecture
description: Designing data schemas and connecting widgets to data sources in StickerNest. Use when the user asks about data binding, schemas, field mapping, external bridges (Notion, Obsidian), the schema registry, or how widgets connect to data. Covers the data/view separation pattern, schema definitions, and data source tiers.

Widget Data Architecture

This skill guides you through StickerNest's data architecture, where widgets don't store data - they display data.

Core Principle: Data/View Separation

code
WRONG: Character Widget = Character Data + Character View (tightly coupled)
RIGHT: Data Schemas + View Widgets (loosely coupled)

DATA SCHEMAS              VIEW WIDGETS
(What is stored)          (How it's displayed)

├─ Characters             ├─ Wiki View
├─ Inventory              ├─ Card Grid View
├─ B-Roll                 ├─ Table View
├─ Locations              ├─ Kanban View
├─ (user-defined)         ├─ Timeline View
└─ (external/Notion)      └─ (community-created)

Connect any schema to any compatible view.

Data Source Tiers

Tier 1: Core Tables (Platform-Provided)

Built-in, optimized schemas for common use cases:

typescript
// Core schemas defined by platform
const CORE_SCHEMAS = [
  'core.asset',      // Files, images, videos
  'core.event',      // Calendar events
  'core.bookmark',   // Links, references
  'core.note',       // Simple text + metadata
  'core.inventory',  // Item collections
  'core.character',  // Character profiles
];

Tier 2: User Schemas (User-Defined)

Users define custom schemas via the schema registry:

typescript
// User creates a custom schema
const brollSchema = await createSchema({
  namespace: 'editor',           // or 'user_123' for private
  name: 'broll_library',
  fields: [
    { name: 'clip_name', type: 'text', required: true, indexed: true },
    { name: 'duration', type: 'number', required: true },
    { name: 'tags', type: 'text[]', indexed: true },
    { name: 'thumbnail', type: 'asset_ref' },
    { name: 'source', type: 'asset_ref', required: true },
    { name: 'notes', type: 'text' },
  ],
  visibility: 'private',  // 'private' | 'unlisted' | 'public'
});

Tier 3: External Bridges (Connected Services)

Connect to data in existing tools:

ServiceAuthData FlowUse Case
NotionOAuthRead/WriteDatabases, wikis
ObsidianLocal/PluginSyncNotes, knowledge graphs
AirtableOAuthRead/WriteStructured databases
Google SheetsOAuthRead/WriteSpreadsheets
SpotifyOAuthReadPlaylists, audio features
GitHubOAuthRead/WriteCode, issues

Tier 4: Hybrid (Cached External)

Sync external data locally for offline/performance:

typescript
// External data cached locally
const hybridConfig = {
  source: 'notion.database_abc123',
  cache: {
    strategy: 'sync',
    staleness: 'minutes',      // 'realtime' | 'minutes' | 'hours' | 'daily'
    conflictResolution: 'external_wins',
  },
};

Schema Definition

Field Types

typescript
type FieldType =
  | 'text'           // String
  | 'richtext'       // Formatted text, markdown
  | 'number'         // Integer or float
  | 'boolean'        // True/false
  | 'date'           // Date only
  | 'datetime'       // Date + time
  | 'enum'           // Single select from options
  | 'text[]'         // Array of strings (tags)
  | 'number[]'       // Array of numbers
  | 'asset_ref'      // Reference to uploaded asset
  | 'url'            // URL string
  | 'email'          // Email string
  | 'relationship'   // Reference to another schema record
  | 'relationship[]' // Array of references
  | 'json'           // Arbitrary JSON object

Schema Structure

typescript
interface SchemaDefinition {
  // Identity
  namespace: string;           // 'author', 'editor', 'user_123'
  name: string;                // 'story_characters'
  version: number;             // Schema version for migrations

  // Fields
  fields: FieldDefinition[];

  // Indexes for query performance
  indexes?: IndexDefinition[];

  // Relationships to other schemas
  relationships?: RelationshipDefinition[];

  // Access control
  visibility: 'private' | 'unlisted' | 'public';
  allowForks: boolean;
}

interface FieldDefinition {
  name: string;                // 'character_name'
  type: FieldType;
  required: boolean;
  indexed?: boolean;           // Create database index
  default?: any;               // Default value
  options?: string[];          // For enum types
  target?: string;             // For relationship types: 'author.settings'
  description?: string;        // For UI and AI context
}

Example: Author's Character Database

typescript
const characterSchema: SchemaDefinition = {
  namespace: 'author',
  name: 'story_characters',
  version: 1,

  fields: [
    { name: 'name', type: 'text', required: true, indexed: true },
    { name: 'role', type: 'enum', required: true,
      options: ['protagonist', 'antagonist', 'supporting', 'minor'] },
    { name: 'backstory', type: 'richtext', required: false },
    { name: 'traits', type: 'text[]', required: false },
    { name: 'age', type: 'number', required: false },
    { name: 'portrait', type: 'asset_ref', required: false },
    { name: 'notes', type: 'text', required: false },
  ],

  relationships: [
    {
      name: 'relationships',
      target: 'author.story_characters',  // Self-referential
      type: 'many_to_many',
      metadata: { name: 'relation_type', type: 'text' },  // "friend", "enemy"
    },
    {
      name: 'appears_in',
      target: 'author.scenes',
      type: 'many_to_many',
    },
  ],

  visibility: 'private',
  allowForks: false,
};

Widget Data Binding

View Widget Requirements

View widgets declare what data shape they can display:

typescript
// In widget manifest
dataBinding: {
  // Required fields - widget won't work without these
  requiredFields: [
    { role: 'title', types: ['text'], description: 'Display title' },
    { role: 'content', types: ['text', 'richtext', 'markdown'] },
  ],

  // Optional fields - widget can use if available
  optionalFields: [
    { role: 'image', types: ['asset_ref', 'url'] },
    { role: 'tags', types: ['text[]'] },
    { role: 'date', types: ['date', 'datetime'] },
    { role: 'author', types: ['text', 'relationship'] },
  ],

  // Features the widget supports
  features: ['search', 'filter', 'sort', 'edit_inline', 'pagination'],
}

Field Mapping

When connecting a widget to data, users map schema fields to widget roles:

typescript
// User binds "Wiki View" widget to "Story Characters" schema
const binding: DataBinding = {
  schemaId: 'author.story_characters',

  // Map schema fields to widget roles
  mappings: {
    title: { sourceField: 'name' },
    content: { sourceField: 'backstory' },
    image: { sourceField: 'portrait' },
    tags: { sourceField: 'traits' },
  },

  // Filter what records to show
  filter: { role: { $in: ['protagonist', 'antagonist'] } },

  // Sort order
  sort: { field: 'name', direction: 'asc' },
};

Connecting Widget to Data (User Flow)

code
1. User adds "Wiki View" widget to canvas
2. System prompts: "Select data source"
3. User sees:
   - My Schemas (author.story_characters, author.scenes...)
   - Connected (Notion: "Characters DB", Obsidian: "/Characters")
   - Community (shared schemas)
4. User selects "author.story_characters"
5. System shows field mapping UI:
   - Title (required) → [name ▼]
   - Content (required) → [backstory ▼]
   - Image (optional) → [portrait ▼]
   - Tags (optional) → [traits ▼]
6. User confirms, widget displays the data

External Bridges

Bridge Widget Pattern

Bridge widgets connect to external services and expose their data:

typescript
interface BridgeWidgetManifest extends WidgetManifest {
  type: 'bridge.notion' | 'bridge.obsidian' | 'bridge.airtable' | ...;

  bridge: {
    service: string;
    auth: 'oauth' | 'api_key' | 'local_file';
    dataFlow: 'read_only' | 'read_write' | 'sync';
    exposedSchema: SchemaDefinition | 'dynamic';
  };
}

Notion Bridge Example

typescript
const notionBridgeManifest: BridgeWidgetManifest = {
  type: 'bridge.notion',
  name: 'Notion Database Bridge',
  version: '1.0.0',

  bridge: {
    service: 'notion',
    auth: 'oauth',
    dataFlow: 'read_write',
    exposedSchema: 'dynamic',  // Inferred from Notion DB structure
  },

  // What this bridge exposes to other widgets
  exposes: {
    capabilities: [
      { name: 'query', description: 'Query the Notion database' },
      { name: 'create', description: 'Create new entries' },
      { name: 'update', description: 'Update existing entries' },
    ],
    events: [
      { name: 'data:changed', description: 'When Notion data changes' },
    ],
  },
};

Bridge Caching Strategy

typescript
interface BridgeCacheConfig {
  // How fresh must data be?
  staleness: 'realtime' | 'minutes' | 'hours' | 'daily';

  // Cache location
  cacheLayer: 'memory' | 'supabase' | 'both';

  // Sync strategy
  sync: {
    pullInterval?: number;      // ms, or null for manual
    pushStrategy: 'immediate' | 'batched' | 'manual';
    conflictResolution: 'external_wins' | 'local_wins' | 'manual';
  };
}

Unified Data API

All data sources (native, user, external) use the same API:

typescript
// Query any data source
const results = await dataAPI.query({
  source: 'author.story_characters',  // or 'notion.db_abc123'
  filter: { role: 'protagonist' },
  sort: { field: 'name', direction: 'asc' },
  limit: 20,
  offset: 0,
});

// Insert data
await dataAPI.insert({
  source: 'author.story_characters',
  data: { name: 'New Character', role: 'supporting' },
});

// Update data
await dataAPI.update({
  source: 'author.story_characters',
  id: 'record_123',
  data: { backstory: 'Updated backstory...' },
});

// Subscribe to changes
dataAPI.subscribe({
  source: 'author.story_characters',
  onChange: (changes) => { /* handle updates */ },
});

AI Access to Data

AI widgets access data through the same system, with permissions:

typescript
// AI widget permission request
{
  permissions: {
    databaseRead: [
      {
        source: 'author.story_characters',
        fields: ['name', 'role', 'backstory', 'traits'],
        filter: 'user_id = $current_user',
        justification: 'To help develop your characters',
      },
    ],
    databaseWrite: [
      {
        source: 'ai_assistant.memory',  // AI's own memory DB
        fields: '*',
        justification: 'To remember our conversation',
      },
    ],
  },
}

Schema Registry Implementation

Database Tables

sql
-- Schema definitions
CREATE TABLE widget_schema_registry (
  id UUID PRIMARY KEY,
  creator_id UUID NOT NULL,
  namespace TEXT NOT NULL,
  name TEXT NOT NULL,
  version INT DEFAULT 1,
  fields JSONB NOT NULL,
  indexes JSONB,
  relationships JSONB,
  visibility TEXT DEFAULT 'private',
  allow_forks BOOLEAN DEFAULT TRUE,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW(),
  UNIQUE(namespace, name)
);

-- User data storage (validated against schema)
CREATE TABLE widget_custom_data (
  id UUID PRIMARY KEY,
  user_id UUID NOT NULL,
  canvas_id UUID,
  schema_id UUID REFERENCES widget_schema_registry(id),
  schema_type TEXT NOT NULL,  -- Denormalized: 'author.story_characters'
  data JSONB NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Indexes
CREATE INDEX idx_custom_data_user ON widget_custom_data(user_id);
CREATE INDEX idx_custom_data_schema ON widget_custom_data(schema_type);
CREATE INDEX idx_custom_data_json ON widget_custom_data USING GIN (data);

Best Practices

For Schema Design

  1. Use meaningful field names - character_name not cn
  2. Add descriptions - Help AI and UI understand fields
  3. Index query fields - Fields used in filters/sorts
  4. Use relationships - Don't embed IDs in text fields
  5. Version schemas - For future migrations

For Widget Data Binding

  1. Declare all required fields - Don't fail silently
  2. Use standard roles - title, content, image, tags, date
  3. Support missing optional fields - Graceful degradation
  4. Emit data change events - Let other widgets react

For External Bridges

  1. Cache aggressively - External APIs are slow
  2. Handle offline - Graceful degradation when disconnected
  3. Respect rate limits - Queue and batch requests
  4. Show sync status - Users need to know if data is fresh

Reference Files

  • Vision document: docs/architecture/GAME_ENGINE_CANVAS_VISION.md
  • Schema types: src/types/schema.ts
  • Data API: src/runtime/DataAPI.ts
  • Bridge implementations: src/widgets/bridges/