mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-10-16 15:06:29 +08:00
* cubesandbox support
* feat(agent): add sandbox shell execution, file management tools, and Artifacts collection/download system
- Add three sandbox agent tools: shell_exec / sandbox_ls / sandbox_read
- shell_exec: safely execute shell commands in session sandbox with timeout & output truncation
- sandbox_ls: enumerate files under sandbox artifact output directory with recursive listing & pagination
- sandbox_read: read sandbox file content with offset-based chunked reading
- Add Artifacts system:
- ArtifactCollector: real-time collection of sandbox tool output files, attached to messages
- /api/artifacts/{session_id}/{message_id}/{filename}: download endpoint for frontend
- ChatArtifactsDrawer.vue: frontend artifacts sidebar UI
- DB migration 000066_message_artifacts: add artifacts column to messages table
- Attachment sandbox staging: upload user attachments to session sandbox on QA request for skill tool access
- Update Agent Prompts: add shell/sandbox tool usage constraints and safety rules
- Extend Agent Observe: include agent steps in Langfuse observability tracing
- Refactor Sandbox SessionManager: support session-scoped file listing, reading, and status queries
- Frontend: render artifact events in AgentStreamDisplay, i18n updates for 4 locales
- Add comprehensive tests: shell_exec_test, sandbox_read_test, artifact_collector_test, etc.
* feat(agent):add remote sandbox client abstract & client errors
* feat(sandbox): implement Redis binding store with distributed lifecycle lock;
* feat(sandbox): add remote session lifecycle with ownership context
Implement remoteSessionLifecycle to coordinate persistent remote sandboxes
across multi-instance deployments. Supports Resolve (lazy provision,
recovery, race-safe create) and Destroy with compare-delete semantics.
Add ownership context isolation to decouple remote resource cleanup from
request cancellation.
* feat(sandbox): add CubeRemoteClient adapter for RemoteSandboxClient
Implement CubeRemoteClient as a provider-neutral RemoteSandboxClient
implementation. Error normalization, timeout mapping, and metadata
support are fully integrated. SDK details remain private to the
adapter.
* refactor(sandbox): decouple SessionBoundManager from Cube
Remove internal/sandbox/cube.go (CubeSandbox, sync.Map reaper). Add
RemoteSandbox as provider-neutral Sandbox interface implementation.
Rewrite SessionBoundManager to depend on RemoteSandboxClient +
SessionSandboxBindingStore + SessionExistenceChecker, with all
lifecycle ops delegated to remoteSessionLifecycle (Redis + distributed
lock). Application-layer interfaces migrated to RemoteDirEntry/StatEntry
with time.Time ModTime. Cleanup delegated to provider idle-timeout.
* refactor(agent): enhance session capability handling in sandbox manager
- Introduced sessionFileStore and sessionShellExecutor functions to centralize capability checks for session-scoped file and shell execution.
- Updated agent service to register tools based on the presence of session filesystem capabilities, improving compatibility with various sandbox backends.
- Refactored tests to validate behavior when session capabilities are absent, ensuring robust handling of different sandbox types.
- Removed Cube-specific checks, promoting a more provider-neutral approach to session management.
* refactor(sandbox): add E2B backend, modernize Cube adapter, and enable multi-instance Redis binding
- Add e2b_remote_client.go (~850 lines) — full RemoteSandboxClient implementation
covering lifecycle, execution, and filesystem ops (ListDir/Stat/MakeDir/Remove)
- Wire E2B into NewManagerFromType and container/bootstrap path (buildE2BManager)
- Dependency: matiasinsaurralde/go-e2b v0.1.0
- Cut ~1500 lines of legacy SDK wrapper (cube_client.go, cube_envd.go,
cube_defaults.go) and replace with direct *cubesandbox.Client usage
- Parse CubeProxy URL directly in adapter (parseProxyURL), removing the
old hand-rolled envd transport layer
- Pass lifecycle extra fields (onTimeout, autoResume) via SDK CreateOptions.Extra
- Target Cube deployment v0.5.1
- Add RemoteNetworkPolicy to RemoteCreateRequest with AllowInternetAccess,
AllowPublicTraffic, AllowOut/DenyOut — shared by Cube and E2B adapters
- Both adapters default to permissive (internet+public on) for backwards compat
- Add selectSessionBindingStore() — Redis-backed or in-memory, controlled by
WEKNORA_SANDBOX_REDIS_NAMESPACE / WEKNORA_REDIS_NAMESPACE
- Add repositorySessionExistenceChecker — checks session existence via DB
(gorm.ErrRecordNotFound → not exists, transient errors → preserve binding)
- newSandboxManager() now accepts redisClient + sessionRepo; deprecated
hardcoded NewMemorySessionSandboxBindingStore() in production path
- buildSessionCreateRequest default timeout action: Kill → Pause+AutoResume
(Cube and E2B). Sessions survive brief idle periods; providers still
handle TTL-based reaping.
- buildSessionCreateRequest() and effectiveHTTPTimeout() parameterized
by RemoteProvider (was hardcoded to Cube Config fields)
- NewSessionBoundManager validates provider and delegates defaults to
per-provider apply*Defaults helpers
- Legacy SDK wrapper: cube_client.go, cube_envd.go, cube_defaults.go,
cube_envd_test.go, cube_net_diag_test.go, cube_test.go (~2500 lines)
- Unused helpers: DefaultManager.validateExecution, IsRemoteTerminal,
IsRemoteUnsupported, IsRemoteTransient, ShouldPreserveRemoteBinding
- Dead Config fields: CubeEnvdPort, CubeIdleTTL, AllowedCmds
- Dead ExecuteResult method: GetOutput
- Unused test: session_artifacts_integration_test.go
- Preloaded skill: sandbox-test-python
- Extract shared httpErrorKind() from per-adapter normalize functions
- Add SupportsFilesystemEnumeration capability flag — gates SessionFileStore
advertisement (E2B returns true post-reconnect, Cube always true)
- Fix stale comments: Cube-only → remote backends, remove "tomorrow"/"future"
- Bump session sandbox TTL defaults: 604800s → 300s (.env.example)
- Update docs/agent-skills.md: full multi-instance deployment guide
Total: 29 files, +1188 / -3721
* feat(agent): bind collected skill artifacts to owning message in resource registry
When ArtifactCollector persists a skill-generated file it now registers a
resource:// binding (owner_type=message, owner_id=assistantMessageID,
relation=artifact) in addition to the messages.artifacts JSONB record. This
mirrors how knowledge uploads and chat attachments bind their resources, so
artifacts can be enumerated and garbage-collected through the resource
registry instead of only via the message JSONB column.
- ArtifactCollector: inject ResourceCatalog, add messageID to Collect/
maybePersist, and add best-effort bindArtifactResource (binding failures are
logged, never drop the artifact; only resource:// refs are bound).
- agent_stream_handler: pass assistantMessageID into Collect.
- Renumber message_artifacts migration 000066 -> 000077 to resolve the
version collision with existing migrations (upstream now uses 000075/000076).
- Tests: update collector signatures and add coverage for the binding path
and for graceful degradation when Bind fails.
Note: binding applies to newly generated artifacts only; existing rows are
not backfilled.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sandbox): harden remote sandbox security and artifact collection
Disable Cube local fallback so skill scripts never run on the host when
the remote provider is unavailable. Require Redis for cube/e2b binding
unless WEKNORA_SANDBOX_ALLOW_MEMORY_BINDING=true for single-node dev.
Collect artifacts with WithoutCancel so user stop does not drop files,
reject shell_exec work_dir outside /workspace, and refuse session-scoped
Execute when a local fallback would be engaged.
* fix(ci): gofmt sandbox files and resolve artifact drawer type error
Apply gofmt to PR-touched Go files and narrow ChatArtifactsDrawer
props in AgentStreamDisplay so session-id is always a string.
* fix(sandbox): adapt to upstream main after rebase
Renumber the message_artifacts migration 000077 -> 000079: upstream now
uses 000077_remove_wiki_log and 000078_chunk_editing_and_custom_metadata.
Declare empty model-handle policies for shell_exec / list_sandbox_files /
read_sandbox_file. Upstream added a coverage test requiring every
UI-exposed tool to declare one; sandbox output is arbitrary skill-authored
content, so it gets the same treatment as execute_skill_script.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(sandbox): replace single tenant sandbox config column with a configs table to support multi-sandbox config per tenant.
* feat(sandbox): add sandbox config entity, cordon lease and session pin field
* feat(sandbox): add sandbox config repository with tenant scoping and cordon lease
* feat(sandbox): stamp sandbox config ID onto sandbox metadata
* feat(sandbox): scope sandbox listing, release and reaping to a single config
* feat(sandbox): resolve managers per config and refuse cordoned or missing configs
* feat(sandbox): implement session pinning and config identity change detection
- Add SessionSandboxPinner with read/pin/clear to bind session to config
- Refactor four entry points (execute/attachment/artifact/destroy) to use pinned config
- Add config_identity.go to detect identity changes by effective values (APIURL/APIKey/Domain/ProxyURL)
- Add SandboxIdentityChanged with key-masked comparison to prevent false 409 conflicts
- Fix artifact collection for default backend and destroy for legacy empty-pin sessions
* feat(sandbox): add multi-tenant sandbox config management and HTTP APIs
Service layer:
- Add sandbox config business service supporting CRUD and inventory listing
- For non-identity changes (e.g., template, TTL), save directly without locking or remote interactions
- For identity changes (e.g., API key, endpoint), reject save if there are still live sandboxes under this config on the remote side,
and return structured inventory details (sandbox count, affected sessions, related agents) for frontend prompting
- Before deleting a config, check remote live sandboxes; if occupied, reject and prompt user to end sessions or create a new config
- Identity comparison based on effective values to avoid normal saves being misidentified as credential rotation
HTTP interfaces and routing:
- Add REST endpoints for sandbox config (List/Create/Get/Update/Delete/Inventory)
- Mask sensitive credentials (API key, etc.) in all read responses
- Return 409 with structured inventory details on rejection scenarios
- sandbox-check probing supports specifying an existing config_id directly
- Remove old handler file, consolidate validation logic into service layer
- DI wiring and route registration for all six endpoints
* feat(sandbox): add sandbox_config_id field to allow agent referencing named sandbox config
- Add SandboxConfigID field to CustomAgentConfig with omitempty; empty value falls back to global defaults
- Validate config existence on create/update; return 400 if not found
- Inject sandboxConfigService into CustomAgentHandler
- Add unit tests covering field serialization and validation logic
* feat(sandbox, agent): Migrate sandbox config to multi-config CRUD, support named backend selection and workspace scripts policy
Migrate frontend API from legacy KV single-config to /sandbox-configs REST interface, supplement type definitions, detection methods and policy fields; refactor sandbox settings page to multi-config card grid, manage configs with cube/e2b tabs, add editor drawer for create/edit/delete, connection testing and occupancy hints, and add workspace scripts policy toggle; add sandbox config dropdown in Agent editor skill section, integrate config caching, and support selecting named config or falling back to deployment default.
* feat(sandbox): Unify sandbox disable semantics and add tenant-level policy master switch
- Backend: Unify interception in resolveTenantSandboxForConfig with priority: global disable > tenant disable > default fallback > named config; add deploymentSandboxDisabled() using env var to avoid mis‑impacting named E2B; update WorkspaceSandboxPolicy comment as master switch for entire workspace; add unit tests for various scenarios.
- Frontend: Sync i18n copy: button text changed to "Disable/Enable sandbox execution"; confirmation dialog clarifies impact scope; success/failure messages aligned.
- Semantic layering: WEKNORA_SANDBOX_MODE=disabled disables globally; tenant‑level "Disable sandbox execution" only disables for the current workspace.
* feat(sandbox): Add SSRF protection, Agent binding to named configs, and reference query capabilities
- Add outbound URL security guard to validate Cube/E2B API/Proxy addresses against SSRF (including cloud metadata), support allowing private endpoints via env var, link-local always blocked
- Add named backend type validation, only cube/e2b can be stored as workspace named config
- Add sandbox config ID field to Agent at runtime, resolve sandbox for attachments and execution based on Agent's selected named config instead of always global default
- Add query to list Agents referencing a sandbox config ID, useful for deletion/changes to show dependents
- Adapt artifact collector and other callers to fill missing resolve signature parameter
* fix(sandbox): fix session teardown and config-update, add HTTP pool sharing for Cube
Session teardown & deletion ordering
- Move sandbox destroy (and pin `Clear`) before soft‑delete operations (`DeleteSession`/`BatchDeleteSessions`/`DeleteAllSessions`) to prevent remote sandbox leaks and stale pins.
- Pass deployment‑wide `SessionBoundManager` as fallback into `resolveTenantSandboxForConfig` for sentinel pins (`"-"`) to support deployment‑default sessions.
## Config update edge cases
- When identity fields change but old credentials cannot build a client or list config sandboxes, log a warning and proceed with the update instead of returning `sandbox_inventory_unverifiable`; live sandboxes still return 409.
- UI: move `sandboxes_still_live` / `unverifiable` alerts to the top of the sandbox config drawer and auto‑scroll on save refusal and connectivity check results.
Artifact collection for Cube skills
- Pre‑create `/workspace/output` via envd with correct ownership/permissions for `DefaultSandboxExecUser` to ensure Cube skill artifacts are collected properly by `ArtifactCollector`.
HTTP pool sharing for named Cube configs
- Named sandbox configs rebuilt a client on every `Resolve`, causing fresh TCP connections to CubeAPI and the envd proxy on each request.
- Add `CubeTransportPool` and `cubeSplitTransport`:
- Control plane traffic reuses the same `SharedTransport` as E2B.
- Data plane traffic uses a guarded transport cached per proxy host:port, preserving the sandbox `Host` header.
- Wire the pool through the tenant resolver `buildClient`; keep SDK defaults for the deployment‑default manager and connectivity probes.
* feat(sandbox): sandbox execution DefaultUser change from root to user
* fix(multi-sandbox-config-per-tenant): align deployment config with tenant resolver and centralize defaults
- Merge form overrides onto DeploymentConfig() via WEKNORA_SANDBOX_* env vars
instead of DefaultConfig() hardcoded values, allowing empty fields to inherit
from .env rather than Go constants
- Centralize deployment baseline parsing in sandbox/defaults.go with
DeploymentConfig, CubeConfigFromEnv, E2BConfigFromEnv
- Delegate container startup to shared helpers to avoid drift
- Propagate tenant env_vars through ResolveEffectiveConfig into
buildSessionCreateRequest for both Cube and E2B session sandboxes
* fix(multi-sandbox-config-per-tenant): fix some bugs frontend.
* fix(sandbox): guard check-path dials, fix staging gate and shared-agent binding tenant
Three defects found while reviewing the remote sandbox work:
- The Cube connectivity probe and the deployment-default Cube/E2B managers
built clients without a guarded transport, so an admin-supplied api_url or
proxy_url could reach link-local addresses via DNS rebinding even though
save-time validation passed. All three now dial through the same outbound
guard the per-tenant resolver enforces.
- Attachment staging was gated on the process-wide manager, so a workspace
pointing an agent at a named Cube/E2B config while the deployment default
stayed docker/local would run its tools remotely against an empty
/workspace/input. The gate now resolves the manager the session actually
runs on.
- Session to sandbox bindings were keyed by the tenant in context, which a
shared agent deliberately swaps for the agent owner's so models, KBs and
named configs resolve in the sharing workspace. Session deletion only ever
knows the session's own tenant, so teardown looked up a key nobody wrote
and abandoned a paused MicroVM that keeps billing. The binding tenant is
now carried separately and survives CloneContext.
* chore(sandbox): renumber migrations above upstream and de-collide test stub
Upstream main took 000079 (knowledge_folder_path) and 000080
(knowledge_base_auto_tag_config) while this branch was open, so the three
sandbox migrations move to 000081-000083. golang-migrate requires unique,
ordered versions, and the RAISE NOTICE labels move with the file names.
Main also added a handler test declaring stubSessionService in the same
package; the artifact download test's configurable variant is renamed rather
than merged, since the two stubs answer GetSession differently.
---------
Co-authored-by: hailongzhao <hailongzhao@tencent.com>
Co-authored-by: wizardchen <wizardchen@tencent.com>
1123 lines
47 KiB
SQL
1123 lines
47 KiB
SQL
-- SQLite schema for WeKnora Lite (consolidated from all Postgres migrations)
|
|
|
|
CREATE TABLE IF NOT EXISTS tenants (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
retriever_engines TEXT NOT NULL DEFAULT '[]',
|
|
status VARCHAR(50) DEFAULT 'active',
|
|
business VARCHAR(255) NOT NULL,
|
|
storage_quota BIGINT NOT NULL DEFAULT 10737418240,
|
|
storage_used BIGINT NOT NULL DEFAULT 0,
|
|
agent_config TEXT DEFAULT NULL,
|
|
context_config TEXT,
|
|
conversation_config TEXT,
|
|
web_search_config TEXT DEFAULT NULL,
|
|
parser_engine_config TEXT DEFAULT NULL,
|
|
storage_engine_config TEXT DEFAULT NULL,
|
|
default_storage_backend_id VARCHAR(36),
|
|
credentials TEXT DEFAULT NULL,
|
|
chat_history_config TEXT,
|
|
retrieval_config TEXT,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_tenants_status ON tenants(status);
|
|
|
|
CREATE TABLE IF NOT EXISTS models (
|
|
id VARCHAR(64) PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
display_name VARCHAR(255) NOT NULL DEFAULT '',
|
|
type VARCHAR(50) NOT NULL,
|
|
source VARCHAR(50) NOT NULL,
|
|
description TEXT,
|
|
parameters TEXT NOT NULL,
|
|
is_default BOOLEAN NOT NULL DEFAULT 0,
|
|
is_builtin BOOLEAN NOT NULL DEFAULT 0,
|
|
managed_by VARCHAR(32) NOT NULL DEFAULT '',
|
|
status VARCHAR(50) NOT NULL DEFAULT 'active',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_models_type ON models(type);
|
|
CREATE INDEX IF NOT EXISTS idx_models_source ON models(source);
|
|
CREATE INDEX IF NOT EXISTS idx_models_is_builtin ON models(is_builtin);
|
|
CREATE INDEX IF NOT EXISTS idx_models_managed_by ON models(managed_by);
|
|
|
|
CREATE TABLE IF NOT EXISTS knowledge_bases (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
tenant_id INTEGER NOT NULL,
|
|
type VARCHAR(32) NOT NULL DEFAULT 'document',
|
|
chunking_config TEXT NOT NULL DEFAULT '{"chunk_size": 512, "chunk_overlap": 50, "split_markers": ["\n\n", "\n", "。"], "keep_separator": true}',
|
|
image_processing_config TEXT NOT NULL DEFAULT '{"enable_multimodal": false, "model_id": ""}',
|
|
embedding_model_id VARCHAR(64) NOT NULL,
|
|
summary_model_id VARCHAR(64) NOT NULL,
|
|
cos_config TEXT NOT NULL DEFAULT '{}',
|
|
storage_provider_config TEXT DEFAULT NULL,
|
|
vlm_config TEXT NOT NULL DEFAULT '{}',
|
|
extract_config TEXT NULL DEFAULT NULL,
|
|
faq_config TEXT,
|
|
question_generation_config TEXT NULL,
|
|
is_temporary BOOLEAN NOT NULL DEFAULT 0,
|
|
is_pinned INTEGER NOT NULL DEFAULT 0,
|
|
pinned_at DATETIME NULL,
|
|
asr_config TEXT,
|
|
vector_store_id VARCHAR(36),
|
|
storage_backend_id VARCHAR(36),
|
|
creator_id VARCHAR(36),
|
|
wiki_config TEXT,
|
|
indexing_strategy TEXT DEFAULT '{"vector_enabled":true,"keyword_enabled":true,"wiki_enabled":false,"graph_enabled":false}',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_knowledge_bases_tenant_id ON knowledge_bases(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_knowledge_bases_tenant_vector_store
|
|
ON knowledge_bases(tenant_id, vector_store_id);
|
|
CREATE INDEX IF NOT EXISTS idx_knowledge_bases_storage_backend
|
|
ON knowledge_bases(tenant_id, storage_backend_id);
|
|
CREATE INDEX IF NOT EXISTS idx_knowledge_bases_tenant_creator
|
|
ON knowledge_bases(tenant_id, creator_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS knowledges (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
knowledge_base_id VARCHAR(36) NOT NULL,
|
|
type VARCHAR(50) NOT NULL,
|
|
title VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
source VARCHAR(2048) NOT NULL,
|
|
parse_status VARCHAR(50) NOT NULL DEFAULT 'unprocessed',
|
|
enable_status VARCHAR(50) NOT NULL DEFAULT 'enabled',
|
|
embedding_model_id VARCHAR(64),
|
|
file_name VARCHAR(255),
|
|
file_type VARCHAR(50),
|
|
file_size BIGINT,
|
|
file_path TEXT,
|
|
file_hash VARCHAR(64),
|
|
storage_size BIGINT NOT NULL DEFAULT 0,
|
|
metadata TEXT,
|
|
custom_metadata TEXT NOT NULL DEFAULT '{}',
|
|
tag_id VARCHAR(36),
|
|
summary_status VARCHAR(32) DEFAULT 'none',
|
|
last_faq_import_result TEXT DEFAULT NULL,
|
|
channel VARCHAR(50) NOT NULL DEFAULT 'web',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
processed_at DATETIME,
|
|
error_message TEXT,
|
|
deleted_at DATETIME
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_knowledges_tenant_id ON knowledges(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_knowledges_base_id ON knowledges(knowledge_base_id);
|
|
CREATE INDEX IF NOT EXISTS idx_knowledges_parse_status ON knowledges(parse_status);
|
|
CREATE INDEX IF NOT EXISTS idx_knowledges_enable_status ON knowledges(enable_status);
|
|
CREATE INDEX IF NOT EXISTS idx_knowledges_tag ON knowledges(tag_id);
|
|
CREATE INDEX IF NOT EXISTS idx_knowledges_summary_status ON knowledges(summary_status);
|
|
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
title VARCHAR(255),
|
|
description TEXT,
|
|
knowledge_base_id VARCHAR(36),
|
|
max_rounds INTEGER NOT NULL DEFAULT 5,
|
|
enable_rewrite BOOLEAN NOT NULL DEFAULT 1,
|
|
fallback_strategy VARCHAR(255) NOT NULL DEFAULT 'fixed',
|
|
fallback_response TEXT NOT NULL DEFAULT '很抱歉,我暂时无法回答这个问题。',
|
|
keyword_threshold FLOAT NOT NULL DEFAULT 0.5,
|
|
vector_threshold FLOAT NOT NULL DEFAULT 0.5,
|
|
rerank_model_id VARCHAR(64),
|
|
embedding_top_k INTEGER NOT NULL DEFAULT 10,
|
|
rerank_top_k INTEGER NOT NULL DEFAULT 10,
|
|
rerank_threshold FLOAT NOT NULL DEFAULT 0.65,
|
|
summary_model_id VARCHAR(64),
|
|
summary_parameters TEXT NOT NULL DEFAULT '{}',
|
|
agent_config TEXT DEFAULT NULL,
|
|
context_config TEXT DEFAULT NULL,
|
|
agent_id VARCHAR(36),
|
|
user_id VARCHAR(512),
|
|
is_pinned BOOLEAN NOT NULL DEFAULT 0,
|
|
pinned_at DATETIME,
|
|
sandbox_config_id VARCHAR(36),
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_tenant_id ON sessions(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_agent_id ON sessions(agent_id);
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_tenant_user_pin
|
|
ON sessions (tenant_id, user_id, is_pinned, pinned_at, updated_at)
|
|
WHERE deleted_at IS NULL;
|
|
|
|
CREATE TABLE IF NOT EXISTS messages (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
request_id VARCHAR(36) NOT NULL,
|
|
session_id VARCHAR(36) NOT NULL,
|
|
role VARCHAR(50) NOT NULL,
|
|
content TEXT NOT NULL,
|
|
rendered_content TEXT NOT NULL DEFAULT '',
|
|
knowledge_references TEXT NOT NULL DEFAULT '[]',
|
|
agent_steps TEXT DEFAULT NULL,
|
|
mentioned_items TEXT DEFAULT '[]',
|
|
images TEXT DEFAULT '[]',
|
|
artifacts TEXT DEFAULT '[]',
|
|
is_completed BOOLEAN NOT NULL DEFAULT 0,
|
|
is_fallback BOOLEAN NOT NULL DEFAULT 0,
|
|
channel VARCHAR(50) NOT NULL DEFAULT '',
|
|
agent_id VARCHAR(36) NOT NULL DEFAULT '',
|
|
agent_tenant_id INTEGER NOT NULL DEFAULT 0,
|
|
model_id VARCHAR(64) NOT NULL DEFAULT '',
|
|
execution_context TEXT NOT NULL DEFAULT '{}',
|
|
agent_duration_ms INTEGER DEFAULT 0,
|
|
knowledge_id VARCHAR(36),
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id);
|
|
CREATE INDEX IF NOT EXISTS idx_messages_knowledge_id ON messages(knowledge_id);
|
|
CREATE INDEX IF NOT EXISTS idx_messages_agent_id ON messages(agent_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS message_suggestion_sets (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
session_id VARCHAR(36) NOT NULL,
|
|
assistant_message_id VARCHAR(36) NOT NULL,
|
|
agent_id VARCHAR(36) NOT NULL DEFAULT '',
|
|
agent_tenant_id INTEGER NOT NULL DEFAULT 0,
|
|
placement VARCHAR(32) NOT NULL,
|
|
config_hash VARCHAR(64) NOT NULL,
|
|
locale VARCHAR(16) NOT NULL DEFAULT '',
|
|
status VARCHAR(16) NOT NULL,
|
|
allow_regenerate BOOLEAN NOT NULL DEFAULT 0,
|
|
suppression_reason VARCHAR(64) NOT NULL DEFAULT '',
|
|
questions TEXT NOT NULL DEFAULT '[]',
|
|
model_id VARCHAR(64) NOT NULL DEFAULT '',
|
|
prompt_tokens INTEGER NOT NULL DEFAULT 0,
|
|
completion_tokens INTEGER NOT NULL DEFAULT 0,
|
|
latency_ms INTEGER NOT NULL DEFAULT 0,
|
|
error_code VARCHAR(64) NOT NULL DEFAULT '',
|
|
lease_until DATETIME,
|
|
generated_at DATETIME,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_message_suggestion_sets_cache_key
|
|
ON message_suggestion_sets(tenant_id, assistant_message_id, placement, config_hash, locale);
|
|
CREATE INDEX IF NOT EXISTS idx_message_suggestion_sets_session
|
|
ON message_suggestion_sets(tenant_id, session_id, created_at);
|
|
CREATE INDEX IF NOT EXISTS idx_message_suggestion_sets_status
|
|
ON message_suggestion_sets(status, lease_until);
|
|
|
|
CREATE TABLE IF NOT EXISTS message_suggestion_events (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
tenant_id INTEGER NOT NULL,
|
|
session_id VARCHAR(36) NOT NULL,
|
|
suggestion_set_id VARCHAR(36) NOT NULL,
|
|
question_id VARCHAR(64) NOT NULL DEFAULT '',
|
|
event_type VARCHAR(32) NOT NULL,
|
|
actor_id VARCHAR(512) NOT NULL DEFAULT '',
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (suggestion_set_id) REFERENCES message_suggestion_sets(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_message_suggestion_events_set
|
|
ON message_suggestion_events(suggestion_set_id, created_at);
|
|
CREATE INDEX IF NOT EXISTS idx_message_suggestion_events_session
|
|
ON message_suggestion_events(tenant_id, session_id, created_at);
|
|
CREATE INDEX IF NOT EXISTS idx_message_suggestion_events_type
|
|
ON message_suggestion_events(event_type, created_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS chunks (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
knowledge_base_id VARCHAR(36) NOT NULL,
|
|
knowledge_id VARCHAR(36) NOT NULL,
|
|
content TEXT NOT NULL,
|
|
source_content TEXT NOT NULL DEFAULT '',
|
|
content_revision INTEGER NOT NULL DEFAULT 0,
|
|
index_status VARCHAR(16) NOT NULL DEFAULT 'ready',
|
|
last_editor_id VARCHAR(64) NOT NULL DEFAULT '',
|
|
context_header TEXT NOT NULL DEFAULT '',
|
|
chunk_index INTEGER NOT NULL,
|
|
is_enabled BOOLEAN NOT NULL DEFAULT 1,
|
|
start_at INTEGER NOT NULL,
|
|
end_at INTEGER NOT NULL,
|
|
pre_chunk_id VARCHAR(36),
|
|
next_chunk_id VARCHAR(36),
|
|
chunk_type VARCHAR(20) NOT NULL DEFAULT 'text',
|
|
parent_chunk_id VARCHAR(36),
|
|
image_info TEXT,
|
|
video_info TEXT,
|
|
relation_chunks TEXT,
|
|
indirect_relation_chunks TEXT,
|
|
metadata TEXT,
|
|
tag_id VARCHAR(36),
|
|
status INTEGER NOT NULL DEFAULT 0,
|
|
content_hash VARCHAR(64),
|
|
flags INTEGER NOT NULL DEFAULT 1,
|
|
seq_id INTEGER,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_chunks_tenant_kg ON chunks(tenant_id, knowledge_id);
|
|
CREATE INDEX IF NOT EXISTS idx_chunks_parent_id ON chunks(parent_chunk_id);
|
|
CREATE INDEX IF NOT EXISTS idx_chunks_chunk_type ON chunks(chunk_type);
|
|
CREATE INDEX IF NOT EXISTS idx_chunks_tag ON chunks(tag_id);
|
|
CREATE INDEX IF NOT EXISTS idx_chunks_content_hash ON chunks(content_hash);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_chunks_seq_id ON chunks(seq_id);
|
|
CREATE INDEX IF NOT EXISTS idx_chunks_kb_tenant ON chunks(knowledge_base_id, tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_chunks_knowledge_enabled ON chunks(knowledge_id, is_enabled, deleted_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS chunk_revisions (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
knowledge_base_id VARCHAR(36) NOT NULL,
|
|
knowledge_id VARCHAR(36) NOT NULL,
|
|
chunk_id VARCHAR(36) NOT NULL,
|
|
revision INTEGER NOT NULL,
|
|
content TEXT NOT NULL DEFAULT '',
|
|
is_enabled BOOLEAN NOT NULL DEFAULT 1,
|
|
editor_id VARCHAR(64) NOT NULL DEFAULT '',
|
|
edit_source VARCHAR(16) NOT NULL DEFAULT 'user',
|
|
edited_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(chunk_id, revision)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_chunk_revisions_tenant_chunk ON chunk_revisions(tenant_id, chunk_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
username VARCHAR(100) NOT NULL UNIQUE,
|
|
email VARCHAR(255) NOT NULL UNIQUE,
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
avatar VARCHAR(500),
|
|
tenant_id INTEGER,
|
|
is_active BOOLEAN NOT NULL DEFAULT 1,
|
|
can_access_all_tenants BOOLEAN NOT NULL DEFAULT 0,
|
|
-- Per-user JSON preferences (memory toggle, future UI knobs).
|
|
-- SQLite has no JSONB; store as TEXT and let GORM (de)serialise via
|
|
-- the driver.Valuer / sql.Scanner methods on types.UserPreferences.
|
|
preferences TEXT NOT NULL DEFAULT '{}',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
|
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
|
CREATE INDEX IF NOT EXISTS idx_users_tenant_id ON users(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS auth_tokens (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
user_id VARCHAR(36) NOT NULL,
|
|
token TEXT NOT NULL,
|
|
token_type VARCHAR(50) NOT NULL,
|
|
expires_at DATETIME NOT NULL,
|
|
is_revoked BOOLEAN NOT NULL DEFAULT 0,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_auth_tokens_user_id ON auth_tokens(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_auth_tokens_token ON auth_tokens(token);
|
|
CREATE INDEX IF NOT EXISTS idx_auth_tokens_token_type ON auth_tokens(token_type);
|
|
CREATE INDEX IF NOT EXISTS idx_auth_tokens_expires_at ON auth_tokens(expires_at);
|
|
|
|
-- tenant_members carries the per-(user, tenant) TenantRole used by the
|
|
-- tenant-level RBAC introduced in #1303. SQLite does not support partial
|
|
-- indexes the same way Postgres does, so we use a plain unique index on
|
|
-- (user_id, tenant_id) — soft-deleted rows are filtered by the GORM scope.
|
|
CREATE TABLE IF NOT EXISTS tenant_members (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id VARCHAR(36) NOT NULL,
|
|
tenant_id INTEGER NOT NULL,
|
|
role VARCHAR(20) NOT NULL DEFAULT 'contributor',
|
|
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
|
invited_by VARCHAR(36),
|
|
joined_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_tenant_members_user_tenant_unique
|
|
ON tenant_members(user_id, tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_tenant_members_tenant_role
|
|
ON tenant_members(tenant_id, role);
|
|
CREATE INDEX IF NOT EXISTS idx_tenant_members_user
|
|
ON tenant_members(user_id);
|
|
|
|
-- audit_logs is the generic per-tenant durability for RBAC events
|
|
-- (and future KB / agent / datasource events). Sqlite mirror of the
|
|
-- 000044_audit_log migration; same column shape with INTEGER for the
|
|
-- BIGSERIAL id and TEXT in place of JSONB for details.
|
|
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
tenant_id INTEGER NOT NULL,
|
|
actor_user_id VARCHAR(36) NOT NULL DEFAULT '',
|
|
actor_role VARCHAR(32) NOT NULL DEFAULT '',
|
|
action VARCHAR(64) NOT NULL,
|
|
target_type VARCHAR(32) NOT NULL DEFAULT '',
|
|
target_id VARCHAR(64) NOT NULL DEFAULT '',
|
|
target_user_id VARCHAR(36) NOT NULL DEFAULT '',
|
|
request_path VARCHAR(512) NOT NULL DEFAULT '',
|
|
request_method VARCHAR(16) NOT NULL DEFAULT '',
|
|
outcome VARCHAR(16) NOT NULL DEFAULT 'success',
|
|
details TEXT NOT NULL DEFAULT '{}',
|
|
scope_type VARCHAR(32) NOT NULL DEFAULT '',
|
|
scope_id VARCHAR(64) NOT NULL DEFAULT '',
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_audit_logs_tenant_id_desc
|
|
ON audit_logs(tenant_id, id DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_audit_logs_actor
|
|
ON audit_logs(actor_user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_audit_logs_tenant_action
|
|
ON audit_logs(tenant_id, action);
|
|
CREATE INDEX IF NOT EXISTS idx_audit_logs_created_at
|
|
ON audit_logs(created_at);
|
|
CREATE INDEX IF NOT EXISTS idx_audit_logs_tenant_scope_desc
|
|
ON audit_logs(tenant_id, scope_type, scope_id, id DESC);
|
|
|
|
-- user_resource_favorites — sqlite mirror of migration 000047. Same
|
|
-- composite PK (user_id, tenant_id, resource_type, resource_id) so the
|
|
-- GORM model and FirstOrCreate idempotency carry over.
|
|
CREATE TABLE IF NOT EXISTS user_resource_favorites (
|
|
user_id VARCHAR(36) NOT NULL,
|
|
tenant_id INTEGER NOT NULL,
|
|
resource_type VARCHAR(16) NOT NULL,
|
|
resource_id VARCHAR(64) NOT NULL,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (user_id, tenant_id, resource_type, resource_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_user_resource_favorites_user_tenant_type_created_at
|
|
ON user_resource_favorites(user_id, tenant_id, resource_type, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_user_resource_favorites_tenant_id
|
|
ON user_resource_favorites(tenant_id);
|
|
|
|
-- user_kb_pins — sqlite mirror of migration 000050. Per-(user, tenant)
|
|
-- pinned knowledge bases; replaces the tenant-wide knowledge_bases.is_pinned
|
|
-- column for ordering purposes. The legacy column on knowledge_bases is
|
|
-- still defined above for back-compat with existing rows but is no longer
|
|
-- written by the application.
|
|
CREATE TABLE IF NOT EXISTS user_kb_pins (
|
|
tenant_id INTEGER NOT NULL,
|
|
user_id VARCHAR(36) NOT NULL,
|
|
kb_id VARCHAR(36) NOT NULL,
|
|
pinned_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (tenant_id, user_id, kb_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_user_kb_pins_user_tenant_pinned_at
|
|
ON user_kb_pins(tenant_id, user_id, pinned_at DESC);
|
|
|
|
-- tenant_invitations — sqlite mirror of migration 000048. SQLite supports
|
|
-- partial unique indexes too, so the same "one pending per (tenant,
|
|
-- invitee)" guard can be applied verbatim.
|
|
CREATE TABLE IF NOT EXISTS tenant_invitations (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
tenant_id INTEGER NOT NULL,
|
|
invitee_user_id VARCHAR(36) NOT NULL,
|
|
invited_by VARCHAR(36),
|
|
role VARCHAR(20) NOT NULL,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
|
message VARCHAR(500),
|
|
expires_at DATETIME NOT NULL,
|
|
responded_at DATETIME,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_tenant_invitations_unique_pending
|
|
ON tenant_invitations(tenant_id, invitee_user_id)
|
|
WHERE status = 'pending' AND deleted_at IS NULL;
|
|
CREATE INDEX IF NOT EXISTS idx_tenant_invitations_tenant
|
|
ON tenant_invitations(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_tenant_invitations_invitee
|
|
ON tenant_invitations(invitee_user_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS knowledge_tags (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
knowledge_base_id VARCHAR(36) NOT NULL,
|
|
name VARCHAR(128) NOT NULL,
|
|
color VARCHAR(32),
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
seq_id INTEGER,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_knowledge_tags_kb_name ON knowledge_tags(tenant_id, knowledge_base_id, name);
|
|
CREATE INDEX IF NOT EXISTS idx_knowledge_tags_kb ON knowledge_tags(tenant_id, knowledge_base_id);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_knowledge_tags_seq_id ON knowledge_tags(seq_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS mcp_services (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
enabled BOOLEAN DEFAULT 1,
|
|
transport_type VARCHAR(50) NOT NULL,
|
|
url VARCHAR(512),
|
|
headers TEXT,
|
|
auth_config TEXT,
|
|
advanced_config TEXT,
|
|
stdio_config TEXT,
|
|
env_vars TEXT,
|
|
is_builtin BOOLEAN NOT NULL DEFAULT 0,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_mcp_services_tenant_id ON mcp_services(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_mcp_services_enabled ON mcp_services(enabled);
|
|
CREATE INDEX IF NOT EXISTS idx_mcp_services_is_builtin ON mcp_services(is_builtin);
|
|
CREATE INDEX IF NOT EXISTS idx_mcp_services_deleted_at ON mcp_services(deleted_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS mcp_tool_approvals (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
service_id VARCHAR(36) NOT NULL,
|
|
tool_name VARCHAR(512) NOT NULL,
|
|
require_approval BOOLEAN NOT NULL DEFAULT 0,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (service_id) REFERENCES mcp_services(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_mcp_tool_approvals_tenant_svc_tool ON mcp_tool_approvals(tenant_id, service_id, tool_name);
|
|
CREATE INDEX IF NOT EXISTS idx_mcp_tool_approvals_service_id ON mcp_tool_approvals(service_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS mcp_oauth_clients (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
service_id VARCHAR(36) NOT NULL,
|
|
client_id VARCHAR(512) NOT NULL,
|
|
client_secret TEXT,
|
|
redirect_uri VARCHAR(1024),
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (service_id) REFERENCES mcp_services(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_mcp_oauth_clients_tenant_svc ON mcp_oauth_clients(tenant_id, service_id);
|
|
CREATE INDEX IF NOT EXISTS idx_mcp_oauth_clients_service_id ON mcp_oauth_clients(service_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS mcp_oauth_tokens (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
user_id VARCHAR(64) NOT NULL,
|
|
service_id VARCHAR(36) NOT NULL,
|
|
access_token TEXT,
|
|
refresh_token TEXT,
|
|
token_type VARCHAR(32),
|
|
expires_at DATETIME,
|
|
refresh_lease_id VARCHAR(36),
|
|
refresh_lease_until DATETIME,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (service_id) REFERENCES mcp_services(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_mcp_oauth_tokens_tenant_user_svc ON mcp_oauth_tokens(tenant_id, user_id, service_id);
|
|
CREATE INDEX IF NOT EXISTS idx_mcp_oauth_tokens_service_id ON mcp_oauth_tokens(service_id);
|
|
CREATE INDEX IF NOT EXISTS idx_mcp_oauth_tokens_user_id ON mcp_oauth_tokens(user_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS custom_agents (
|
|
id VARCHAR(36) NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
avatar VARCHAR(64),
|
|
is_builtin BOOLEAN NOT NULL DEFAULT 0,
|
|
tenant_id INTEGER NOT NULL,
|
|
created_by VARCHAR(36),
|
|
runnable_by_viewer BOOLEAN NOT NULL DEFAULT 1,
|
|
config TEXT NOT NULL DEFAULT '{}',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME,
|
|
PRIMARY KEY (id, tenant_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_custom_agents_tenant_id ON custom_agents(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_custom_agents_is_builtin ON custom_agents(is_builtin);
|
|
CREATE INDEX IF NOT EXISTS idx_custom_agents_deleted_at ON custom_agents(deleted_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS tenant_sandbox_configs (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
sandbox_type VARCHAR(32) NOT NULL,
|
|
config TEXT NOT NULL DEFAULT '{}',
|
|
cordoned_at DATETIME,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_tenant_sandbox_configs_tenant_id ON tenant_sandbox_configs(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_tenant_sandbox_configs_deleted_at ON tenant_sandbox_configs(deleted_at);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_tenant_sandbox_configs_tenant_name
|
|
ON tenant_sandbox_configs(tenant_id, name) WHERE deleted_at IS NULL;
|
|
|
|
CREATE TABLE IF NOT EXISTS organizations (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
owner_id VARCHAR(36) NOT NULL,
|
|
-- Plan 3 (#1303): owning tenant pinned at create time; see migration 000046.
|
|
owner_tenant_id INTEGER NOT NULL DEFAULT 0,
|
|
invite_code VARCHAR(32),
|
|
require_approval BOOLEAN DEFAULT 0,
|
|
invite_code_expires_at DATETIME,
|
|
invite_code_validity_days SMALLINT NOT NULL DEFAULT 7,
|
|
avatar VARCHAR(512) DEFAULT '',
|
|
searchable BOOLEAN NOT NULL DEFAULT 0,
|
|
member_limit INTEGER NOT NULL DEFAULT 50,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_organizations_owner_id ON organizations(owner_id);
|
|
CREATE INDEX IF NOT EXISTS idx_organizations_owner_tenant ON organizations(owner_tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_organizations_deleted_at ON organizations(deleted_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS organization_tenant_members (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
organization_id VARCHAR(36) NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
tenant_id INTEGER NOT NULL,
|
|
role VARCHAR(32) NOT NULL DEFAULT 'viewer',
|
|
representative_user_id VARCHAR(36) NOT NULL DEFAULT '',
|
|
joined_at DATETIME,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_org_tenant_members_unique ON organization_tenant_members(organization_id, tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_org_tenant_members_by_tenant ON organization_tenant_members(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_org_tenant_members_role ON organization_tenant_members(organization_id, role);
|
|
|
|
CREATE TABLE IF NOT EXISTS kb_shares (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
knowledge_base_id VARCHAR(36) NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
|
|
organization_id VARCHAR(36) NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
shared_by_user_id VARCHAR(36) NOT NULL,
|
|
source_tenant_id INTEGER NOT NULL,
|
|
permission VARCHAR(32) NOT NULL DEFAULT 'viewer',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_kb_shares_kb_id ON kb_shares(knowledge_base_id);
|
|
CREATE INDEX IF NOT EXISTS idx_kb_shares_org_id ON kb_shares(organization_id);
|
|
CREATE INDEX IF NOT EXISTS idx_kb_shares_source_tenant ON kb_shares(source_tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_kb_shares_deleted_at ON kb_shares(deleted_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS organization_join_requests (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
organization_id VARCHAR(36) NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
user_id VARCHAR(36) NOT NULL,
|
|
tenant_id INTEGER NOT NULL,
|
|
status VARCHAR(32) NOT NULL DEFAULT 'pending',
|
|
requested_role VARCHAR(32) NOT NULL DEFAULT 'viewer',
|
|
request_type VARCHAR(32) NOT NULL DEFAULT 'join',
|
|
prev_role VARCHAR(32),
|
|
message TEXT,
|
|
reviewed_by VARCHAR(36),
|
|
reviewed_at DATETIME,
|
|
review_message TEXT,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_org_join_requests_org_id ON organization_join_requests(organization_id);
|
|
CREATE INDEX IF NOT EXISTS idx_org_join_requests_user_id ON organization_join_requests(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_org_join_requests_status ON organization_join_requests(status);
|
|
-- Plan 3 (#1303): at most one pending request per (org, tenant, type).
|
|
-- Approved/rejected rows are not constrained so the audit trail stays.
|
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_org_join_requests_pending_per_tenant
|
|
ON organization_join_requests(organization_id, tenant_id, request_type)
|
|
WHERE status = 'pending';
|
|
|
|
CREATE TABLE IF NOT EXISTS agent_shares (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
agent_id VARCHAR(36) NOT NULL,
|
|
organization_id VARCHAR(36) NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
shared_by_user_id VARCHAR(36) NOT NULL,
|
|
source_tenant_id INTEGER NOT NULL,
|
|
permission VARCHAR(32) NOT NULL DEFAULT 'viewer',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME,
|
|
FOREIGN KEY (agent_id, source_tenant_id) REFERENCES custom_agents(id, tenant_id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_agent_shares_agent_id ON agent_shares(agent_id);
|
|
CREATE INDEX IF NOT EXISTS idx_agent_shares_org_id ON agent_shares(organization_id);
|
|
CREATE INDEX IF NOT EXISTS idx_agent_shares_source_tenant ON agent_shares(source_tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_agent_shares_deleted_at ON agent_shares(deleted_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS tenant_disabled_shared_agents (
|
|
tenant_id BIGINT NOT NULL,
|
|
agent_id VARCHAR(36) NOT NULL,
|
|
source_tenant_id BIGINT NOT NULL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (tenant_id, agent_id, source_tenant_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_tenant_disabled_shared_agents_tenant_id ON tenant_disabled_shared_agents(tenant_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS im_channel_sessions (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
platform VARCHAR(20) NOT NULL,
|
|
user_id VARCHAR(128) NOT NULL,
|
|
chat_id VARCHAR(128) NOT NULL DEFAULT '',
|
|
session_id VARCHAR(36) NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
tenant_id INTEGER NOT NULL,
|
|
agent_id VARCHAR(36) DEFAULT '',
|
|
im_channel_id VARCHAR(36) DEFAULT '',
|
|
thread_id VARCHAR(128) NOT NULL DEFAULT '',
|
|
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
|
metadata TEXT DEFAULT '{}',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_lookup
|
|
ON im_channel_sessions (platform, user_id, chat_id, tenant_id)
|
|
WHERE deleted_at IS NULL;
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_thread_lookup
|
|
ON im_channel_sessions (platform, chat_id, thread_id, tenant_id)
|
|
WHERE deleted_at IS NULL AND thread_id != '';
|
|
CREATE INDEX IF NOT EXISTS idx_im_channel_tenant ON im_channel_sessions (tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_im_channel_session ON im_channel_sessions (session_id);
|
|
CREATE INDEX IF NOT EXISTS idx_im_channel_sessions_channel ON im_channel_sessions (im_channel_id)
|
|
WHERE im_channel_id != '';
|
|
|
|
CREATE TABLE IF NOT EXISTS im_channels (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
agent_id VARCHAR(36) NOT NULL,
|
|
platform VARCHAR(20) NOT NULL,
|
|
name VARCHAR(255) NOT NULL DEFAULT '',
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
mode VARCHAR(20) NOT NULL DEFAULT 'websocket',
|
|
output_mode VARCHAR(20) NOT NULL DEFAULT 'stream',
|
|
credentials TEXT NOT NULL DEFAULT '{}',
|
|
knowledge_base_id VARCHAR(36) DEFAULT '',
|
|
bot_identity VARCHAR(255) NOT NULL DEFAULT '',
|
|
session_mode VARCHAR(20) NOT NULL DEFAULT 'user',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_im_channels_tenant ON im_channels (tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_im_channels_agent ON im_channels (agent_id);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_im_channels_bot_identity
|
|
ON im_channels (bot_identity)
|
|
WHERE deleted_at IS NULL AND bot_identity != '';
|
|
|
|
CREATE TABLE IF NOT EXISTS embed_channels (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
agent_id VARCHAR(36) NOT NULL DEFAULT 'builtin-quick-answer',
|
|
name VARCHAR(255) NOT NULL DEFAULT '',
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
publish_token VARCHAR(64) NOT NULL DEFAULT '',
|
|
allowed_origins TEXT NOT NULL DEFAULT '[]',
|
|
welcome_message TEXT NOT NULL DEFAULT '',
|
|
rate_limit_per_minute INTEGER NOT NULL DEFAULT 30,
|
|
rate_limit_per_day INTEGER NOT NULL DEFAULT 10000,
|
|
primary_color VARCHAR(32) NOT NULL DEFAULT '',
|
|
page_title VARCHAR(255) NOT NULL DEFAULT '',
|
|
header_title_mode VARCHAR(32) NOT NULL DEFAULT 'channel',
|
|
show_suggested_questions INTEGER NOT NULL DEFAULT 1,
|
|
widget_position VARCHAR(32) NOT NULL DEFAULT 'bottom-right',
|
|
allow_web_search INTEGER NOT NULL DEFAULT 0,
|
|
allow_file_upload INTEGER NOT NULL DEFAULT 0,
|
|
default_locale VARCHAR(16) NOT NULL DEFAULT '',
|
|
webhook_url VARCHAR(512) NOT NULL DEFAULT '',
|
|
webhook_secret VARCHAR(128) NOT NULL DEFAULT '',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_embed_channels_tenant ON embed_channels (tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_embed_channels_agent ON embed_channels (agent_id);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_embed_channels_publish_token
|
|
ON embed_channels (publish_token)
|
|
WHERE publish_token != '' AND deleted_at IS NULL;
|
|
|
|
CREATE TABLE IF NOT EXISTS data_sources (
|
|
id VARCHAR(36) NOT NULL PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
knowledge_base_id VARCHAR(36) NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
type VARCHAR(50) NOT NULL,
|
|
config TEXT,
|
|
sync_schedule VARCHAR(100),
|
|
sync_mode VARCHAR(20) DEFAULT 'incremental',
|
|
status VARCHAR(32) DEFAULT 'active',
|
|
conflict_strategy VARCHAR(32) DEFAULT 'overwrite',
|
|
sync_deletions INTEGER DEFAULT 1,
|
|
last_sync_at DATETIME NULL,
|
|
last_sync_cursor TEXT,
|
|
last_sync_result TEXT,
|
|
error_message TEXT,
|
|
sync_log_retention_days INTEGER DEFAULT 30,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_data_sources_tenant_id ON data_sources (tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_data_sources_knowledge_base_id ON data_sources (knowledge_base_id);
|
|
CREATE INDEX IF NOT EXISTS idx_data_sources_type ON data_sources (type);
|
|
CREATE INDEX IF NOT EXISTS idx_data_sources_status ON data_sources (status);
|
|
CREATE INDEX IF NOT EXISTS idx_data_sources_deleted_at ON data_sources (deleted_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS sync_logs (
|
|
id VARCHAR(36) NOT NULL PRIMARY KEY,
|
|
data_source_id VARCHAR(36) NOT NULL REFERENCES data_sources(id) ON DELETE CASCADE,
|
|
tenant_id INTEGER NOT NULL,
|
|
status VARCHAR(32) NOT NULL,
|
|
started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
finished_at DATETIME NULL,
|
|
items_total INTEGER DEFAULT 0,
|
|
items_created INTEGER DEFAULT 0,
|
|
items_updated INTEGER DEFAULT 0,
|
|
items_deleted INTEGER DEFAULT 0,
|
|
items_skipped INTEGER DEFAULT 0,
|
|
items_failed INTEGER DEFAULT 0,
|
|
error_message TEXT,
|
|
result TEXT,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_sync_logs_data_source_id ON sync_logs (data_source_id);
|
|
CREATE INDEX IF NOT EXISTS idx_sync_logs_tenant_id ON sync_logs (tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_sync_logs_status ON sync_logs (status);
|
|
CREATE INDEX IF NOT EXISTS idx_sync_logs_started_at ON sync_logs (started_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS web_search_providers (
|
|
id VARCHAR(36) NOT NULL PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
provider VARCHAR(50) NOT NULL,
|
|
description TEXT,
|
|
parameters TEXT,
|
|
is_default INTEGER DEFAULT 0,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_web_search_providers_tenant_id ON web_search_providers (tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_web_search_providers_provider ON web_search_providers (provider);
|
|
CREATE INDEX IF NOT EXISTS idx_web_search_providers_deleted_at ON web_search_providers (deleted_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS vector_stores (
|
|
id VARCHAR(36) NOT NULL PRIMARY KEY,
|
|
name VARCHAR(255) NOT NULL,
|
|
engine_type VARCHAR(50) NOT NULL,
|
|
connection_config TEXT NOT NULL DEFAULT '{}',
|
|
index_config TEXT NOT NULL DEFAULT '{}',
|
|
tenant_id INTEGER NOT NULL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME NULL
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_vector_stores_name_tenant
|
|
ON vector_stores(name, tenant_id)
|
|
WHERE deleted_at IS NULL;
|
|
CREATE INDEX IF NOT EXISTS idx_vector_stores_tenant_id ON vector_stores(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_vector_stores_engine_type ON vector_stores(engine_type);
|
|
CREATE INDEX IF NOT EXISTS idx_vector_stores_deleted_at ON vector_stores(deleted_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS storage_backends (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
provider VARCHAR(32) NOT NULL,
|
|
config TEXT NOT NULL DEFAULT '{}',
|
|
source VARCHAR(16) NOT NULL DEFAULT 'user',
|
|
status VARCHAR(16) NOT NULL DEFAULT 'active',
|
|
legacy_alias BOOLEAN NOT NULL DEFAULT 0,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_storage_backends_name_tenant
|
|
ON storage_backends(tenant_id, name) WHERE deleted_at IS NULL;
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_storage_backends_legacy_alias
|
|
ON storage_backends(tenant_id, provider) WHERE deleted_at IS NULL AND legacy_alias = 1;
|
|
CREATE INDEX IF NOT EXISTS idx_storage_backends_tenant ON storage_backends(tenant_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS resources (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
handle VARCHAR(22) NOT NULL UNIQUE,
|
|
tenant_id INTEGER NOT NULL,
|
|
storage_backend_id VARCHAR(36),
|
|
provider VARCHAR(32) NOT NULL,
|
|
physical_path TEXT NOT NULL,
|
|
location_hash VARCHAR(64) NOT NULL,
|
|
kind VARCHAR(32) NOT NULL DEFAULT 'file',
|
|
mime_type VARCHAR(255) NOT NULL DEFAULT '',
|
|
original_name VARCHAR(1024) NOT NULL DEFAULT '',
|
|
size INTEGER NOT NULL DEFAULT 0,
|
|
content_hash VARCHAR(64) NOT NULL DEFAULT '',
|
|
lifecycle VARCHAR(16) NOT NULL DEFAULT 'persistent',
|
|
expires_at DATETIME,
|
|
state VARCHAR(16) NOT NULL DEFAULT 'active',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_resources_tenant_location
|
|
ON resources(tenant_id, location_hash) WHERE deleted_at IS NULL;
|
|
CREATE INDEX IF NOT EXISTS idx_resources_tenant ON resources(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_resources_backend ON resources(storage_backend_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS resource_bindings (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
resource_id VARCHAR(36) NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
|
|
tenant_id INTEGER NOT NULL,
|
|
owner_type VARCHAR(32) NOT NULL,
|
|
owner_id VARCHAR(64) NOT NULL,
|
|
relation VARCHAR(32) NOT NULL DEFAULT 'attachment',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_resource_bindings_unique
|
|
ON resource_bindings(resource_id, owner_type, owner_id, relation);
|
|
CREATE INDEX IF NOT EXISTS idx_resource_bindings_owner
|
|
ON resource_bindings(tenant_id, owner_type, owner_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS resource_access_grants (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
token_hash VARCHAR(64) NOT NULL UNIQUE,
|
|
resource_id VARCHAR(36) NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
|
|
access_scope VARCHAR(16) NOT NULL DEFAULT 'read',
|
|
expires_at DATETIME NOT NULL,
|
|
revoked_at DATETIME,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_resource_access_grants_resource
|
|
ON resource_access_grants(resource_id);
|
|
CREATE INDEX IF NOT EXISTS idx_resource_access_grants_expires
|
|
ON resource_access_grants(expires_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS tenant_api_keys (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
tenant_id INTEGER,
|
|
scope_type TEXT NOT NULL DEFAULT 'tenant'
|
|
CHECK (scope_type IN ('tenant', 'platform')),
|
|
name TEXT NOT NULL,
|
|
key_hash TEXT NOT NULL UNIQUE,
|
|
api_key TEXT NOT NULL DEFAULT '',
|
|
full_access BOOLEAN NOT NULL DEFAULT 0,
|
|
knowledge_base_ids TEXT NOT NULL DEFAULT '[]',
|
|
capabilities TEXT NOT NULL DEFAULT '[]',
|
|
last_used_at DATETIME,
|
|
expires_at DATETIME,
|
|
revoked_at DATETIME,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
|
|
CHECK (
|
|
(scope_type = 'tenant' AND tenant_id IS NOT NULL)
|
|
OR (scope_type = 'platform' AND tenant_id IS NULL AND full_access = 0)
|
|
)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_tenant_api_keys_tenant ON tenant_api_keys(tenant_id);
|
|
CREATE INDEX IF NOT EXISTS idx_tenant_api_keys_revoked_at ON tenant_api_keys(revoked_at);
|
|
CREATE INDEX IF NOT EXISTS idx_tenant_api_keys_scope_type ON tenant_api_keys(scope_type);
|
|
|
|
CREATE TABLE IF NOT EXISTS temporary_documents (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
session_id VARCHAR(36) NOT NULL,
|
|
resource_ref TEXT NOT NULL,
|
|
file_name VARCHAR(1024) NOT NULL,
|
|
file_type VARCHAR(32) NOT NULL,
|
|
mime_type VARCHAR(255) NOT NULL DEFAULT '',
|
|
file_size INTEGER NOT NULL,
|
|
status VARCHAR(16) NOT NULL DEFAULT 'uploaded',
|
|
content TEXT NOT NULL DEFAULT '',
|
|
chunks TEXT NOT NULL DEFAULT '[]',
|
|
image_refs TEXT NOT NULL DEFAULT '[]',
|
|
metadata TEXT NOT NULL DEFAULT '{}',
|
|
processing_options TEXT NOT NULL DEFAULT '{}',
|
|
token_count INTEGER NOT NULL DEFAULT 0,
|
|
chunk_count INTEGER NOT NULL DEFAULT 0,
|
|
error_message TEXT NOT NULL DEFAULT '',
|
|
expires_at DATETIME NOT NULL,
|
|
started_at DATETIME,
|
|
ready_at DATETIME,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_temporary_documents_scope ON temporary_documents(tenant_id, session_id);
|
|
CREATE INDEX IF NOT EXISTS idx_temporary_documents_status ON temporary_documents(status);
|
|
CREATE INDEX IF NOT EXISTS idx_temporary_documents_expires ON temporary_documents(expires_at);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Wiki (consolidated from Postgres migrations 000037, 000040, 000061, 000075)
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
CREATE TABLE IF NOT EXISTS wiki_pages (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
knowledge_base_id VARCHAR(36) NOT NULL,
|
|
slug VARCHAR(255) NOT NULL,
|
|
title VARCHAR(512) NOT NULL DEFAULT '',
|
|
page_type VARCHAR(32) NOT NULL DEFAULT 'summary',
|
|
status VARCHAR(32) NOT NULL DEFAULT 'published',
|
|
content TEXT NOT NULL DEFAULT '',
|
|
summary TEXT NOT NULL DEFAULT '',
|
|
parent_slug VARCHAR(255) NOT NULL DEFAULT '',
|
|
folder_id VARCHAR(36) NOT NULL DEFAULT '',
|
|
category_path TEXT DEFAULT '[]',
|
|
wiki_path VARCHAR(1024) NOT NULL DEFAULT '',
|
|
depth INTEGER NOT NULL DEFAULT 0,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
source_refs TEXT DEFAULT '[]',
|
|
chunk_refs TEXT DEFAULT '[]',
|
|
in_links TEXT DEFAULT '[]',
|
|
out_links TEXT DEFAULT '[]',
|
|
page_metadata TEXT DEFAULT '{}',
|
|
aliases TEXT DEFAULT '[]',
|
|
version INTEGER NOT NULL DEFAULT 1,
|
|
last_edit_source VARCHAR(16) NOT NULL DEFAULT '',
|
|
last_editor_id VARCHAR(64) NOT NULL DEFAULT '',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_wiki_pages_kb_slug
|
|
ON wiki_pages (knowledge_base_id, slug)
|
|
WHERE deleted_at IS NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_wiki_pages_kb_id
|
|
ON wiki_pages (knowledge_base_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_wiki_pages_page_type
|
|
ON wiki_pages (knowledge_base_id, page_type);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_wiki_pages_parent_slug
|
|
ON wiki_pages (knowledge_base_id, parent_slug);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_wiki_pages_tree
|
|
ON wiki_pages (knowledge_base_id, page_type, wiki_path, sort_order, title);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_wiki_pages_folder
|
|
ON wiki_pages (knowledge_base_id, folder_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_wiki_pages_tenant_id
|
|
ON wiki_pages (tenant_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_wiki_pages_deleted_at
|
|
ON wiki_pages (deleted_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS wiki_folders (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL DEFAULT 0,
|
|
knowledge_base_id VARCHAR(36) NOT NULL,
|
|
parent_id VARCHAR(36) NOT NULL DEFAULT '',
|
|
name VARCHAR(255) NOT NULL,
|
|
path VARCHAR(1024) NOT NULL DEFAULT '',
|
|
depth INTEGER NOT NULL DEFAULT 0,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_wiki_folders_parent_name
|
|
ON wiki_folders (knowledge_base_id, parent_id, name)
|
|
WHERE deleted_at IS NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_wiki_folders_parent
|
|
ON wiki_folders (knowledge_base_id, parent_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_wiki_folders_deleted_at
|
|
ON wiki_folders (deleted_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS wiki_page_issues (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
knowledge_base_id VARCHAR(36) NOT NULL,
|
|
slug VARCHAR(255) NOT NULL,
|
|
issue_type VARCHAR(50) NOT NULL,
|
|
description TEXT NOT NULL,
|
|
suspected_knowledge_ids TEXT,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
|
reported_by VARCHAR(100) NOT NULL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at DATETIME
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_wiki_page_issues_tenant_id
|
|
ON wiki_page_issues(tenant_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_wiki_page_issues_knowledge_base_id
|
|
ON wiki_page_issues(knowledge_base_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_wiki_page_issues_slug
|
|
ON wiki_page_issues(slug);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_wiki_page_issues_status
|
|
ON wiki_page_issues(status);
|
|
|
|
CREATE TABLE IF NOT EXISTS wiki_page_revisions (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
knowledge_base_id VARCHAR(36) NOT NULL,
|
|
page_id VARCHAR(36) NOT NULL,
|
|
slug VARCHAR(255) NOT NULL,
|
|
version INTEGER NOT NULL,
|
|
title VARCHAR(512) NOT NULL DEFAULT '',
|
|
page_type VARCHAR(32) NOT NULL DEFAULT 'summary',
|
|
status VARCHAR(32) NOT NULL DEFAULT 'published',
|
|
content TEXT NOT NULL DEFAULT '',
|
|
summary TEXT NOT NULL DEFAULT '',
|
|
aliases TEXT DEFAULT '[]',
|
|
edit_source VARCHAR(16) NOT NULL DEFAULT '',
|
|
editor_id VARCHAR(64) NOT NULL DEFAULT '',
|
|
edited_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_wiki_page_revisions_page_version
|
|
ON wiki_page_revisions (page_id, version);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_wiki_page_revisions_kb_slug
|
|
ON wiki_page_revisions (knowledge_base_id, slug);
|