From 72e040546ae1967a62ae37c57eaa50dd91dbd796 Mon Sep 17 00:00:00 2001 From: Rogee Date: Tue, 22 Sep 2026 09:58:58 +0800 Subject: [PATCH] feat: add account-scoped data synchronization --- control-plane/account_store.go | 1160 +++++++++++++++++ control-plane/account_store_test.go | 386 ++++++ .../cmd/wxagent-control-plane/main.go | 16 + control-plane/data_routes.go | 421 ++++++ control-plane/data_routes_test.go | 126 ++ control-plane/go.mod | 17 +- control-plane/go.sum | 47 + control-plane/protocol.go | 16 +- control-plane/server.go | 165 ++- .../web/dist/assets/index-6ql7JGil.css | 1 + .../web/dist/assets/index-BkrtfWai.css | 1 - .../web/dist/assets/index-C2PP8Qbj.js | 40 - .../web/dist/assets/index-Ihi-UMyS.js | 40 + control-plane/web/dist/index.html | 4 +- control-plane/web/package.json | 1 + control-plane/web/src/main.jsx | 150 ++- control-plane/web/src/readState.js | 72 + control-plane/web/src/readState.test.js | 45 + control-plane/web/src/styles.css | 28 +- docs/WxAgent-CSharp-开发计划.md | 4 + ...xAgent-会话消息同步与分账号存储开发计划.md | 310 +++++ ...-远程多节点控制与白名单数据上报开发计划.md | 4 + ...WxAgent-会话消息同步-P4-live-evidence.json | 95 ++ .../WxAgent-会话消息同步-P4-验收记录.md | 86 ++ node-agent/WxAgent.Core/RemoteContracts.cs | 17 +- .../WxAgent.Core/RemoteControlClient.cs | 46 + node-agent/WxAgent.Core/RemoteDataSync.cs | 435 +++++++ .../WxAgent.Host/WindowsAgentBackend.cs | 10 +- .../WxAgent.Service/ReadOnlyContracts.cs | 21 +- .../RemoteAgentHostedService.cs | 199 ++- node-agent/WxAgent.Service/ServiceHost.cs | 2 + node-agent/WxAgent.Service/ServiceOptions.cs | 15 + node-agent/WxAgent.Tray/Program.cs | 17 +- .../DatabaseMessageSyncCollector.cs | 96 ++ .../WxAgent.Windows/WechatMessageDbReader.cs | 123 +- .../WxAgent.Core.Tests/RemoteDataSyncTests.cs | 163 +++ .../ReadCoverageTests.cs | 32 + 37 files changed, 4252 insertions(+), 159 deletions(-) create mode 100644 control-plane/account_store.go create mode 100644 control-plane/account_store_test.go create mode 100644 control-plane/data_routes.go create mode 100644 control-plane/data_routes_test.go create mode 100644 control-plane/go.sum create mode 100644 control-plane/web/dist/assets/index-6ql7JGil.css delete mode 100644 control-plane/web/dist/assets/index-BkrtfWai.css delete mode 100644 control-plane/web/dist/assets/index-C2PP8Qbj.js create mode 100644 control-plane/web/dist/assets/index-Ihi-UMyS.js create mode 100644 control-plane/web/src/readState.js create mode 100644 control-plane/web/src/readState.test.js create mode 100644 docs/WxAgent-会话消息同步与分账号存储开发计划.md create mode 100644 docs/validation/WxAgent-会话消息同步-P4-live-evidence.json create mode 100644 docs/validation/WxAgent-会话消息同步-P4-验收记录.md create mode 100644 node-agent/WxAgent.Core/RemoteDataSync.cs create mode 100644 node-agent/WxAgent.Windows/DatabaseMessageSyncCollector.cs create mode 100644 tests/node-agent/WxAgent.Core.Tests/RemoteDataSyncTests.cs create mode 100644 tests/node-agent/WxAgent.Service.Tests/ReadCoverageTests.cs diff --git a/control-plane/account_store.go b/control-plane/account_store.go new file mode 100644 index 0000000..555860c --- /dev/null +++ b/control-plane/account_store.go @@ -0,0 +1,1160 @@ +package controlplane + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "database/sql" + + _ "modernc.org/sqlite" +) + +var ( + ErrAccountBindingConflict = errors.New("account binding conflicts with the registered source") + ErrAccountNotAuthorized = errors.New("account is not currently authorized for data ingestion") + ErrAccountStoreNotFound = errors.New("account data store was not found") + ErrBatchConflict = errors.New("ingest batch id or payload conflicts with an existing batch") + ErrBatchSequenceGap = errors.New("ingest batch sequence has a gap") + ErrBatchSequenceConflict = errors.New("ingest batch sequence conflicts with confirmed progress") + ErrAccountCapacityExceeded = errors.New("account data capacity budget exceeded") +) + +const ( + accountSchemaVersion = "1" + catalogSchemaVersion = "1" +) + +// AccountStoreManager owns the small catalog and opens one platform-owned +// SQLite shard per verified account. The existing JSON Store remains the +// source of truth for control-plane tasks, nodes, events and audit records. +type AccountStoreManagerOptions struct { + Retention time.Duration + MaxShardBytes int64 + MaxBatchBytes int64 +} + +type AccountStoreManager struct { + root string + catalog *sql.DB + retention time.Duration + maxShardBytes int64 + maxBatchBytes int64 + mu sync.Mutex + closed bool +} + +type AccountRegistration struct { + AccountID string + StableIdentity string + SourceNodeID string + SourceGeneration string + Verified bool + AuthorizationVersion int + AuthorizationExpiresAt *time.Time + ReportingScopes []ReportingScope +} + +type ReportingScope struct { + ChatID string + DataType string + ExpiresAt *time.Time + ConfigVersion int +} + +type ConversationRecord struct { + ChatID string + ChatType string + Title string + LastActivityAt *time.Time + Source string + ObservedAt time.Time + DirectoryState string +} + +type MessageRecord struct { + MessageID string + ChatID string + SourceMessageID string + Direction string + MessageType string + Text string + SourceTime time.Time + ObservedAt time.Time + SourceVersion string + PayloadHash string +} + +type IngestBatch struct { + BatchID string + SourceGeneration string + StreamKey string + Sequence int64 + CursorStart string + CursorEnd string + PayloadHash string + CoverageState string + Conversations []ConversationRecord + Messages []MessageRecord +} + +type BatchApplyResult struct { + Duplicate bool + ConfirmedSequence int64 + ConfirmedCursor string +} + +type AccountStore struct { + manager *AccountStoreManager + accountID string + path string + db *sql.DB + maxShardBytes int64 + maxBatchBytes int64 + closeOnce sync.Once + closeErr error +} + +func OpenAccountStoreManager(root string, options ...AccountStoreManagerOptions) (*AccountStoreManager, error) { + if strings.TrimSpace(root) == "" { + return nil, errors.New("account data directory is required") + } + cleanRoot := filepath.Clean(root) + if err := os.MkdirAll(cleanRoot, 0o700); err != nil { + return nil, fmt.Errorf("create account data directory: %w", err) + } + catalog, err := openSQLite(filepath.Join(cleanRoot, "catalog.sqlite")) + if err != nil { + return nil, err + } + var configured AccountStoreManagerOptions + if len(options) > 0 { + configured = options[0] + } + if configured.Retention < 0 || configured.MaxShardBytes < 0 || configured.MaxBatchBytes < 0 { + _ = catalog.Close() + return nil, errors.New("account store budgets must be non-negative") + } + manager := &AccountStoreManager{ + root: cleanRoot, catalog: catalog, + retention: configured.Retention, maxShardBytes: configured.MaxShardBytes, maxBatchBytes: configured.MaxBatchBytes, + } + if err := manager.initCatalog(context.Background()); err != nil { + _ = catalog.Close() + return nil, err + } + return manager, nil +} + +func (m *AccountStoreManager) Close() error { + m.mu.Lock() + defer m.mu.Unlock() + if m.closed { + return nil + } + m.closed = true + if m.catalog == nil { + return nil + } + return m.catalog.Close() +} + +func (m *AccountStoreManager) RegisterAccount(ctx context.Context, registration AccountRegistration) (*AccountStore, error) { + if err := validateRegistration(registration); err != nil { + return nil, err + } + m.mu.Lock() + defer m.mu.Unlock() + if m.closed { + return nil, errors.New("account store manager is closed") + } + + var existing struct { + sourceNodeID, sourceGeneration, stableIdentity, shardPath string + verified, authorizationVersion int + authorizationExpiresAt sql.NullString + } + err := m.catalog.QueryRowContext(ctx, ` + SELECT source_node_id, source_generation, stable_identity, shard_path, + verified, authorization_version, authorization_expires_at + FROM accounts WHERE account_id = ?`, registration.AccountID). + Scan(&existing.sourceNodeID, &existing.sourceGeneration, &existing.stableIdentity, + &existing.shardPath, &existing.verified, &existing.authorizationVersion, + &existing.authorizationExpiresAt) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, fmt.Errorf("read account registration: %w", err) + } + + shardPath := existing.shardPath + created := errors.Is(err, sql.ErrNoRows) + if created { + shardPath = accountShardPath(m.root, registration.AccountID) + if err := createAccountShard(shardPath); err != nil { + return nil, err + } + } else if existing.sourceGeneration != registration.SourceGeneration || existing.stableIdentity != registration.StableIdentity { + return nil, ErrAccountBindingConflict + } + + tx, err := m.catalog.BeginTx(ctx, nil) + if err != nil { + return nil, fmt.Errorf("begin account registration: %w", err) + } + rollback := func(cause error) (*AccountStore, error) { + _ = tx.Rollback() + return nil, cause + } + expiresAt := nullableTimeArg(registration.AuthorizationExpiresAt) + now := formatTime(ptrTime(time.Now().UTC())) + if created { + _, err = tx.ExecContext(ctx, ` + INSERT INTO accounts ( + account_id, source_node_id, source_generation, stable_identity, + verified, authorization_version, authorization_expires_at, + shard_path, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + registration.AccountID, registration.SourceNodeID, registration.SourceGeneration, + registration.StableIdentity, boolInt(registration.Verified), registration.AuthorizationVersion, + expiresAt, shardPath, now, now) + } else { + _, err = tx.ExecContext(ctx, ` + UPDATE accounts + SET verified = ?, authorization_version = ?, authorization_expires_at = ?, updated_at = ? + WHERE account_id = ?`, + boolInt(registration.Verified), registration.AuthorizationVersion, expiresAt, now, registration.AccountID) + } + if err != nil { + return rollback(fmt.Errorf("write account registration: %w", err)) + } + if registration.ReportingScopes != nil { + if _, err := tx.ExecContext(ctx, `DELETE FROM reporting_scopes WHERE account_id = ?`, registration.AccountID); err != nil { + return rollback(fmt.Errorf("replace reporting scopes: %w", err)) + } + for _, scope := range registration.ReportingScopes { + if err := validateScope(scope); err != nil { + return rollback(err) + } + if _, err := tx.ExecContext(ctx, ` + INSERT INTO reporting_scopes (account_id, chat_id, data_type, expires_at, config_version) + VALUES (?, ?, ?, ?, ?)`, registration.AccountID, scope.ChatID, scope.DataType, + nullableTimeArg(scope.ExpiresAt), scope.ConfigVersion); err != nil { + return rollback(fmt.Errorf("write reporting scope: %w", err)) + } + } + } + if err := tx.Commit(); err != nil { + return nil, fmt.Errorf("commit account registration: %w", err) + } + return m.openAccountLocked(ctx, registration.AccountID, shardPath) +} + +func (m *AccountStoreManager) OpenAccount(ctx context.Context, accountID string) (*AccountStore, error) { + if strings.TrimSpace(accountID) == "" { + return nil, errors.New("account id is required") + } + m.mu.Lock() + defer m.mu.Unlock() + if m.closed { + return nil, errors.New("account store manager is closed") + } + var shardPath string + if err := m.catalog.QueryRowContext(ctx, `SELECT shard_path FROM accounts WHERE account_id = ?`, accountID).Scan(&shardPath); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrAccountStoreNotFound + } + return nil, fmt.Errorf("find account shard: %w", err) + } + return m.openAccountLocked(ctx, accountID, shardPath) +} + +func (m *AccountStoreManager) openAccountLocked(ctx context.Context, accountID, shardPath string) (*AccountStore, error) { + db, err := openSQLite(shardPath) + if err != nil { + return nil, err + } + if err := validateAccountSchema(ctx, db); err != nil { + _ = db.Close() + return nil, err + } + if err := ensureAccountIndexes(ctx, db); err != nil { + _ = db.Close() + return nil, err + } + return &AccountStore{ + manager: m, accountID: accountID, path: shardPath, db: db, + maxShardBytes: m.maxShardBytes, maxBatchBytes: m.maxBatchBytes, + }, nil +} + +func (m *AccountStoreManager) initCatalog(ctx context.Context) error { + _, err := m.catalog.ExecContext(ctx, ` + CREATE TABLE IF NOT EXISTS schema_meta ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS accounts ( + account_id TEXT PRIMARY KEY, + source_node_id TEXT NOT NULL, + source_generation TEXT NOT NULL, + stable_identity TEXT NOT NULL, + verified INTEGER NOT NULL CHECK (verified IN (0, 1)), + authorization_version INTEGER NOT NULL, + authorization_expires_at TEXT, + shard_path TEXT NOT NULL UNIQUE, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS reporting_scopes ( + account_id TEXT NOT NULL REFERENCES accounts(account_id) ON DELETE CASCADE, + chat_id TEXT NOT NULL, + data_type TEXT NOT NULL, + expires_at TEXT, + config_version INTEGER NOT NULL, + PRIMARY KEY (account_id, chat_id, data_type) + ); + INSERT INTO schema_meta(key, value) VALUES ('schema_version', '1') + ON CONFLICT(key) DO UPDATE SET value = excluded.value; + `) + if err != nil { + return fmt.Errorf("initialize account catalog: %w", err) + } + return nil +} + +func (s *AccountStore) Close() error { + s.closeOnce.Do(func() { s.closeErr = s.db.Close() }) + return s.closeErr +} + +func (s *AccountStore) Path() string { return s.path } +func (s *AccountStore) AccountID() string { return s.accountID } + +type MaintenanceReport struct { + Accounts int + DeletedMessages int64 + DeletedConversations int64 + DeletedBatches int64 + Checkpointed int + OverBudgetAccounts []string + Errors []string +} + +// RunMaintenance applies the configured retention policy and performs a +// passive WAL checkpoint for each account. One broken shard is reported and +// isolated so it cannot prevent maintenance of other accounts. +func (m *AccountStoreManager) RunMaintenance(ctx context.Context, now time.Time) (MaintenanceReport, error) { + if now.IsZero() { + now = time.Now().UTC() + } else { + now = now.UTC() + } + m.mu.Lock() + if m.closed { + m.mu.Unlock() + return MaintenanceReport{}, errors.New("account store manager is closed") + } + rows, err := m.catalog.QueryContext(ctx, `SELECT account_id FROM accounts ORDER BY account_id`) + if err != nil { + m.mu.Unlock() + return MaintenanceReport{}, fmt.Errorf("list account shards: %w", err) + } + var accountIDs []string + for rows.Next() { + var accountID string + if err := rows.Scan(&accountID); err != nil { + _ = rows.Close() + m.mu.Unlock() + return MaintenanceReport{}, fmt.Errorf("scan account shard: %w", err) + } + accountIDs = append(accountIDs, accountID) + } + if err := rows.Err(); err != nil { + _ = rows.Close() + m.mu.Unlock() + return MaintenanceReport{}, fmt.Errorf("read account shards: %w", err) + } + _ = rows.Close() + m.mu.Unlock() + + report := MaintenanceReport{Accounts: len(accountIDs)} + for _, accountID := range accountIDs { + store, err := m.OpenAccount(ctx, accountID) + if err != nil { + report.Errors = append(report.Errors, accountID+":open") + continue + } + deletedMessages, deletedConversations, deletedBatches, err := store.maintain(ctx, now, m.retention) + if err != nil { + report.Errors = append(report.Errors, accountID+":maintenance") + _ = store.Close() + continue + } + report.DeletedMessages += deletedMessages + report.DeletedConversations += deletedConversations + report.DeletedBatches += deletedBatches + report.Checkpointed++ + if m.maxShardBytes > 0 { + if size, sizeErr := accountStorageBytes(store.path); sizeErr != nil { + report.Errors = append(report.Errors, accountID+":size") + } else if size > m.maxShardBytes { + report.OverBudgetAccounts = append(report.OverBudgetAccounts, accountID) + } + } + _ = store.Close() + } + return report, nil +} + +func (s *AccountStore) maintain(ctx context.Context, now time.Time, retention time.Duration) (int64, int64, int64, error) { + var deletedMessages, deletedConversations, deletedBatches int64 + if retention > 0 { + cutoff := formatTime(ptrTime(now.Add(-retention))) + result, err := s.db.ExecContext(ctx, `DELETE FROM messages WHERE observed_at < ?`, cutoff) + if err != nil { + return 0, 0, 0, fmt.Errorf("retain messages: %w", err) + } + deletedMessages, _ = result.RowsAffected() + result, err = s.db.ExecContext(ctx, `DELETE FROM conversations WHERE observed_at < ? AND NOT EXISTS (SELECT 1 FROM messages WHERE messages.chat_id = conversations.chat_id)`, cutoff) + if err != nil { + return 0, 0, 0, fmt.Errorf("retain conversations: %w", err) + } + deletedConversations, _ = result.RowsAffected() + result, err = s.db.ExecContext(ctx, `DELETE FROM ingest_batches WHERE status = 'confirmed' AND confirmed_at IS NOT NULL AND confirmed_at < ?`, cutoff) + if err != nil { + return 0, 0, 0, fmt.Errorf("retain ingest batches: %w", err) + } + deletedBatches, _ = result.RowsAffected() + } + if _, err := s.db.ExecContext(ctx, `PRAGMA wal_checkpoint(PASSIVE)`); err != nil { + return 0, 0, 0, fmt.Errorf("checkpoint account database: %w", err) + } + if _, err := s.db.ExecContext(ctx, `PRAGMA optimize`); err != nil { + return 0, 0, 0, fmt.Errorf("optimize account database: %w", err) + } + return deletedMessages, deletedConversations, deletedBatches, nil +} + +// RestoreAccount validates a consistent account backup before atomically +// replacing the registered shard. The catalog binding is retained, so a +// restored file cannot change the account/source identity. +func (m *AccountStoreManager) RestoreAccount(ctx context.Context, accountID, backupPath string) error { + if strings.TrimSpace(accountID) == "" || strings.TrimSpace(backupPath) == "" { + return errors.New("account id and backup path are required") + } + m.mu.Lock() + if m.closed { + m.mu.Unlock() + return errors.New("account store manager is closed") + } + var shardPath string + if err := m.catalog.QueryRowContext(ctx, `SELECT shard_path FROM accounts WHERE account_id = ?`, accountID).Scan(&shardPath); err != nil { + m.mu.Unlock() + if errors.Is(err, sql.ErrNoRows) { + return ErrAccountStoreNotFound + } + return fmt.Errorf("find account shard: %w", err) + } + m.mu.Unlock() + + source, err := os.Open(filepath.Clean(backupPath)) + if err != nil { + return fmt.Errorf("open account backup: %w", err) + } + defer source.Close() + temporary, err := os.CreateTemp(filepath.Dir(shardPath), ".restore-*") + if err != nil { + return fmt.Errorf("create restore file: %w", err) + } + temporaryPath := temporary.Name() + defer os.Remove(temporaryPath) + if _, err := io.Copy(temporary, source); err != nil { + _ = temporary.Close() + return fmt.Errorf("copy account backup: %w", err) + } + if err := temporary.Sync(); err != nil { + _ = temporary.Close() + return fmt.Errorf("sync restored account: %w", err) + } + if err := temporary.Close(); err != nil { + return fmt.Errorf("close restored account: %w", err) + } + if err := os.Chmod(temporaryPath, 0o600); err != nil { + return fmt.Errorf("protect restored account: %w", err) + } + check, err := openSQLite(temporaryPath) + if err != nil { + return fmt.Errorf("open restored account: %w", err) + } + if err := validateAccountSchema(ctx, check); err != nil { + _ = check.Close() + return err + } + if err := ensureAccountIndexes(ctx, check); err != nil { + _ = check.Close() + return err + } + var integrity string + if err := check.QueryRowContext(ctx, `PRAGMA integrity_check`).Scan(&integrity); err != nil { + _ = check.Close() + return fmt.Errorf("check restored account: %w", err) + } + if integrity != "ok" { + _ = check.Close() + return fmt.Errorf("restored account integrity check returned %q", integrity) + } + if err := check.Close(); err != nil { + return fmt.Errorf("close restored account: %w", err) + } + oldPath := shardPath + fmt.Sprintf(".before-restore-%d", time.Now().UnixNano()) + if err := os.Rename(shardPath, oldPath); err != nil { + return fmt.Errorf("stage current account shard: %w", err) + } + if err := os.Rename(temporaryPath, shardPath); err != nil { + _ = os.Rename(oldPath, shardPath) + return fmt.Errorf("install restored account shard: %w", err) + } + if err := os.Remove(oldPath); err != nil && !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("remove old account shard: %w", err) + } + return nil +} + +// Backup creates a consistent SQLite backup without copying a live main file +// while WAL frames are pending. The destination must not already exist. +func (s *AccountStore) Backup(ctx context.Context, targetPath string) error { + if strings.TrimSpace(targetPath) == "" { + return errors.New("backup path is required") + } + cleanPath := filepath.Clean(targetPath) + if _, err := os.Stat(cleanPath); err == nil { + return errors.New("backup path already exists") + } else if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("check backup path: %w", err) + } + if err := os.MkdirAll(filepath.Dir(cleanPath), 0o700); err != nil { + return fmt.Errorf("create backup directory: %w", err) + } + if _, err := s.db.ExecContext(ctx, "VACUUM INTO ?", cleanPath); err != nil { + return fmt.Errorf("backup account database: %w", err) + } + if err := os.Chmod(cleanPath, 0o600); err != nil { + return fmt.Errorf("protect account backup: %w", err) + } + return nil +} + +func (s *AccountStore) ApplyBatch(ctx context.Context, batch IngestBatch) (BatchApplyResult, error) { + if err := validateBatch(batch); err != nil { + return BatchApplyResult{}, err + } + scopes, sourceGeneration, err := s.manager.activeScopes(ctx, s.accountID) + if err != nil { + return BatchApplyResult{}, err + } + if sourceGeneration != batch.SourceGeneration { + return BatchApplyResult{}, ErrAccountBindingConflict + } + if err := authorizeBatch(batch, scopes); err != nil { + return BatchApplyResult{}, err + } + if err := s.ensureCapacity(batch); err != nil { + return BatchApplyResult{}, err + } + + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return BatchApplyResult{}, fmt.Errorf("begin ingest batch: %w", err) + } + rollback := func(cause error) (BatchApplyResult, error) { + _ = tx.Rollback() + return BatchApplyResult{}, cause + } + var existingHash, existingGeneration, existingStatus string + var existingSequence int64 + err = tx.QueryRowContext(ctx, `SELECT payload_hash, source_generation, sequence, status FROM ingest_batches WHERE batch_id = ?`, batch.BatchID). + Scan(&existingHash, &existingGeneration, &existingSequence, &existingStatus) + if err == nil { + if existingHash != batch.PayloadHash || existingGeneration != batch.SourceGeneration || existingSequence != batch.Sequence { + return rollback(ErrBatchConflict) + } + if existingStatus == "confirmed" { + if err := tx.Rollback(); err != nil { + return BatchApplyResult{}, fmt.Errorf("close duplicate ingest transaction: %w", err) + } + return BatchApplyResult{Duplicate: true, ConfirmedSequence: existingSequence, ConfirmedCursor: batch.CursorEnd}, nil + } + } else if !errors.Is(err, sql.ErrNoRows) { + return rollback(fmt.Errorf("read ingest batch: %w", err)) + } + + var confirmedSequence int64 + var currentGeneration string + err = tx.QueryRowContext(ctx, `SELECT source_generation, confirmed_sequence FROM sync_state WHERE stream_key = ?`, batch.StreamKey). + Scan(¤tGeneration, &confirmedSequence) + if errors.Is(err, sql.ErrNoRows) { + currentGeneration = batch.SourceGeneration + confirmedSequence = 0 + } else if err != nil { + return rollback(fmt.Errorf("read sync state: %w", err)) + } else if currentGeneration != batch.SourceGeneration { + return rollback(ErrAccountBindingConflict) + } + if batch.Sequence > confirmedSequence+1 { + return rollback(ErrBatchSequenceGap) + } + if batch.Sequence <= confirmedSequence && existingStatus != "confirmed" { + return rollback(ErrBatchSequenceConflict) + } + + now := formatTime(ptrTime(time.Now().UTC())) + if existingStatus == "" { + if _, err := tx.ExecContext(ctx, ` + INSERT INTO ingest_batches (batch_id, source_generation, stream_key, sequence, cursor_start, cursor_end, payload_hash, status, received_at) + VALUES (?, ?, ?, ?, ?, ?, ?, 'applying', ?)`, batch.BatchID, batch.SourceGeneration, batch.StreamKey, + batch.Sequence, batch.CursorStart, batch.CursorEnd, batch.PayloadHash, now); err != nil { + return rollback(fmt.Errorf("record ingest batch: %w", err)) + } + } + for _, conversation := range batch.Conversations { + if _, err := tx.ExecContext(ctx, ` + INSERT INTO conversations (chat_id, chat_type, title, last_activity_at, source, observed_at, directory_state) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(chat_id) DO UPDATE SET + chat_type = excluded.chat_type, + title = excluded.title, + last_activity_at = excluded.last_activity_at, + source = excluded.source, + observed_at = excluded.observed_at, + directory_state = excluded.directory_state`, conversation.ChatID, conversation.ChatType, + conversation.Title, formatTime(conversation.LastActivityAt), conversation.Source, + formatTime(ptrTime(conversation.ObservedAt)), conversation.DirectoryState); err != nil { + return rollback(fmt.Errorf("write conversation: %w", err)) + } + } + for _, message := range batch.Messages { + if _, err := tx.ExecContext(ctx, ` + INSERT INTO messages ( + message_id, chat_id, source_message_id, direction, message_type, text, + source_time, observed_at, source_version, payload_hash + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(message_id) DO UPDATE SET + chat_id = excluded.chat_id, + source_message_id = excluded.source_message_id, + direction = excluded.direction, + message_type = excluded.message_type, + text = excluded.text, + source_time = excluded.source_time, + observed_at = excluded.observed_at, + source_version = excluded.source_version, + payload_hash = excluded.payload_hash`, message.MessageID, message.ChatID, + message.SourceMessageID, message.Direction, message.MessageType, message.Text, + formatTime(ptrTime(message.SourceTime)), formatTime(ptrTime(message.ObservedAt)), + message.SourceVersion, message.PayloadHash); err != nil { + return rollback(fmt.Errorf("write message: %w", err)) + } + } + if _, err := tx.ExecContext(ctx, ` + INSERT INTO sync_state (stream_key, source_generation, confirmed_sequence, confirmed_cursor, coverage_state, last_success_at) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(stream_key) DO UPDATE SET + source_generation = excluded.source_generation, + confirmed_sequence = excluded.confirmed_sequence, + confirmed_cursor = excluded.confirmed_cursor, + coverage_state = excluded.coverage_state, + last_success_at = excluded.last_success_at, + error_code = NULL, + error_message = NULL, + updated_at = excluded.last_success_at`, batch.StreamKey, batch.SourceGeneration, + batch.Sequence, batch.CursorEnd, defaultCoverage(batch.CoverageState), now); err != nil { + return rollback(fmt.Errorf("write sync state: %w", err)) + } + if _, err := tx.ExecContext(ctx, ` + UPDATE ingest_batches SET status = 'confirmed', confirmed_at = ? WHERE batch_id = ?`, now, batch.BatchID); err != nil { + return rollback(fmt.Errorf("confirm ingest batch: %w", err)) + } + if err := tx.Commit(); err != nil { + return BatchApplyResult{}, fmt.Errorf("commit ingest batch: %w", err) + } + return BatchApplyResult{ConfirmedSequence: batch.Sequence, ConfirmedCursor: batch.CursorEnd}, nil +} + +func (m *AccountStoreManager) activeScopes(ctx context.Context, accountID string) ([]ReportingScope, string, error) { + m.mu.Lock() + defer m.mu.Unlock() + if m.closed { + return nil, "", errors.New("account store manager is closed") + } + var verified int + var generation string + var expires sql.NullString + if err := m.catalog.QueryRowContext(ctx, `SELECT verified, source_generation, authorization_expires_at FROM accounts WHERE account_id = ?`, accountID).Scan(&verified, &generation, &expires); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, "", ErrAccountStoreNotFound + } + return nil, "", fmt.Errorf("read account authorization: %w", err) + } + if verified == 0 || (expires.Valid && strings.TrimSpace(expires.String) != "" && parseTime(expires.String).Before(time.Now().UTC())) { + return nil, "", ErrAccountNotAuthorized + } + rows, err := m.catalog.QueryContext(ctx, `SELECT chat_id, data_type, expires_at, config_version FROM reporting_scopes WHERE account_id = ?`, accountID) + if err != nil { + return nil, "", fmt.Errorf("read reporting scopes: %w", err) + } + defer rows.Close() + var scopes []ReportingScope + for rows.Next() { + var scope ReportingScope + var expiresAt sql.NullString + if err := rows.Scan(&scope.ChatID, &scope.DataType, &expiresAt, &scope.ConfigVersion); err != nil { + return nil, "", fmt.Errorf("scan reporting scope: %w", err) + } + if expiresAt.Valid && strings.TrimSpace(expiresAt.String) != "" { + expiry := parseTime(expiresAt.String) + scope.ExpiresAt = &expiry + } + if scope.ExpiresAt == nil || scope.ExpiresAt.After(time.Now().UTC()) { + scopes = append(scopes, scope) + } + } + if err := rows.Err(); err != nil { + return nil, "", fmt.Errorf("read reporting scopes: %w", err) + } + return scopes, generation, nil +} + +func (s *AccountStore) QueryConversations(ctx context.Context, limit, offset int) ([]ConversationRecord, error) { + if limit < 1 || limit > 200 || offset < 0 { + return nil, errors.New("invalid conversation pagination") + } + scopes, _, err := s.manager.activeScopes(ctx, s.accountID) + if err != nil { + return nil, err + } + if len(scopes) == 0 { + return nil, ErrAccountNotAuthorized + } + rows, err := s.db.QueryContext(ctx, `SELECT chat_id, chat_type, title, last_activity_at, source, observed_at, directory_state FROM conversations ORDER BY COALESCE(last_activity_at, observed_at) DESC, chat_id`) + if err != nil { + return nil, fmt.Errorf("query conversations: %w", err) + } + defer rows.Close() + var records []ConversationRecord + for rows.Next() { + var item ConversationRecord + var lastActivity, observed string + if err := rows.Scan(&item.ChatID, &item.ChatType, &item.Title, &lastActivity, &item.Source, &observed, &item.DirectoryState); err != nil { + return nil, err + } + if lastActivity != "" { + value := parseTime(lastActivity) + item.LastActivityAt = &value + } + item.ObservedAt = parseTime(observed) + if scopeAllows(scopes, item.ChatID, "conversations") { + records = append(records, item) + } + } + if err := rows.Err(); err != nil { + return nil, err + } + if offset >= len(records) { + return []ConversationRecord{}, nil + } + end := offset + limit + if end > len(records) { + end = len(records) + } + return records[offset:end], nil +} + +type AccountSyncStatus struct { + StreamKey string + SourceGeneration string + ConfirmedSequence int64 + ConfirmedCursor string + CoverageState string + LastSuccessAt *time.Time + ErrorCode string + ErrorMessage string +} + +func (s *AccountStore) AuthorizeChat(ctx context.Context, chatID, dataType string) (bool, error) { + scopes, _, err := s.manager.activeScopes(ctx, s.accountID) + if err != nil { + return false, err + } + return scopeAllows(scopes, chatID, dataType), nil +} + +func (s *AccountStore) ensureCapacity(batch IngestBatch) error { + estimated := estimateBatchBytes(batch) + if s.maxBatchBytes > 0 && estimated > s.maxBatchBytes { + return fmt.Errorf("%w: batch exceeds byte budget", ErrAccountCapacityExceeded) + } + if s.maxShardBytes <= 0 { + return nil + } + current, err := accountStorageBytes(s.path) + if err != nil { + return fmt.Errorf("check account capacity: %w", err) + } + if current+estimated > s.maxShardBytes { + return fmt.Errorf("%w: shard exceeds byte budget", ErrAccountCapacityExceeded) + } + return nil +} + +func (s *AccountStore) GetSyncStatus(ctx context.Context, streamKey string) (AccountSyncStatus, error) { + if strings.TrimSpace(streamKey) == "" { + return AccountSyncStatus{}, errors.New("sync stream key is required") + } + var status AccountSyncStatus + var lastSuccess, errorCode, errorMessage sql.NullString + err := s.db.QueryRowContext(ctx, `SELECT stream_key, source_generation, confirmed_sequence, confirmed_cursor, coverage_state, last_success_at, error_code, error_message FROM sync_state WHERE stream_key = ?`, streamKey). + Scan(&status.StreamKey, &status.SourceGeneration, &status.ConfirmedSequence, &status.ConfirmedCursor, &status.CoverageState, &lastSuccess, &errorCode, &errorMessage) + if errors.Is(err, sql.ErrNoRows) { + return AccountSyncStatus{StreamKey: streamKey, CoverageState: "unknown"}, nil + } + if err != nil { + return AccountSyncStatus{}, fmt.Errorf("query sync status: %w", err) + } + if lastSuccess.Valid && strings.TrimSpace(lastSuccess.String) != "" { + value := parseTime(lastSuccess.String) + status.LastSuccessAt = &value + } + if errorCode.Valid { + status.ErrorCode = errorCode.String + } + if errorMessage.Valid { + status.ErrorMessage = errorMessage.String + } + return status, nil +} + +func (s *AccountStore) QueryMessages(ctx context.Context, chatID string, limit, offset int) ([]MessageRecord, error) { + if strings.TrimSpace(chatID) == "" || limit < 1 || limit > 200 || offset < 0 { + return nil, errors.New("invalid message query") + } + allowed, err := s.AuthorizeChat(ctx, chatID, "messages") + if err != nil { + return nil, err + } + if !allowed { + return nil, ErrAccountNotAuthorized + } + rows, err := s.db.QueryContext(ctx, `SELECT message_id, chat_id, source_message_id, direction, message_type, text, source_time, observed_at, source_version, payload_hash FROM messages WHERE chat_id = ? ORDER BY source_time DESC, message_id DESC LIMIT ? OFFSET ?`, chatID, limit, offset) + if err != nil { + return nil, fmt.Errorf("query messages: %w", err) + } + defer rows.Close() + var records []MessageRecord + for rows.Next() { + var item MessageRecord + var sourceTime, observed string + if err := rows.Scan(&item.MessageID, &item.ChatID, &item.SourceMessageID, &item.Direction, &item.MessageType, &item.Text, &sourceTime, &observed, &item.SourceVersion, &item.PayloadHash); err != nil { + return nil, err + } + item.SourceTime = parseTime(sourceTime) + item.ObservedAt = parseTime(observed) + records = append(records, item) + } + return records, rows.Err() +} + +func (m *AccountStoreManager) SetScopes(ctx context.Context, accountID string, scopes []ReportingScope) error { + m.mu.Lock() + defer m.mu.Unlock() + if m.closed { + return errors.New("account store manager is closed") + } + tx, err := m.catalog.BeginTx(ctx, nil) + if err != nil { + return err + } + if _, err := tx.ExecContext(ctx, `DELETE FROM reporting_scopes WHERE account_id = ?`, accountID); err != nil { + _ = tx.Rollback() + return err + } + for _, scope := range scopes { + if err := validateScope(scope); err != nil { + _ = tx.Rollback() + return err + } + if _, err := tx.ExecContext(ctx, `INSERT INTO reporting_scopes (account_id, chat_id, data_type, expires_at, config_version) VALUES (?, ?, ?, ?, ?)`, accountID, scope.ChatID, scope.DataType, nullableTimeArg(scope.ExpiresAt), scope.ConfigVersion); err != nil { + _ = tx.Rollback() + return err + } + } + return tx.Commit() +} + +func validateAccountSchema(ctx context.Context, db *sql.DB) error { + var version string + if err := db.QueryRowContext(ctx, `SELECT value FROM schema_meta WHERE key = 'schema_version'`).Scan(&version); err != nil { + return fmt.Errorf("read account schema version: %w", err) + } + if version != accountSchemaVersion { + return fmt.Errorf("unsupported account schema version %q", version) + } + return nil +} + +func ensureAccountIndexes(ctx context.Context, db *sql.DB) error { + if _, err := db.ExecContext(ctx, ` + CREATE INDEX IF NOT EXISTS idx_messages_chat_time ON messages(chat_id, source_time DESC, message_id DESC); + CREATE INDEX IF NOT EXISTS idx_messages_observed_at ON messages(observed_at); + CREATE INDEX IF NOT EXISTS idx_conversations_activity ON conversations(last_activity_at DESC, observed_at DESC, chat_id); + `); err != nil { + return fmt.Errorf("ensure account indexes: %w", err) + } + return nil +} + +func createAccountShard(path string) error { + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + return fmt.Errorf("create account shard directory: %w", err) + } + db, err := openSQLite(path) + if err != nil { + return err + } + defer db.Close() + _, err = db.Exec(` + CREATE TABLE IF NOT EXISTS schema_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL); + CREATE TABLE IF NOT EXISTS conversations ( + chat_id TEXT PRIMARY KEY, + chat_type TEXT NOT NULL, + title TEXT NOT NULL, + last_activity_at TEXT, + source TEXT NOT NULL, + observed_at TEXT NOT NULL, + directory_state TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS messages ( + message_id TEXT PRIMARY KEY, + chat_id TEXT NOT NULL, + source_message_id TEXT NOT NULL, + direction TEXT NOT NULL, + message_type TEXT NOT NULL, + text TEXT NOT NULL, + source_time TEXT NOT NULL, + observed_at TEXT NOT NULL, + source_version TEXT NOT NULL, + payload_hash TEXT NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_messages_chat_time ON messages(chat_id, source_time DESC, message_id DESC); + CREATE INDEX IF NOT EXISTS idx_messages_observed_at ON messages(observed_at); + CREATE INDEX IF NOT EXISTS idx_conversations_activity ON conversations(last_activity_at DESC, observed_at DESC, chat_id); + CREATE TABLE IF NOT EXISTS sync_state ( + stream_key TEXT PRIMARY KEY, + source_generation TEXT NOT NULL, + confirmed_sequence INTEGER NOT NULL DEFAULT 0, + confirmed_cursor TEXT NOT NULL DEFAULT '', + coverage_state TEXT NOT NULL DEFAULT 'unknown', + last_success_at TEXT, + error_code TEXT, + error_message TEXT, + updated_at TEXT NOT NULL DEFAULT '' + ); + CREATE TABLE IF NOT EXISTS ingest_batches ( + batch_id TEXT PRIMARY KEY, + source_generation TEXT NOT NULL, + stream_key TEXT NOT NULL, + sequence INTEGER NOT NULL, + cursor_start TEXT NOT NULL, + cursor_end TEXT NOT NULL, + payload_hash TEXT NOT NULL, + status TEXT NOT NULL, + received_at TEXT NOT NULL, + confirmed_at TEXT + ); + INSERT INTO schema_meta(key, value) VALUES ('schema_version', '1') + ON CONFLICT(key) DO UPDATE SET value = excluded.value; + `) + if err != nil { + return fmt.Errorf("initialize account shard: %w", err) + } + return nil +} + +func openSQLite(path string) (*sql.DB, error) { + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + return nil, fmt.Errorf("create sqlite parent directory: %w", err) + } + db, err := sql.Open("sqlite", path) + if err != nil { + return nil, fmt.Errorf("open sqlite database: %w", err) + } + db.SetMaxOpenConns(1) + db.SetMaxIdleConns(1) + for _, pragma := range []string{ + "PRAGMA busy_timeout = 5000", + "PRAGMA journal_mode = WAL", + "PRAGMA synchronous = FULL", + "PRAGMA foreign_keys = ON", + } { + if _, err := db.Exec(pragma); err != nil { + _ = db.Close() + return nil, fmt.Errorf("configure sqlite: %w", err) + } + } + if err := os.Chmod(path, 0o600); err != nil { + _ = db.Close() + return nil, fmt.Errorf("protect sqlite database: %w", err) + } + return db, nil +} + +func validateRegistration(registration AccountRegistration) error { + for name, value := range map[string]string{ + "account id": registration.AccountID, + "stable identity": registration.StableIdentity, + "source node id": registration.SourceNodeID, + "source generation": registration.SourceGeneration, + } { + if strings.TrimSpace(value) == "" { + return fmt.Errorf("%s is required", name) + } + } + if registration.AuthorizationVersion < 0 { + return errors.New("authorization version must be non-negative") + } + return nil +} + +func validateScope(scope ReportingScope) error { + if strings.TrimSpace(scope.ChatID) == "" || strings.TrimSpace(scope.DataType) == "" { + return errors.New("reporting scope chat id and data type are required") + } + if scope.ConfigVersion < 0 { + return errors.New("scope config version must be non-negative") + } + return nil +} + +func validateBatch(batch IngestBatch) error { + if strings.TrimSpace(batch.BatchID) == "" || strings.TrimSpace(batch.SourceGeneration) == "" || strings.TrimSpace(batch.StreamKey) == "" || batch.Sequence < 1 || strings.TrimSpace(batch.PayloadHash) == "" { + return errors.New("batch id, source generation, stream key, positive sequence, and payload hash are required") + } + for _, conversation := range batch.Conversations { + if strings.TrimSpace(conversation.ChatID) == "" || strings.TrimSpace(conversation.Source) == "" || conversation.ObservedAt.IsZero() { + return errors.New("conversation identity and observation time are required") + } + } + for _, message := range batch.Messages { + if strings.TrimSpace(message.MessageID) == "" || strings.TrimSpace(message.ChatID) == "" || strings.TrimSpace(message.PayloadHash) == "" || message.SourceTime.IsZero() || message.ObservedAt.IsZero() { + return errors.New("message identity, payload hash, source time, and observation time are required") + } + } + return nil +} + +func authorizeBatch(batch IngestBatch, scopes []ReportingScope) error { + if len(scopes) == 0 && (len(batch.Conversations) > 0 || len(batch.Messages) > 0) { + return ErrAccountNotAuthorized + } + for _, conversation := range batch.Conversations { + if !scopeAllows(scopes, conversation.ChatID, "conversations") { + return ErrAccountNotAuthorized + } + } + for _, message := range batch.Messages { + if !scopeAllows(scopes, message.ChatID, "messages") { + return ErrAccountNotAuthorized + } + } + return nil +} + +func scopeAllows(scopes []ReportingScope, chatID, dataType string) bool { + now := time.Now().UTC() + for _, scope := range scopes { + if scope.ChatID != chatID || (scope.ExpiresAt != nil && !scope.ExpiresAt.After(now)) { + continue + } + if scope.DataType == "*" || scope.DataType == "read" || scope.DataType == dataType { + return true + } + } + return false +} + +func accountShardPath(root, accountID string) string { + digest := sha256.Sum256([]byte(accountID)) + return filepath.Join(root, "accounts", "account-"+hex.EncodeToString(digest[:16]), "data.sqlite") +} + +func defaultCoverage(value string) string { + if value == "" { + return "complete" + } + return value +} + +func boolInt(value bool) int { + if value { + return 1 + } + return 0 +} + +func ptrTime(value time.Time) *time.Time { return &value } + +func formatTime(value *time.Time) string { + if value == nil || value.IsZero() { + return "" + } + return value.UTC().Format(time.RFC3339Nano) +} + +func nullableTimeArg(value *time.Time) any { + if value == nil || value.IsZero() { + return nil + } + return formatTime(value) +} + +func accountStorageBytes(path string) (int64, error) { + var total int64 + for _, candidate := range []string{path, path + "-wal", path + "-shm"} { + info, err := os.Stat(candidate) + if errors.Is(err, os.ErrNotExist) { + continue + } + if err != nil { + return 0, err + } + total += info.Size() + } + return total, nil +} + +func estimateBatchBytes(batch IngestBatch) int64 { + var total int64 = 256 + for _, item := range batch.Conversations { + total += int64(len(item.ChatID) + len(item.ChatType) + len(item.Title) + len(item.Source) + len(item.DirectoryState) + 128) + } + for _, item := range batch.Messages { + total += int64(len(item.MessageID) + len(item.ChatID) + len(item.SourceMessageID) + len(item.Direction) + len(item.MessageType) + len(item.Text) + len(item.SourceVersion) + len(item.PayloadHash) + 192) + } + return total +} + +func parseTime(value string) time.Time { + if value == "" { + return time.Time{} + } + parsed, err := time.Parse(time.RFC3339Nano, value) + if err != nil { + return time.Time{} + } + return parsed +} diff --git a/control-plane/account_store_test.go b/control-plane/account_store_test.go new file mode 100644 index 0000000..3d75e3b --- /dev/null +++ b/control-plane/account_store_test.go @@ -0,0 +1,386 @@ +package controlplane + +import ( + "context" + "database/sql" + "errors" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + "sync" + "testing" + "time" + + _ "modernc.org/sqlite" +) + +func TestAccountStoreIsolatesAccountsAndAppliesIdempotentBatches(t *testing.T) { + ctx := context.Background() + root := t.TempDir() + manager, err := OpenAccountStoreManager(root) + if err != nil { + t.Fatal(err) + } + defer manager.Close() + + scopesA := []ReportingScope{ + {ChatID: "chat-a", DataType: "read", ConfigVersion: 1}, + } + accountA, err := manager.RegisterAccount(ctx, AccountRegistration{ + AccountID: "account-a", StableIdentity: "wechat-a", SourceNodeID: "node-a", SourceGeneration: "generation-a", Verified: true, AuthorizationVersion: 1, ReportingScopes: scopesA, + }) + if err != nil { + t.Fatal(err) + } + defer accountA.Close() + accountB, err := manager.RegisterAccount(ctx, AccountRegistration{ + AccountID: "account-b", StableIdentity: "wechat-b", SourceNodeID: "node-b", SourceGeneration: "generation-b", Verified: true, AuthorizationVersion: 1, + ReportingScopes: []ReportingScope{{ChatID: "chat-b", DataType: "read", ConfigVersion: 1}}, + }) + if err != nil { + t.Fatal(err) + } + defer accountB.Close() + if accountA.Path() == accountB.Path() { + t.Fatal("accounts share a shard path") + } + if _, err := os.Stat(filepath.Join(root, "catalog.sqlite")); err != nil { + t.Fatal(err) + } + + now := time.Now().UTC().Truncate(time.Microsecond) + batch := IngestBatch{ + BatchID: "batch-1", SourceGeneration: "generation-a", StreamKey: "messages", Sequence: 1, + CursorStart: "0", CursorEnd: "1", PayloadHash: "hash-1", CoverageState: "complete", + Conversations: []ConversationRecord{{ChatID: "chat-a", ChatType: "private", Title: "A", Source: "db", ObservedAt: now, DirectoryState: "active"}}, + Messages: []MessageRecord{{MessageID: "message-1", ChatID: "chat-a", SourceMessageID: "source-1", Direction: "incoming", MessageType: "text", Text: "hello", SourceTime: now, ObservedAt: now, SourceVersion: "wx-1", PayloadHash: "message-hash"}}, + } + result, err := accountA.ApplyBatch(ctx, batch) + if err != nil { + t.Fatal(err) + } + if result.Duplicate || result.ConfirmedSequence != 1 { + t.Fatalf("unexpected first apply result: %+v", result) + } + duplicate, err := accountA.ApplyBatch(ctx, batch) + if err != nil { + t.Fatal(err) + } + if !duplicate.Duplicate || duplicate.ConfirmedSequence != 1 { + t.Fatalf("unexpected duplicate result: %+v", duplicate) + } + conflicting := batch + conflicting.PayloadHash = "different" + if _, err := accountA.ApplyBatch(ctx, conflicting); !errors.Is(err, ErrBatchConflict) { + t.Fatalf("expected batch conflict, got %v", err) + } + gap := batch + gap.BatchID = "batch-3" + gap.Sequence = 3 + gap.PayloadHash = "hash-3" + if _, err := accountA.ApplyBatch(ctx, gap); !errors.Is(err, ErrBatchSequenceGap) { + t.Fatalf("expected sequence gap, got %v", err) + } + unauthorized := batch + unauthorized.BatchID = "batch-2" + unauthorized.Sequence = 2 + unauthorized.PayloadHash = "hash-2" + unauthorized.Messages = []MessageRecord{{MessageID: "message-b", ChatID: "chat-b", SourceMessageID: "source-b", Direction: "incoming", MessageType: "text", Text: "no", SourceTime: now, ObservedAt: now, SourceVersion: "wx-1", PayloadHash: "message-b-hash"}} + if _, err := accountA.ApplyBatch(ctx, unauthorized); !errors.Is(err, ErrAccountNotAuthorized) { + t.Fatalf("expected unauthorized batch, got %v", err) + } + + messages, err := accountA.QueryMessages(ctx, "chat-a", 20, 0) + if err != nil || len(messages) != 1 || messages[0].Text != "hello" { + t.Fatalf("unexpected stored messages: %v %+v", err, messages) + } + conversations, err := accountA.QueryConversations(ctx, 20, 0) + if err != nil || len(conversations) != 1 || conversations[0].ChatID != "chat-a" { + t.Fatalf("unexpected stored conversations: %v %+v", err, conversations) + } +} + +func TestAccountStoreBackupRestoreAndSchemaFailureAreSafe(t *testing.T) { + ctx := context.Background() + root := t.TempDir() + manager, err := OpenAccountStoreManager(root) + if err != nil { + t.Fatal(err) + } + store, err := manager.RegisterAccount(ctx, AccountRegistration{ + AccountID: "account-a", StableIdentity: "wechat-a", SourceNodeID: "node-a", SourceGeneration: "generation-a", Verified: true, + ReportingScopes: []ReportingScope{{ChatID: "chat-a", DataType: "messages", ConfigVersion: 1}}, + }) + if err != nil { + t.Fatal(err) + } + now := time.Now().UTC() + batch1 := IngestBatch{ + BatchID: "batch-1", SourceGeneration: "generation-a", StreamKey: "messages", Sequence: 1, CursorStart: "0", CursorEnd: "1", PayloadHash: "hash-1", CoverageState: "complete", + Messages: []MessageRecord{{MessageID: "message-1", ChatID: "chat-a", SourceMessageID: "source-1", Direction: "incoming", MessageType: "text", Text: "one", SourceTime: now, ObservedAt: now, SourceVersion: "wx", PayloadHash: "message-hash-1"}}, + } + if _, err := store.ApplyBatch(ctx, batch1); err != nil { + t.Fatal(err) + } + backup := filepath.Join(root, "backups", "account-a.sqlite") + if err := store.Backup(ctx, backup); err != nil { + t.Fatal(err) + } + if err := store.Close(); err != nil { + t.Fatal(err) + } + if err := manager.RestoreAccount(ctx, "account-a", backup); err != nil { + t.Fatal(err) + } + restored, err := manager.OpenAccount(ctx, "account-a") + if err != nil { + t.Fatal(err) + } + batch2 := batch1 + batch2.BatchID = "batch-2" + batch2.Sequence = 2 + batch2.CursorStart = "1" + batch2.CursorEnd = "2" + batch2.PayloadHash = "hash-2" + batch2.Messages = []MessageRecord{{MessageID: "message-2", ChatID: "chat-a", SourceMessageID: "source-2", Direction: "incoming", MessageType: "text", Text: "two", SourceTime: now.Add(time.Second), ObservedAt: now.Add(time.Second), SourceVersion: "wx", PayloadHash: "message-hash-2"}} + if result, err := restored.ApplyBatch(ctx, batch2); err != nil || result.ConfirmedSequence != 2 { + t.Fatalf("cursor did not continue after restore: %+v %v", result, err) + } + if err := restored.Close(); err != nil { + t.Fatal(err) + } + + corruptBackup := filepath.Join(root, "backups", "corrupt.sqlite") + if err := copyFile(backup, corruptBackup); err != nil { + t.Fatal(err) + } + corruptDB, err := sql.Open("sqlite", corruptBackup) + if err != nil { + t.Fatal(err) + } + if _, err := corruptDB.Exec(`UPDATE schema_meta SET value = '999' WHERE key = 'schema_version'`); err != nil { + t.Fatal(err) + } + if err := corruptDB.Close(); err != nil { + t.Fatal(err) + } + if err := manager.RestoreAccount(ctx, "account-a", corruptBackup); err == nil { + t.Fatal("expected unsupported schema version") + } + if _, err := manager.OpenAccount(ctx, "account-a"); err != nil { + t.Fatalf("original shard should remain usable after failed restore: %v", err) + } +} + +func TestAccountStoreMaintenanceCapacityAndRevocation(t *testing.T) { + ctx := context.Background() + root := t.TempDir() + manager, err := OpenAccountStoreManager(root, AccountStoreManagerOptions{Retention: time.Hour, MaxBatchBytes: 1024}) + if err != nil { + t.Fatal(err) + } + defer manager.Close() + store, err := manager.RegisterAccount(ctx, AccountRegistration{ + AccountID: "account-a", StableIdentity: "wechat-a", SourceNodeID: "node-a", SourceGeneration: "generation-a", Verified: true, + ReportingScopes: []ReportingScope{{ChatID: "chat-a", DataType: "read", ConfigVersion: 1}}, + }) + if err != nil { + t.Fatal(err) + } + old := time.Now().UTC().Add(-2 * time.Hour) + batch := IngestBatch{ + BatchID: "old-batch", SourceGeneration: "generation-a", StreamKey: "messages", Sequence: 1, PayloadHash: "old-hash", + Conversations: []ConversationRecord{{ChatID: "chat-a", ChatType: "private", Title: "old", Source: "db", ObservedAt: old, DirectoryState: "active"}}, + Messages: []MessageRecord{{MessageID: "old-message", ChatID: "chat-a", SourceMessageID: "old-source", Direction: "incoming", MessageType: "text", Text: "old", SourceTime: old, ObservedAt: old, SourceVersion: "wx", PayloadHash: "old-message-hash"}}, + } + if _, err := store.ApplyBatch(ctx, batch); err != nil { + t.Fatal(err) + } + report, err := manager.RunMaintenance(ctx, time.Now().UTC()) + if err != nil { + t.Fatal(err) + } + if report.DeletedMessages != 1 || report.DeletedConversations != 1 || report.Checkpointed != 1 { + t.Fatalf("unexpected maintenance report: %+v", report) + } + if _, err := store.QueryMessages(ctx, "chat-a", 20, 0); err != nil { + t.Fatal(err) + } + if err := manager.SetScopes(ctx, "account-a", nil); err != nil { + t.Fatal(err) + } + if _, err := store.QueryMessages(ctx, "chat-a", 20, 0); !errors.Is(err, ErrAccountNotAuthorized) { + t.Fatalf("expected revoked query to be rejected, got %v", err) + } + if err := manager.SetScopes(ctx, "account-a", []ReportingScope{{ChatID: "chat-a", DataType: "read", ConfigVersion: 2}}); err != nil { + t.Fatal(err) + } + + oversized := batch + oversized.BatchID = "oversized" + oversized.Sequence = 2 + oversized.PayloadHash = "oversized-hash" + oversized.Messages = []MessageRecord{{MessageID: "oversized-message", ChatID: "chat-a", SourceMessageID: "oversized-source", Direction: "incoming", MessageType: "text", Text: strings.Repeat("x", 2000), SourceTime: time.Now().UTC(), ObservedAt: time.Now().UTC(), SourceVersion: "wx", PayloadHash: "oversized-message-hash"}} + if _, err := store.ApplyBatch(ctx, oversized); !errors.Is(err, ErrAccountCapacityExceeded) { + t.Fatalf("expected oversized batch to be rejected by capacity budget, got %v", err) + } +} + +func TestAccountStoreSyntheticConcurrentReadWrite(t *testing.T) { + ctx := context.Background() + root := t.TempDir() + manager, err := OpenAccountStoreManager(root, AccountStoreManagerOptions{MaxShardBytes: 8 * 1024 * 1024, MaxBatchBytes: 256 * 1024}) + if err != nil { + t.Fatal(err) + } + defer manager.Close() + const accountCount = 8 + const batchesPerAccount = 5 + const messagesPerBatch = 20 + for accountIndex := 0; accountIndex < accountCount; accountIndex++ { + accountID := fmt.Sprintf("account-%02d", accountIndex) + chatID := fmt.Sprintf("chat-%02d", accountIndex) + store, err := manager.RegisterAccount(ctx, AccountRegistration{ + AccountID: accountID, StableIdentity: accountID, SourceNodeID: "pressure-node", SourceGeneration: "pressure-generation", Verified: true, + ReportingScopes: []ReportingScope{{ChatID: chatID, DataType: "read", ConfigVersion: 1}}, + }) + if err != nil { + t.Fatal(err) + } + for batchIndex := 0; batchIndex < batchesPerAccount; batchIndex++ { + messages := make([]MessageRecord, 0, messagesPerBatch) + for messageIndex := 0; messageIndex < messagesPerBatch; messageIndex++ { + messages = append(messages, MessageRecord{MessageID: fmt.Sprintf("%s-%d-%d", accountID, batchIndex, messageIndex), ChatID: chatID, SourceMessageID: fmt.Sprintf("source-%d-%d", batchIndex, messageIndex), Direction: "incoming", MessageType: "text", Text: "synthetic", SourceTime: time.Now().UTC(), ObservedAt: time.Now().UTC(), SourceVersion: "pressure", PayloadHash: fmt.Sprintf("hash-%d-%d", batchIndex, messageIndex)}) + } + if _, err := store.ApplyBatch(ctx, IngestBatch{BatchID: fmt.Sprintf("%s-batch-%d", accountID, batchIndex), SourceGeneration: "pressure-generation", StreamKey: "messages", Sequence: int64(batchIndex + 1), PayloadHash: fmt.Sprintf("batch-hash-%s-%d", accountID, batchIndex), Messages: messages}); err != nil { + t.Fatal(err) + } + } + if err := store.Close(); err != nil { + t.Fatal(err) + } + } + start := time.Now() + var wg sync.WaitGroup + var mu sync.Mutex + var slowest time.Duration + var rowsRead int + for accountIndex := 0; accountIndex < accountCount; accountIndex++ { + accountID := fmt.Sprintf("account-%02d", accountIndex) + chatID := fmt.Sprintf("chat-%02d", accountIndex) + wg.Add(1) + go func() { + defer wg.Done() + store, err := manager.OpenAccount(ctx, accountID) + if err != nil { + t.Errorf("open %s: %v", accountID, err) + return + } + defer store.Close() + localStart := time.Now() + for i := 0; i < 20; i++ { + items, err := store.QueryMessages(ctx, chatID, 100, 0) + if err != nil { + t.Errorf("query %s: %v", accountID, err) + return + } + mu.Lock() + rowsRead += len(items) + mu.Unlock() + } + mu.Lock() + if elapsed := time.Since(localStart); elapsed > slowest { + slowest = elapsed + } + mu.Unlock() + }() + } + wg.Wait() + t.Logf("synthetic accounts=%d messages=%d reads=%d elapsed=%s slowest-account=%s", accountCount, accountCount*batchesPerAccount*messagesPerBatch, rowsRead, time.Since(start), slowest) + if rowsRead != accountCount*batchesPerAccount*messagesPerBatch*20 { + t.Fatalf("unexpected synthetic read count: %d", rowsRead) + } +} + +func TestAccountStoreAddsIndexesToExistingShard(t *testing.T) { + ctx := context.Background() + root := t.TempDir() + manager, err := OpenAccountStoreManager(root) + if err != nil { + t.Fatal(err) + } + store, err := manager.RegisterAccount(ctx, AccountRegistration{AccountID: "account-a", StableIdentity: "wechat-a", SourceNodeID: "node-a", SourceGeneration: "generation-a", Verified: true, ReportingScopes: []ReportingScope{{ChatID: "chat-a", DataType: "read", ConfigVersion: 1}}}) + if err != nil { + t.Fatal(err) + } + if _, err := store.db.ExecContext(ctx, `DROP INDEX idx_messages_observed_at; DROP INDEX idx_conversations_activity;`); err != nil { + t.Fatal(err) + } + if err := store.Close(); err != nil { + t.Fatal(err) + } + if err := manager.Close(); err != nil { + t.Fatal(err) + } + manager, err = OpenAccountStoreManager(root) + if err != nil { + t.Fatal(err) + } + defer manager.Close() + store, err = manager.OpenAccount(ctx, "account-a") + if err != nil { + t.Fatal(err) + } + defer store.Close() + rows, err := store.db.QueryContext(ctx, `SELECT name FROM sqlite_master WHERE type = 'index' AND name IN ('idx_messages_observed_at', 'idx_conversations_activity') ORDER BY name`) + if err != nil { + t.Fatal(err) + } + defer rows.Close() + var names []string + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + t.Fatal(err) + } + names = append(names, name) + } + if err := rows.Err(); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(names, []string{"idx_conversations_activity", "idx_messages_observed_at"}) { + t.Fatalf("indexes were not restored on existing shard: %v", names) + } +} + +func TestAccountStoreRejectsExpiredAuthorization(t *testing.T) { + ctx := context.Background() + manager, err := OpenAccountStoreManager(t.TempDir()) + if err != nil { + t.Fatal(err) + } + defer manager.Close() + expired := time.Now().UTC().Add(-time.Minute) + store, err := manager.RegisterAccount(ctx, AccountRegistration{ + AccountID: "account-a", StableIdentity: "wechat-a", SourceNodeID: "node-a", SourceGeneration: "generation-a", Verified: true, + AuthorizationExpiresAt: &expired, ReportingScopes: []ReportingScope{{ChatID: "chat-a", DataType: "messages", ConfigVersion: 1}}, + }) + if err != nil { + t.Fatal(err) + } + defer store.Close() + _, err = store.ApplyBatch(ctx, IngestBatch{BatchID: "batch-1", SourceGeneration: "generation-a", StreamKey: "messages", Sequence: 1, PayloadHash: "hash"}) + if !errors.Is(err, ErrAccountNotAuthorized) { + t.Fatalf("expected expired authorization error, got %v", err) + } +} + +func copyFile(source, target string) error { + data, err := os.ReadFile(source) + if err != nil { + return err + } + return os.WriteFile(target, data, 0o600) +} diff --git a/control-plane/cmd/wxagent-control-plane/main.go b/control-plane/cmd/wxagent-control-plane/main.go index dfedac3..ab219ac 100644 --- a/control-plane/cmd/wxagent-control-plane/main.go +++ b/control-plane/cmd/wxagent-control-plane/main.go @@ -28,6 +28,10 @@ func main() { config.TaskRetention = readDurationEnv("WXAGENT_CONTROL_PLANE_TASK_RETENTION", config.TaskRetention) config.EventRetention = readDurationEnv("WXAGENT_CONTROL_PLANE_EVENT_RETENTION", config.EventRetention) config.AuditRetention = readDurationEnv("WXAGENT_CONTROL_PLANE_AUDIT_RETENTION", config.AuditRetention) + config.AccountRetention = readDurationEnv("WXAGENT_ACCOUNT_DATA_RETENTION", config.AccountRetention) + config.AccountMaxShardBytes = readInt64Env("WXAGENT_ACCOUNT_MAX_SHARD_BYTES", config.AccountMaxShardBytes) + config.AccountMaxBatchBytes = readInt64Env("WXAGENT_ACCOUNT_MAX_BATCH_BYTES", config.AccountMaxBatchBytes) + config.AccountMaintenanceInterval = readDurationEnv("WXAGENT_ACCOUNT_MAINTENANCE_INTERVAL", config.AccountMaintenanceInterval) config.TLSCertFile = os.Getenv("WXAGENT_CONTROL_PLANE_TLS_CERT_FILE") config.TLSKeyFile = os.Getenv("WXAGENT_CONTROL_PLANE_TLS_KEY_FILE") config.MTLSClientCAFile = os.Getenv("WXAGENT_MTLS_CLIENT_CA_FILE") @@ -119,6 +123,18 @@ func readIntEnv(name string, fallback int) int { return parsed } +func readInt64Env(name string, fallback int64) int64 { + value := strings.TrimSpace(os.Getenv(name)) + if value == "" { + return fallback + } + parsed, err := strconv.ParseInt(value, 10, 64) + if err != nil || parsed < 0 { + log.Fatalf("%s must be a non-negative integer", name) + } + return parsed +} + func readDurationEnv(name string, fallback time.Duration) time.Duration { value := strings.TrimSpace(os.Getenv(name)) if value == "" { diff --git a/control-plane/data_routes.go b/control-plane/data_routes.go new file mode 100644 index 0000000..dea3dbd --- /dev/null +++ b/control-plane/data_routes.go @@ -0,0 +1,421 @@ +package controlplane + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "strconv" + "time" +) + +const maxDataBatchBytes = 512 * 1024 + +type dataBatchRequest struct { + NodeID string `json:"node_id"` + AccountID string `json:"account_id"` + BatchID string `json:"batch_id"` + SourceGeneration string `json:"source_generation"` + StreamKey string `json:"stream_key"` + Sequence int64 `json:"sequence"` + CursorStart string `json:"cursor_start"` + CursorEnd string `json:"cursor_end"` + PayloadHash string `json:"payload_hash"` + CoverageState string `json:"coverage_state"` + Conversations []dataConversationRequest `json:"conversations"` + Messages []dataMessageRequest `json:"messages"` +} + +type dataConversationRequest struct { + ChatID string `json:"chat_id"` + ChatType ChatType `json:"chat_type"` + Title string `json:"title"` + LastActivityAt *time.Time `json:"last_activity_at"` + Source string `json:"source"` + ObservedAt time.Time `json:"observed_at"` + DirectoryState string `json:"directory_state"` +} + +type dataMessageRequest struct { + MessageID string `json:"message_id"` + ChatID string `json:"chat_id"` + ChatType ChatType `json:"chat_type"` + SourceMessageID string `json:"source_message_id"` + Direction string `json:"direction"` + MessageType string `json:"message_type"` + Text string `json:"text"` + SourceTime time.Time `json:"source_time"` + ObservedAt time.Time `json:"observed_at"` + SourceVersion string `json:"source_version"` + PayloadHash string `json:"payload_hash"` +} + +type dataBatchAck struct { + Accepted bool `json:"accepted"` + Duplicate bool `json:"duplicate"` + BatchID string `json:"batch_id"` + ConfirmedSequence int64 `json:"confirmed_sequence"` + ConfirmedCursor string `json:"confirmed_cursor"` + Reason string `json:"reason,omitempty"` +} + +type dataSyncView struct { + State string `json:"state"` + Source string `json:"source"` + LastSuccessAt *time.Time `json:"last_success_at,omitempty"` + ConfirmedSequence int64 `json:"confirmed_sequence"` + ConfirmedCursor string `json:"confirmed_cursor,omitempty"` + BacklogCount *int64 `json:"backlog_count"` + ErrorCode string `json:"error_code,omitempty"` + ErrorMessage string `json:"error_message,omitempty"` +} + +type dataConversationView struct { + ChatID string `json:"chat_id"` + ChatType string `json:"chat_type"` + Title string `json:"title"` + LastActivityAt *time.Time `json:"last_activity_at,omitempty"` + Source string `json:"source"` + ObservedAt time.Time `json:"observed_at"` + DirectoryState string `json:"directory_state"` +} + +type dataMessageView struct { + MessageID string `json:"message_id"` + ChatID string `json:"chat_id"` + SourceMessageID string `json:"source_message_id"` + Direction string `json:"direction"` + MessageType string `json:"message_type"` + Text string `json:"text"` + SourceTime time.Time `json:"source_time"` + ObservedAt time.Time `json:"observed_at"` + SourceVersion string `json:"source_version"` +} + +func (s *Server) registerDataAccounts(nodeID string, registration NodeRegistration) error { + for _, account := range registration.Accounts { + if !account.Verified { + continue + } + scopes := make([]ReportingScope, 0, len(account.AllowedChats)) + for _, chat := range account.AllowedChats { + dataType := "read" + if chat.ChatID == "" || !validChatType(chat.ChatType) { + continue + } + scopes = append(scopes, ReportingScope{ChatID: chat.ChatID, DataType: dataType, ConfigVersion: int(registration.ReportingConfigVersion)}) + } + store, err := s.accountStores.RegisterAccount(context.Background(), AccountRegistration{ + AccountID: account.AccountID, + StableIdentity: account.AccountID, + SourceNodeID: nodeID, + SourceGeneration: account.AccountID, + Verified: true, + AuthorizationVersion: int(registration.ReportingConfigVersion), + ReportingScopes: scopes, + }) + if err != nil { + return err + } + _ = store.Close() + } + return nil +} + +type nodeDataSyncStatusView struct { + State string `json:"state"` + SourceGeneration string `json:"source_generation"` + ConfirmedSequence int64 `json:"confirmed_sequence"` + ConfirmedCursor string `json:"confirmed_cursor"` + LastSuccessAt *time.Time `json:"last_success_at,omitempty"` +} + +func (s *Server) nodeDataRoute(w http.ResponseWriter, r *http.Request, nodeID string, parts []string, _ string) error { + if len(parts) != 3 || parts[0] != "accounts" || parts[2] != "sync-status" || r.Method != http.MethodGet || !validIdentifier(parts[1], 200) { + return requestError{status: http.StatusNotFound, code: "NotFound", message: "Resource was not found."} + } + accountID := parts[1] + var authorized bool + if err := s.store.Read(func(state PersistedState) error { + node, ok := state.Nodes[nodeID] + if !ok { + return requestError{status: http.StatusConflict, code: "NodeNotRegistered", message: "Register the node before reading sync status."} + } + authorized = nodeHasAccount(node, accountID) + return nil + }); err != nil { + return err + } + if !authorized { + return requestError{status: http.StatusForbidden, code: "AccountNotAuthorized", message: "The node is not authorized for this account."} + } + store, err := s.accountStores.OpenAccount(r.Context(), accountID) + if err != nil { + return requestError{status: http.StatusNotFound, code: "AccountDataNotFound", message: "No platform data is available for this account."} + } + defer store.Close() + status, err := store.GetSyncStatus(r.Context(), "messages") + if err != nil { + return requestError{status: http.StatusInternalServerError, code: "DataStatusFailed", message: "The platform sync status query failed."} + } + writeJSON(w, http.StatusOK, nodeDataSyncStatusView{State: status.CoverageState, SourceGeneration: status.SourceGeneration, ConfirmedSequence: status.ConfirmedSequence, ConfirmedCursor: status.ConfirmedCursor, LastSuccessAt: status.LastSuccessAt}) + return nil +} + +func (s *Server) ingestDataBatch(w http.ResponseWriter, r *http.Request, correlationID string) error { + nodeID, err := s.authenticateNode(r) + if err != nil { + return err + } + var request dataBatchRequest + if err := decodeJSON(r, &request, maxDataBatchBytes); err != nil { + return err + } + if request.NodeID != nodeID || request.AccountID == "" || request.BatchID == "" || request.SourceGeneration == "" || request.StreamKey != "messages" || request.Sequence < 1 || request.PayloadHash == "" || len(request.Messages) > 5000 || len(request.Conversations) > 1000 { + return requestError{status: http.StatusBadRequest, code: "InvalidDataBatch", message: "The data batch is invalid."} + } + var account AccountSummary + if err := s.store.Read(func(state PersistedState) error { + node, ok := state.Nodes[nodeID] + if !ok { + return requestError{status: http.StatusConflict, code: "NodeNotRegistered", message: "Register the node before sending data."} + } + for _, candidate := range node.Accounts { + if candidate.AccountID == request.AccountID { + account = candidate + return nil + } + } + return requestError{status: http.StatusForbidden, code: "AccountNotAuthorized", message: "The node is not authorized for this account."} + }); err != nil { + return err + } + if !account.Verified { + return requestError{status: http.StatusForbidden, code: "AccountNotVerified", message: "The account identity is not verified."} + } + store, err := s.accountStores.OpenAccount(r.Context(), request.AccountID) + if err != nil { + return requestError{status: http.StatusForbidden, code: "AccountDataUnavailable", message: "The account data store is not available."} + } + defer store.Close() + batch := IngestBatch{ + BatchID: request.BatchID, SourceGeneration: request.SourceGeneration, StreamKey: request.StreamKey, + Sequence: request.Sequence, CursorStart: request.CursorStart, CursorEnd: request.CursorEnd, + PayloadHash: request.PayloadHash, CoverageState: request.CoverageState, + } + for _, conversation := range request.Conversations { + if !validChatType(conversation.ChatType) || conversation.ObservedAt.IsZero() { + return requestError{status: http.StatusBadRequest, code: "InvalidDataBatch", message: "A conversation record is invalid."} + } + batch.Conversations = append(batch.Conversations, ConversationRecord{ + ChatID: conversation.ChatID, ChatType: string(conversation.ChatType), Title: conversation.Title, + LastActivityAt: conversation.LastActivityAt, Source: conversation.Source, + ObservedAt: conversation.ObservedAt, DirectoryState: conversation.DirectoryState, + }) + } + for _, message := range request.Messages { + if !validChatType(message.ChatType) || message.SourceTime.IsZero() || message.ObservedAt.IsZero() || message.MessageID == "" || message.ChatID == "" || message.PayloadHash == "" { + return requestError{status: http.StatusBadRequest, code: "InvalidDataBatch", message: "A message record is invalid."} + } + batch.Messages = append(batch.Messages, MessageRecord{ + MessageID: message.MessageID, ChatID: message.ChatID, SourceMessageID: message.SourceMessageID, + Direction: message.Direction, MessageType: message.MessageType, Text: message.Text, + SourceTime: message.SourceTime, ObservedAt: message.ObservedAt, SourceVersion: message.SourceVersion, + PayloadHash: message.PayloadHash, + }) + } + result, err := store.ApplyBatch(r.Context(), batch) + if err != nil { + switch { + case errors.Is(err, ErrAccountNotAuthorized): + return requestError{status: http.StatusForbidden, code: "DataNotAuthorized", message: "The data batch contains data outside the active reporting scopes."} + case errors.Is(err, ErrBatchConflict): + return requestError{status: http.StatusConflict, code: "BatchConflict", message: "The batch id or payload conflicts with an existing batch."} + case errors.Is(err, ErrBatchSequenceGap): + return requestError{status: http.StatusConflict, code: "BatchSequenceGap", message: "The batch sequence has a gap; resend from the confirmed cursor."} + case errors.Is(err, ErrBatchSequenceConflict): + return requestError{status: http.StatusConflict, code: "BatchSequenceConflict", message: "The batch sequence is older than confirmed progress."} + case errors.Is(err, ErrAccountBindingConflict): + return requestError{status: http.StatusConflict, code: "AccountBindingConflict", message: "The source generation does not match the verified account binding."} + case errors.Is(err, ErrAccountCapacityExceeded): + return requestError{status: http.StatusInsufficientStorage, code: "AccountCapacityExceeded", message: "The account data capacity budget has been reached."} + default: + return requestError{status: http.StatusInternalServerError, code: "DataBatchFailed", message: "The platform could not persist the data batch."} + } + } + writeJSON(w, http.StatusOK, dataBatchAck{Accepted: true, Duplicate: result.Duplicate, BatchID: request.BatchID, ConfirmedSequence: result.ConfirmedSequence, ConfirmedCursor: result.ConfirmedCursor, Reason: "Persisted"}) + return nil +} + +func validChatType(chatType ChatType) bool { + return chatType == ChatGroup || chatType == ChatPrivate +} + +func (s *Server) dataRoute(w http.ResponseWriter, r *http.Request, correlationID string) error { + username, err := s.authenticateWeb(r) + if err != nil { + return err + } + parts := pathParts(r.URL.Path) + if len(parts) < 5 || parts[0] != "v1" || parts[1] != "data" || parts[2] != "accounts" || !validIdentifier(parts[3], 200) { + return requestError{status: http.StatusNotFound, code: "NotFound", message: "Resource was not found."} + } + accountID := parts[3] + if len(parts) != 5 { + return requestError{status: http.StatusNotFound, code: "NotFound", message: "Resource was not found."} + } + switch parts[4] { + case "conversations": + if r.Method != http.MethodGet { + return requestError{status: http.StatusMethodNotAllowed, code: "MethodNotAllowed", message: "Method is not allowed."} + } + return s.listStoredConversations(w, r, accountID) + case "messages": + if r.Method != http.MethodGet { + return requestError{status: http.StatusMethodNotAllowed, code: "MethodNotAllowed", message: "Method is not allowed."} + } + return s.listStoredMessages(w, r, accountID) + case "sync-status": + if r.Method != http.MethodGet { + return requestError{status: http.StatusMethodNotAllowed, code: "MethodNotAllowed", message: "Method is not allowed."} + } + return s.storedSyncStatus(w, r, accountID) + case "refresh": + if r.Method != http.MethodPost { + return requestError{status: http.StatusMethodNotAllowed, code: "MethodNotAllowed", message: "Method is not allowed."} + } + return s.requestDataRefresh(w, r, accountID, username, correlationID) + case "revoke": + if r.Method != http.MethodPost { + return requestError{status: http.StatusMethodNotAllowed, code: "MethodNotAllowed", message: "Method is not allowed."} + } + return s.revokeDataAuthorization(w, r, accountID, username, correlationID) + default: + return requestError{status: http.StatusNotFound, code: "NotFound", message: "Resource was not found."} + } +} + +func (s *Server) listStoredConversations(w http.ResponseWriter, r *http.Request, accountID string) error { + limit, offset := queryLimit(r.URL.Query().Get("limit")), queryOffset(r.URL.Query().Get("offset")) + store, err := s.accountStores.OpenAccount(r.Context(), accountID) + if err != nil { + return requestError{status: http.StatusNotFound, code: "AccountDataNotFound", message: "No platform data is available for this account."} + } + defer store.Close() + items, err := store.QueryConversations(r.Context(), limit, offset) + if err != nil { + if errors.Is(err, ErrAccountNotAuthorized) { + return requestError{status: http.StatusForbidden, code: "DataNotAuthorized", message: "The account has no active conversation reporting scope."} + } + return requestError{status: http.StatusInternalServerError, code: "DataQueryFailed", message: "The platform data query failed."} + } + status, err := store.GetSyncStatus(r.Context(), "messages") + if err != nil { + return requestError{status: http.StatusInternalServerError, code: "DataStatusFailed", message: "The platform sync status query failed."} + } + views := make([]dataConversationView, 0, len(items)) + for _, item := range items { + views = append(views, dataConversationView{item.ChatID, item.ChatType, item.Title, item.LastActivityAt, item.Source, item.ObservedAt, item.DirectoryState}) + } + writeJSON(w, http.StatusOK, map[string]any{"items": views, "limit": limit, "offset": offset, "has_more": len(views) == limit, "next_offset": offset + len(views), "sync": syncView(status)}) + return nil +} + +func (s *Server) listStoredMessages(w http.ResponseWriter, r *http.Request, accountID string) error { + chatID := r.URL.Query().Get("chat_id") + if !validIdentifier(chatID, 512) { + return requestError{status: http.StatusBadRequest, code: "InvalidChat", message: "chat_id is required."} + } + limit, offset := queryLimit(r.URL.Query().Get("limit")), queryOffset(r.URL.Query().Get("offset")) + store, err := s.accountStores.OpenAccount(r.Context(), accountID) + if err != nil { + return requestError{status: http.StatusNotFound, code: "AccountDataNotFound", message: "No platform data is available for this account."} + } + defer store.Close() + allowed, err := store.AuthorizeChat(r.Context(), chatID, "messages") + if err != nil || !allowed { + return requestError{status: http.StatusForbidden, code: "DataNotAuthorized", message: "The chat is not in the active reporting scope."} + } + items, err := store.QueryMessages(r.Context(), chatID, limit, offset) + if err != nil { + return requestError{status: http.StatusInternalServerError, code: "DataQueryFailed", message: "The platform data query failed."} + } + status, err := store.GetSyncStatus(r.Context(), "messages") + if err != nil { + return requestError{status: http.StatusInternalServerError, code: "DataStatusFailed", message: "The platform sync status query failed."} + } + views := make([]dataMessageView, 0, len(items)) + for _, item := range items { + views = append(views, dataMessageView{item.MessageID, item.ChatID, item.SourceMessageID, item.Direction, item.MessageType, item.Text, item.SourceTime, item.ObservedAt, item.SourceVersion}) + } + writeJSON(w, http.StatusOK, map[string]any{"items": views, "limit": limit, "offset": offset, "has_more": len(views) == limit, "next_offset": offset + len(views), "sync": syncView(status)}) + return nil +} + +func (s *Server) storedSyncStatus(w http.ResponseWriter, r *http.Request, accountID string) error { + store, err := s.accountStores.OpenAccount(r.Context(), accountID) + if err != nil { + return requestError{status: http.StatusNotFound, code: "AccountDataNotFound", message: "No platform data is available for this account."} + } + defer store.Close() + status, err := store.GetSyncStatus(r.Context(), "messages") + if err != nil { + return requestError{status: http.StatusInternalServerError, code: "DataStatusFailed", message: "The platform sync status query failed."} + } + writeJSON(w, http.StatusOK, syncView(status)) + return nil +} + +func syncView(status AccountSyncStatus) dataSyncView { + return dataSyncView{State: status.CoverageState, Source: "platform-cache", LastSuccessAt: status.LastSuccessAt, ConfirmedSequence: status.ConfirmedSequence, ConfirmedCursor: status.ConfirmedCursor, BacklogCount: nil, ErrorCode: status.ErrorCode, ErrorMessage: status.ErrorMessage} +} + +func (s *Server) revokeDataAuthorization(w http.ResponseWriter, r *http.Request, accountID, username, correlationID string) error { + if err := s.accountStores.SetScopes(r.Context(), accountID, nil); err != nil { + if errors.Is(err, ErrAccountStoreNotFound) { + return requestError{status: http.StatusNotFound, code: "AccountDataNotFound", message: "No platform data is available for this account."} + } + return requestError{status: http.StatusInternalServerError, code: "AuthorizationRevokeFailed", message: "The account authorization could not be revoked."} + } + if err := s.appendAudit("user:"+username, "data.authorization.revoke", accountID, correlationID, "success"); err != nil { + return err + } + writeJSON(w, http.StatusOK, map[string]any{"account_id": accountID, "revoked": true}) + return nil +} + +func (s *Server) requestDataRefresh(w http.ResponseWriter, r *http.Request, accountID, username, correlationID string) error { + var nodeID string + if err := s.store.Read(func(state PersistedState) error { + for id, node := range state.Nodes { + for _, account := range node.Accounts { + if account.AccountID == accountID && account.Verified { + nodeID = id + break + } + } + if nodeID != "" { + break + } + } + return nil + }); err != nil { + return err + } + if nodeID == "" { + return requestError{status: http.StatusConflict, code: "NoAuthorizedNode", message: "No verified node is available for this account."} + } + payload := json.RawMessage(`{"stream_key":"messages","reason":"web-refresh"}`) + return s.createTaskSubmission(w, TaskSubmission{NodeID: nodeID, AccountID: accountID, Kind: "sync-data", IdempotencyKey: "sync:" + accountID + ":" + strconv.FormatInt(time.Now().UTC().Unix()/5, 10), Payload: payload}, username, correlationID) +} + +func queryOffset(raw string) int { + if raw == "" { + return 0 + } + value, err := strconv.Atoi(raw) + if err != nil || value < 0 || value > 10_000_000 { + return 0 + } + return value +} diff --git a/control-plane/data_routes_test.go b/control-plane/data_routes_test.go new file mode 100644 index 0000000..7ae4eb9 --- /dev/null +++ b/control-plane/data_routes_test.go @@ -0,0 +1,126 @@ +package controlplane + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "path/filepath" + "testing" + "time" +) + +func TestDataBatchRoundTripAndWebQueriesUseAccountShard(t *testing.T) { + server, err := NewServer(ServerConfig{ + DataFile: filepath.Join(t.TempDir(), "control-plane.json"), + NodeTokens: map[string]string{"node-a": "secret-a"}, + WebUsers: map[string]string{"admin": "web-secret"}, + HeartbeatTimeout: time.Minute, + LeaseTTL: time.Minute, + }) + if err != nil { + t.Fatal(err) + } + defer server.Close() + httpServer := httptest.NewServer(server.Handler()) + defer httpServer.Close() + client := httpServer.Client() + + register := doJSON(t, client, http.MethodPost, httpServer.URL+"/v1/nodes/register", "Bearer secret-a", NodeRegistration{ + NodeID: "node-a", ConnectionID: "connection-a", AgentVersion: "test", ProtocolVersion: ProtocolVersion, + Capabilities: []string{"heartbeat", "poll-tasks", "sync-data"}, + Accounts: []AccountSummary{{AccountID: "account-a", Active: true, Verified: true, AllowedChats: []AllowedChatSummary{{ChatID: "chat-a", ChatType: ChatPrivate}}}}, + }) + if register.Code != http.StatusOK { + t.Fatalf("register status = %d: %s", register.Code, register.Body.String()) + } + + login := doJSON(t, client, http.MethodPost, httpServer.URL+"/v1/auth/login", "", map[string]string{"username": "admin", "password": "web-secret"}) + var session struct { + AccessToken string `json:"access_token"` + } + decodeBody(t, login, &session) + webAuth := "Bearer " + session.AccessToken + + now := time.Now().UTC().Truncate(time.Millisecond) + batch := dataBatchRequest{ + NodeID: "node-a", AccountID: "account-a", BatchID: "batch-1", SourceGeneration: "account-a", StreamKey: "messages", Sequence: 1, + CursorStart: "{}", CursorEnd: `{"chat-a\u001fmessage/a.db":1}`, PayloadHash: "batch-hash", CoverageState: "complete", + Conversations: []dataConversationRequest{{ChatID: "chat-a", ChatType: ChatPrivate, Title: "测试会话", Source: "db", ObservedAt: now, DirectoryState: "observed"}}, + Messages: []dataMessageRequest{{MessageID: "chat-a:local:1", ChatID: "chat-a", ChatType: ChatPrivate, SourceMessageID: "1", Direction: "incoming", MessageType: "text", Text: "hello", SourceTime: now, ObservedAt: now, SourceVersion: "test", PayloadHash: "message-hash"}}, + } + posted := doJSON(t, client, http.MethodPost, httpServer.URL+"/v1/data/batches", "Bearer secret-a", batch) + if posted.Code != http.StatusOK { + t.Fatalf("batch status = %d: %s", posted.Code, posted.Body.String()) + } + var ack dataBatchAck + decodeBody(t, posted, &ack) + if !ack.Accepted || ack.Duplicate { + t.Fatalf("unexpected batch ack: %+v", ack) + } + + duplicate := doJSON(t, client, http.MethodPost, httpServer.URL+"/v1/data/batches", "Bearer secret-a", batch) + if duplicate.Code != http.StatusOK { + t.Fatalf("duplicate status = %d: %s", duplicate.Code, duplicate.Body.String()) + } + decodeBody(t, duplicate, &ack) + if !ack.Duplicate { + t.Fatalf("expected duplicate ack: %+v", ack) + } + + conversations := doJSON(t, client, http.MethodGet, httpServer.URL+"/v1/data/accounts/account-a/conversations?limit=20", webAuth, nil) + if conversations.Code != http.StatusOK { + t.Fatalf("conversation query status = %d: %s", conversations.Code, conversations.Body.String()) + } + var conversationBody struct { + Items []dataConversationView `json:"items"` + Sync dataSyncView `json:"sync"` + } + decodeBody(t, conversations, &conversationBody) + if len(conversationBody.Items) != 1 || conversationBody.Items[0].ChatID != "chat-a" || conversationBody.Sync.State != "complete" { + t.Fatalf("unexpected conversation body: %+v", conversationBody) + } + + messages := doJSON(t, client, http.MethodGet, httpServer.URL+"/v1/data/accounts/account-a/messages?chat_id=chat-a&limit=20", webAuth, nil) + if messages.Code != http.StatusOK { + t.Fatalf("message query status = %d: %s", messages.Code, messages.Body.String()) + } + var messageBody struct { + Items []dataMessageView `json:"items"` + } + decodeBody(t, messages, &messageBody) + if len(messageBody.Items) != 1 || messageBody.Items[0].Text != "hello" { + t.Fatalf("unexpected message body: %+v", messageBody) + } + + refresh := doJSON(t, client, http.MethodPost, httpServer.URL+"/v1/data/accounts/account-a/refresh", webAuth, nil) + if refresh.Code != http.StatusAccepted { + t.Fatalf("refresh status = %d: %s", refresh.Code, refresh.Body.String()) + } +} + +func TestDataBatchCannotCrossReportingScope(t *testing.T) { + server, err := NewServer(ServerConfig{ + DataFile: filepath.Join(t.TempDir(), "control-plane.json"), NodeTokens: map[string]string{"node-a": "secret-a"}, WebUsers: map[string]string{"admin": "web-secret"}, + HeartbeatTimeout: time.Minute, LeaseTTL: time.Minute, + }) + if err != nil { + t.Fatal(err) + } + defer server.Close() + httpServer := httptest.NewServer(server.Handler()) + defer httpServer.Close() + client := httpServer.Client() + register := NodeRegistration{NodeID: "node-a", ConnectionID: "connection-a", AgentVersion: "test", ProtocolVersion: ProtocolVersion, Accounts: []AccountSummary{{AccountID: "account-a", Verified: true, AllowedChats: []AllowedChatSummary{{ChatID: "chat-a", ChatType: ChatPrivate}}}}} + if response := doJSON(t, client, http.MethodPost, httpServer.URL+"/v1/nodes/register", "Bearer secret-a", register); response.Code != http.StatusOK { + t.Fatalf("register: %d %s", response.Code, response.Body.String()) + } + body := dataBatchRequest{NodeID: "node-a", AccountID: "account-a", BatchID: "denied", SourceGeneration: "account-a", StreamKey: "messages", Sequence: 1, PayloadHash: "hash", CursorStart: "{}", CursorEnd: "{}", Messages: []dataMessageRequest{{MessageID: "denied", ChatID: "chat-b", ChatType: ChatPrivate, SourceTime: time.Now().UTC(), ObservedAt: time.Now().UTC(), PayloadHash: "message"}}} + response := doJSON(t, client, http.MethodPost, httpServer.URL+"/v1/data/batches", "Bearer secret-a", body) + if response.Code != http.StatusForbidden { + t.Fatalf("denied status = %d: %s", response.Code, response.Body.String()) + } + var payload map[string]any + if err := json.Unmarshal([]byte(response.Body.String()), &payload); err != nil { + t.Fatal(err) + } +} diff --git a/control-plane/go.mod b/control-plane/go.mod index 86f13e8..cdf3376 100644 --- a/control-plane/go.mod +++ b/control-plane/go.mod @@ -1,3 +1,18 @@ module git.ipao.vip/rogee/wx-win-agent/control-plane -go 1.23 +go 1.23.0 + +require modernc.org/sqlite v1.36.3 + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect + golang.org/x/sys v0.31.0 // indirect + modernc.org/libc v1.61.13 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.8.2 // indirect +) diff --git a/control-plane/go.sum b/control-plane/go.sum new file mode 100644 index 0000000..d4989e9 --- /dev/null +++ b/control-plane/go.sum @@ -0,0 +1,47 @@ +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo= +golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= +modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo= +modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw= +modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8= +modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI= +modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.36.3 h1:qYMYlFR+rtLDUzuXoST1SDIdEPbX8xzuhdF90WsX1ss= +modernc.org/sqlite v1.36.3/go.mod h1:ADySlx7K4FdY5MaJcEv86hTJ0PjedAloTUuif0YS3ws= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/control-plane/protocol.go b/control-plane/protocol.go index 63e3546..2ee5118 100644 --- a/control-plane/protocol.go +++ b/control-plane/protocol.go @@ -52,11 +52,17 @@ type NodeRegistration struct { } type AccountSummary struct { - AccountID string `json:"account_id"` - Active bool `json:"active"` - Verified bool `json:"verified"` - AllowedGroupCount int `json:"allowed_group_count"` - AllowedPrivateCount int `json:"allowed_private_count"` + AccountID string `json:"account_id"` + Active bool `json:"active"` + Verified bool `json:"verified"` + AllowedGroupCount int `json:"allowed_group_count"` + AllowedPrivateCount int `json:"allowed_private_count"` + AllowedChats []AllowedChatSummary `json:"allowed_chats,omitempty"` +} + +type AllowedChatSummary struct { + ChatID string `json:"chat_id"` + ChatType ChatType `json:"chat_type"` } type Heartbeat struct { diff --git a/control-plane/server.go b/control-plane/server.go index 6982f8f..ecb0402 100644 --- a/control-plane/server.go +++ b/control-plane/server.go @@ -27,55 +27,65 @@ import ( ) type ServerConfig struct { - ListenAddr string - DataFile string - NodeTokens map[string]string - WebUsers map[string]string - LeaseTTL time.Duration - HeartbeatTimeout time.Duration - SessionTTL time.Duration - TLSCertFile string - TLSKeyFile string - MTLSClientCAFile string - MTLSRequireNodeCert bool - MTLSRevokedCertsFile string - BackupDir string - BackupCount int - BackupInterval time.Duration - TaskRetention time.Duration - EventRetention time.Duration - AuditRetention time.Duration - AIBaseURL string - AIAPIKey string - AIModel string - AITimeout time.Duration - AISchedulerInterval time.Duration - AIProvider AIProvider + ListenAddr string + DataFile string + AccountDataDir string + NodeTokens map[string]string + WebUsers map[string]string + LeaseTTL time.Duration + HeartbeatTimeout time.Duration + SessionTTL time.Duration + TLSCertFile string + TLSKeyFile string + MTLSClientCAFile string + MTLSRequireNodeCert bool + MTLSRevokedCertsFile string + BackupDir string + BackupCount int + BackupInterval time.Duration + TaskRetention time.Duration + EventRetention time.Duration + AuditRetention time.Duration + AccountRetention time.Duration + AccountMaxShardBytes int64 + AccountMaxBatchBytes int64 + AccountMaintenanceInterval time.Duration + AIBaseURL string + AIAPIKey string + AIModel string + AITimeout time.Duration + AISchedulerInterval time.Duration + AIProvider AIProvider } func DefaultServerConfig() ServerConfig { return ServerConfig{ - ListenAddr: "127.0.0.1:8090", - DataFile: "control-plane-data.json", - NodeTokens: map[string]string{}, - WebUsers: map[string]string{}, - LeaseTTL: 30 * time.Second, - HeartbeatTimeout: 45 * time.Second, - SessionTTL: 8 * time.Hour, - BackupCount: 7, - BackupInterval: 5 * time.Minute, - TaskRetention: 30 * 24 * time.Hour, - EventRetention: 30 * 24 * time.Hour, - AuditRetention: 90 * 24 * time.Hour, - AIModel: "gpt-4o-mini", - AITimeout: 60 * time.Second, - AISchedulerInterval: 5 * time.Second, + ListenAddr: "127.0.0.1:8090", + DataFile: "control-plane-data.json", + NodeTokens: map[string]string{}, + WebUsers: map[string]string{}, + LeaseTTL: 30 * time.Second, + HeartbeatTimeout: 45 * time.Second, + SessionTTL: 8 * time.Hour, + BackupCount: 7, + BackupInterval: 5 * time.Minute, + TaskRetention: 30 * 24 * time.Hour, + EventRetention: 30 * 24 * time.Hour, + AuditRetention: 90 * 24 * time.Hour, + AccountRetention: 30 * 24 * time.Hour, + AccountMaxShardBytes: 512 * 1024 * 1024, + AccountMaxBatchBytes: maxDataBatchBytes, + AccountMaintenanceInterval: 5 * time.Minute, + AIModel: "gpt-4o-mini", + AITimeout: 60 * time.Second, + AISchedulerInterval: 5 * time.Second, } } type Server struct { config ServerConfig store *Store + accountStores *AccountStoreManager tlsConfig *tls.Config nodeTokenHashes map[string][32]byte userPasswords map[string][32]byte @@ -111,6 +121,9 @@ func NewServer(config ServerConfig) (*Server, error) { if config.DataFile == "" { config.DataFile = defaults.DataFile } + if config.AccountDataDir == "" { + config.AccountDataDir = config.DataFile + ".accounts" + } if config.BackupDir == "" { config.BackupDir = config.DataFile + ".backups" } @@ -129,6 +142,18 @@ func NewServer(config ServerConfig) (*Server, error) { if config.AuditRetention == 0 { config.AuditRetention = defaults.AuditRetention } + if config.AccountRetention == 0 { + config.AccountRetention = defaults.AccountRetention + } + if config.AccountMaxShardBytes == 0 { + config.AccountMaxShardBytes = defaults.AccountMaxShardBytes + } + if config.AccountMaxBatchBytes == 0 { + config.AccountMaxBatchBytes = defaults.AccountMaxBatchBytes + } + if config.AccountMaintenanceInterval == 0 { + config.AccountMaintenanceInterval = defaults.AccountMaintenanceInterval + } if config.LeaseTTL <= 0 { config.LeaseTTL = defaults.LeaseTTL } @@ -156,8 +181,8 @@ func NewServer(config ServerConfig) (*Server, error) { if config.WebUsers == nil { config.WebUsers = map[string]string{} } - if config.BackupCount < 0 || config.BackupInterval < 0 || config.TaskRetention < 0 || config.EventRetention < 0 || config.AuditRetention < 0 || config.AITimeout < 0 || config.AISchedulerInterval < 0 { - return nil, errors.New("backup count and retention settings must be non-negative") + if config.BackupCount < 0 || config.BackupInterval < 0 || config.TaskRetention < 0 || config.EventRetention < 0 || config.AuditRetention < 0 || config.AccountRetention < 0 || config.AccountMaxShardBytes < 0 || config.AccountMaxBatchBytes < 0 || config.AccountMaintenanceInterval < 0 || config.AITimeout < 0 || config.AISchedulerInterval < 0 { + return nil, errors.New("backup count, account budgets, and retention settings must be non-negative") } tlsConfig, err := newTLSConfig(config) if err != nil { @@ -170,10 +195,20 @@ func NewServer(config ServerConfig) (*Server, error) { if err != nil { return nil, err } + accountStores, err := OpenAccountStoreManager(config.AccountDataDir, AccountStoreManagerOptions{ + Retention: config.AccountRetention, + MaxShardBytes: config.AccountMaxShardBytes, + MaxBatchBytes: config.AccountMaxBatchBytes, + }) + if err != nil { + _ = store.Close() + return nil, err + } aiCtx, aiCancel := context.WithCancel(context.Background()) s := &Server{ config: config, store: store, + accountStores: accountStores, tlsConfig: tlsConfig, nodeTokenHashes: map[string][32]byte{}, userPasswords: map[string][32]byte{}, @@ -193,9 +228,10 @@ func NewServer(config ServerConfig) (*Server, error) { s.userPasswords[username] = sha256.Sum256([]byte(password)) } } - s.aiWG.Add(2) + s.aiWG.Add(3) go s.aiWorker() go s.aiScheduler() + go s.accountMaintenance() return s, nil } @@ -223,12 +259,34 @@ func (s *Server) ListenAndServe(ctx context.Context) error { return err } -// Close releases the active/passive store lock and stops background AI work. +func (s *Server) accountMaintenance() { + defer s.aiWG.Done() + run := func() { + _, _ = s.accountStores.RunMaintenance(s.aiCtx, time.Now().UTC()) + } + run() + interval := s.config.AccountMaintenanceInterval + if interval <= 0 { + return + } + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-s.aiCtx.Done(): + return + case <-ticker.C: + run() + } + } +} + +// Close releases the active/passive store lock and stops background work. func (s *Server) Close() error { s.closeOnce.Do(func() { s.aiCancel() s.aiWG.Wait() - s.closeErr = s.store.Close() + s.closeErr = errors.Join(s.accountStores.Close(), s.store.Close()) }) return s.closeErr } @@ -266,6 +324,10 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { err = s.listNodes(w, r) case r.URL.Path == "/v1/nodes/register" && r.Method == http.MethodPost: err = s.registerNode(w, r, correlationID) + case r.URL.Path == "/v1/data/batches" && r.Method == http.MethodPost: + err = s.ingestDataBatch(w, r, correlationID) + case strings.HasPrefix(r.URL.Path, "/v1/data/accounts/"): + err = s.dataRoute(w, r, correlationID) case strings.HasPrefix(r.URL.Path, "/v1/nodes/"): err = s.nodeRoute(w, r, correlationID) case strings.HasPrefix(r.URL.Path, "/v1/reads/"): @@ -331,6 +393,12 @@ func (s *Server) registerNode(w http.ResponseWriter, r *http.Request, correlatio if (request.ConnectionID != "" && !validIdentifier(request.ConnectionID, 200)) || !validIdentifier(request.AgentVersion, 80) || request.ProtocolVersion != ProtocolVersion || len(request.Capabilities) > 100 || !validCapabilities(request.Capabilities) || !validAccountSummaries(request.Accounts) { return requestError{status: http.StatusBadRequest, code: "InvalidRegistration", message: "Node registration is invalid."} } + if err := s.registerDataAccounts(nodeID, request); err != nil { + if errors.Is(err, ErrAccountBindingConflict) { + return requestError{status: http.StatusConflict, code: "AccountBindingConflict", message: "The registered account source conflicts with an existing binding."} + } + return requestError{status: http.StatusInternalServerError, code: "AccountRegistrationFailed", message: "The platform could not register account data storage."} + } now := time.Now().UTC() if err := s.store.Mutate(func(state *PersistedState) error { node := state.Nodes[nodeID] @@ -388,6 +456,8 @@ func (s *Server) nodeRoute(w http.ResponseWriter, r *http.Request, correlationID return requestError{status: http.StatusMethodNotAllowed, code: "MethodNotAllowed", message: "Method is not allowed."} } return s.ingestEvent(w, r, nodeID, correlationID) + case "data": + return s.nodeDataRoute(w, r, nodeID, parts[4:], correlationID) default: return requestError{status: http.StatusNotFound, code: "NotFound", message: "Resource was not found."} } @@ -906,6 +976,13 @@ func validTaskPayload(kind string, payload jsonRaw) bool { if kind == "send-text" { return validSendTextPayload(payload) } + if kind == "sync-data" { + var value struct { + StreamKey string `json:"stream_key"` + Reason string `json:"reason"` + } + return decodeRaw(payload, &value) && value.StreamKey == "messages" && (value.Reason == "" || validIdentifier(value.Reason, 80)) + } if !isReadTaskKind(kind) || len(payload) == 0 || len(payload) > 64*1024 || !json.Valid(payload) { return false } diff --git a/control-plane/web/dist/assets/index-6ql7JGil.css b/control-plane/web/dist/assets/index-6ql7JGil.css new file mode 100644 index 0000000..3d549c2 --- /dev/null +++ b/control-plane/web/dist/assets/index-6ql7JGil.css @@ -0,0 +1 @@ +:root{font-family:Inter,Noto Sans SC,Segoe UI,sans-serif;color:#172033;background:#f6f7fb;font-synthesis:none;text-rendering:optimizeLegibility;--ink: #172033;--muted: #738096;--subtle: #9aa5b6;--line: #e5e9f0;--line-strong: #d6dce6;--panel: #ffffff;--surface: #f6f7fb;--blue: #315ee8;--blue-soft: #edf2ff;--green: #168568;--green-soft: #e9f8f2;--amber: #a8681b;--amber-soft: #fff5e5;--red: #be4e5c;--red-soft: #fff0f2;--navy: #202b43;--shadow: 0 12px 30px rgba(25, 39, 68, .08)}*{box-sizing:border-box}::selection{color:#fff;background:var(--blue)}html{min-width:320px;background:var(--surface)}body{min-width:320px;min-height:100vh;margin:0;background:var(--surface)}button,input,textarea,select{font:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.52}button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:3px solid rgba(49,94,232,.22);outline-offset:2px}svg{display:block;flex:0 0 auto}.login-page{display:grid;place-items:center;min-height:100vh;padding:24px;background:#f6f7fb}.login-panel{width:min(430px,100%);padding:42px;border:1px solid var(--line);border-radius:18px;background:var(--panel);box-shadow:var(--shadow)}.brand-mark{display:grid;place-items:center;width:46px;height:46px;border-radius:13px;color:#fff;background:var(--navy);font-size:21px;font-weight:800;letter-spacing:-.04em}.brand-mark.small{width:36px;height:36px;border-radius:10px;font-size:17px}.eyebrow{margin:0;color:var(--subtle);font-size:10px;font-weight:800;letter-spacing:.14em;text-transform:uppercase}.login-panel .eyebrow{margin-top:28px}.login-panel h1{margin:11px 0 8px;font-size:32px;letter-spacing:-.045em}.login-intro{max-width:36ch;margin:0;color:var(--muted);font-size:14px;line-height:1.7}.login-form{display:grid;gap:16px;margin-top:30px}label{display:grid;gap:7px;color:#5b687d;font-size:12px;font-weight:700}input,textarea,select{width:100%;border:1px solid var(--line-strong);border-radius:9px;color:var(--ink);background:#fff;transition:border-color .16s ease,box-shadow .16s ease}input,select{height:42px;padding:0 12px}textarea{padding:11px 12px;resize:vertical}input::placeholder,textarea::placeholder{color:#a3adbc}input:focus,textarea:focus,select:focus{border-color:#7d9af0;box-shadow:0 0 0 3px #315ee81a;outline:0}.form-error,.global-error,.inline-error,.read-notice{display:flex;gap:9px;align-items:center;border-radius:9px}.form-error,.global-error,.inline-error{border:1px solid #f0cbd1;color:var(--red);background:var(--red-soft)}.read-notice{margin:8px 0;padding:9px 11px;border:1px solid #ead9a6;color:#765b1c;background:#fff9e8;font-size:12px}.form-error{padding:10px 12px;font-size:12px}.login-footnote{display:flex;gap:9px;align-items:center;margin:24px 0 0;color:var(--subtle);font-size:11px}.secure-dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:#28a77d;box-shadow:0 0 0 4px #28a77d1f}.app-shell{display:flex;min-height:100vh}.sidebar{position:fixed;inset:0 auto 0 0;z-index:3;display:flex;flex-direction:column;width:248px;padding:26px 16px 17px;border-right:1px solid var(--line);background:#fff}.sidebar-brand{display:flex;gap:11px;align-items:center;padding:0 9px 40px}.sidebar-brand strong,.sidebar-brand span{display:block}.sidebar-brand strong{color:var(--ink);font-size:15px;letter-spacing:-.02em}.sidebar-brand span{margin-top:4px;color:var(--subtle);font-size:10px}.nav-caption{margin:0;padding:0 12px 10px;color:var(--subtle);font-size:10px;font-weight:800;letter-spacing:.14em;text-transform:uppercase}.sidebar nav{display:grid;gap:4px}.nav-item{display:flex;gap:12px;align-items:center;width:100%;min-height:44px;padding:0 12px;border:0;border-radius:9px;color:#77849a;text-align:left;background:transparent;font-size:13px;font-weight:700}.nav-item:hover{color:var(--ink);background:#f4f6fa}.nav-item.active{color:var(--blue);background:var(--blue-soft)}.nav-item.active svg{color:var(--blue)}.nav-item svg{color:#98a4b7}.sidebar-spacer{flex:1}.connection-hint{display:flex;gap:10px;align-items:center;margin:14px 4px 17px;padding:12px 11px;border:1px solid #e6ecf3;border-radius:10px;background:#fbfcfe}.connection-hint strong,.connection-hint small,.user-name strong,.user-name small{display:block}.connection-hint strong{color:#4c5c72;font-size:11px}.connection-hint small{margin-top:4px;color:var(--subtle);font-size:10px}.user-menu{display:flex;gap:9px;align-items:center;padding:14px 7px 0;border-top:1px solid var(--line)}.avatar,.node-avatar,.session-avatar,.contact-avatar,.message-avatar{display:grid;place-items:center;flex:0 0 auto;width:32px;height:32px;border-radius:10px;color:#3157bf;background:#e8efff;font-size:11px;font-weight:800}.user-name{flex:1;min-width:0}.user-name strong{overflow:hidden;color:#415069;font-size:11px;text-overflow:ellipsis;white-space:nowrap}.user-name small{margin-top:3px;color:var(--subtle);font-size:10px}.user-menu button{display:grid;place-items:center;width:30px;height:30px;padding:0;border:0;border-radius:7px;color:#8d99aa;background:transparent}.user-menu button:hover{color:var(--red);background:var(--red-soft)}.main-area{width:calc(100% - 248px);min-width:0;margin-left:248px}.topbar{display:flex;gap:24px;align-items:flex-end;justify-content:space-between;min-height:126px;padding:32px 48px 28px;border-bottom:1px solid var(--line);background:#fff}.topbar-copy h1{margin:0;color:var(--ink);font-size:28px;letter-spacing:-.045em}.topbar-copy p{margin:7px 0 0;color:var(--muted);font-size:12px}.topbar-actions,.heading-actions,.toolbar{display:flex;gap:9px;align-items:center}.client-switcher{display:flex;gap:7px;align-items:center}.client-switcher span{color:var(--subtle);font-size:10px;font-weight:800;white-space:nowrap}.client-switcher select{width:190px;height:37px;color:#46556b;font-size:11px;font-weight:700}.refresh-button,.secondary-button,.primary-button,.danger-button{min-height:37px;padding:0 13px;border-radius:8px;font-size:11px;font-weight:800}.refresh-button,.secondary-button{border:1px solid var(--line-strong);color:#5e6c81;background:#fff}.refresh-button{display:flex;gap:7px;align-items:center;white-space:nowrap}.refresh-button:hover,.secondary-button:hover:not(:disabled){border-color:#9eb4ed;color:var(--blue);background:#fbfcff}.primary-button{border:1px solid var(--blue);color:#fff;background:var(--blue);box-shadow:0 5px 12px #315ee82e}.primary-button:hover:not(:disabled){background:#234bc7}.full{width:100%;height:44px;margin-top:4px;font-size:12px}.text-button{display:inline-flex;gap:5px;align-items:center;padding:0;border:0;color:var(--blue);background:transparent;font-size:11px;font-weight:800}.text-button:hover:not(:disabled){color:#1f45bb}.global-error{margin:17px 48px 0;padding:10px 13px;font-size:12px}.global-error span{flex:1}.global-error button{display:grid;place-items:center;padding:0;border:0;color:inherit;background:transparent}.connection-banner{display:flex;gap:11px;align-items:center;margin:20px 48px 0;padding:12px 14px;border:1px solid var(--line);border-radius:10px;background:#fff}.connection-banner.warning{border-color:#f1dfba;color:var(--amber);background:var(--amber-soft)}.connection-banner.negative{border-color:#f0cbd1;color:var(--red);background:var(--red-soft)}.connection-banner>div{display:grid;gap:3px;flex:1;min-width:0}.connection-banner strong{color:#4a5363;font-size:12px}.connection-banner span{color:#7c8799;font-size:11px}.page-content{max-width:1500px;padding:28px 48px 24px}.footer{display:flex;justify-content:space-between;gap:20px;padding:0 48px 28px;color:#a0aabb;font-size:10px}.status-badge{display:inline-flex;gap:6px;align-items:center;min-height:24px;padding:0 8px;border-radius:999px;font-size:10px;font-weight:800;white-space:nowrap}.status-badge i{width:5px;height:5px;border-radius:50%;background:currentColor}.status-badge.positive{color:var(--green);background:var(--green-soft)}.status-badge.warning{color:var(--amber);background:var(--amber-soft)}.status-badge.negative{color:var(--red);background:var(--red-soft)}.status-badge.neutral{color:#718096;background:#eef1f5}.workspace-panel,.session-panel,.conversation-panel{min-width:0;border:1px solid var(--line);border-radius:12px;background:var(--panel);box-shadow:0 4px 14px #19274406}.workspace-panel{padding:24px}.section-heading{display:flex;gap:18px;align-items:flex-start;justify-content:space-between;margin-bottom:20px}.section-heading.compact{margin-bottom:15px}.section-heading h2,.section-heading h3,.conversation-head h2,.context-empty h3,.gated-panel h2{margin:0;color:var(--ink);letter-spacing:-.03em}.section-heading h2,.conversation-head h2,.gated-panel h2{font-size:18px}.section-heading h3{font-size:15px}.section-heading p,.conversation-head span,.gated-panel p{margin:6px 0 0;color:var(--muted);font-size:11px;line-height:1.6}.icon-button{display:grid;place-items:center;width:31px;height:31px;padding:0;border:1px solid var(--line-strong);border-radius:8px;color:var(--muted);background:#fff}.icon-button:hover:not(:disabled){color:var(--blue);border-color:#9eb4ed}.message-workspace{display:grid;grid-template-columns:minmax(260px,320px) minmax(0,1fr);min-height:590px;overflow:hidden;border:1px solid var(--line);border-radius:12px;background:var(--panel);box-shadow:0 4px 14px #19274406}.session-panel{border:0;border-right:1px solid var(--line);border-radius:12px 0 0 12px;box-shadow:none}.conversation-panel{display:flex;flex-direction:column;border:0;border-radius:0 12px 12px 0;box-shadow:none}.session-panel,.conversation-panel{padding:21px}.search-box{display:flex;gap:8px;align-items:center;height:36px;padding:0 10px;border:1px solid var(--line);border-radius:8px;color:#96a2b3;background:#fbfcfe}.search-box:focus-within{border-color:#9eb4ed;box-shadow:0 0 0 3px #315ee814}.search-box input{height:100%;padding:0;border:0;background:transparent;box-shadow:none;font-size:11px}.search-box input:focus{outline:0}.search-box.wide{flex:1;min-width:200px}.session-list,.contact-list,.diagnostic-list,.task-list{display:grid}.session-list{margin:12px -21px 0}.session-row{display:flex;gap:10px;align-items:center;width:100%;min-width:0;padding:12px 21px;border:0;border-top:1px solid #f0f2f6;color:inherit;text-align:left;background:transparent}.session-row:hover,.session-row.selected{background:#f7f9ff}.session-row.selected{box-shadow:inset 3px 0 var(--blue)}.session-row b{display:grid;place-items:center;min-width:20px;height:20px;padding:0 5px;border-radius:999px;color:#fff;background:var(--blue);font-size:10px}.session-copy{flex:1;min-width:0}.session-copy strong,.session-copy small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.session-copy strong{color:#334259;font-size:12px}.session-copy small{margin-top:4px;color:var(--subtle);font-size:10px}.conversation-head{display:flex;gap:18px;align-items:flex-start;justify-content:space-between;padding-bottom:19px;border-bottom:1px solid var(--line)}.conversation-head h2{margin-top:6px}.message-list{display:grid;gap:18px;flex:1;align-content:start;min-height:300px;max-height:430px;padding:22px 4px;overflow:auto}.message-row{display:flex;gap:10px;align-items:flex-start}.message-row>div:last-child{min-width:0}.message-avatar{width:28px;height:28px;border-radius:8px;color:#61718b;background:#eef1f6}.message-meta{display:flex;gap:10px;align-items:baseline}.message-meta strong{color:#405069;font-size:11px}.message-meta span{color:var(--subtle);font-size:10px}.message-row p{max-width:65ch;margin:5px 0 0;color:#59677b;font-size:12px;line-height:1.7;white-space:pre-wrap;word-break:break-word}.composer{display:flex;gap:9px;align-items:flex-end;padding-top:15px;border-top:1px solid var(--line)}.composer textarea{flex:1;min-height:54px;color:var(--muted);background:#f8f9fb;resize:none}.composer .primary-button{min-width:66px}.gated-panel{display:grid;grid-template-columns:54px minmax(0,600px);gap:18px;align-items:flex-start;min-height:300px;padding:38px}.gated-mark{display:grid;place-items:center;width:54px;height:54px;border-radius:14px;color:var(--amber);background:var(--amber-soft)}.gated-panel h2{margin-top:4px}.gated-panel p{max-width:62ch;margin-top:12px;font-size:13px}.gated-panel .primary-button{margin-top:25px}.gated-panel .text-button{margin-left:13px}.step-list{display:flex;flex-wrap:wrap;gap:8px;margin-top:22px}.step-list span{display:inline-flex;gap:7px;align-items:center;padding:7px 10px;border:1px solid var(--line);border-radius:7px;color:#68768a;background:#fbfcfe;font-size:11px;font-weight:700}.step-list b{display:grid;place-items:center;width:18px;height:18px;border-radius:50%;color:var(--blue);background:var(--blue-soft);font-size:10px}.toolbar{margin:-3px 0 19px}.check-filter{display:flex;grid-template-columns:none;gap:7px;align-items:center;color:#68768a;font-size:11px;white-space:nowrap}.check-filter input,.checkbox-label input{width:15px;height:15px;margin:0;accent-color:var(--blue)}.contact-list{margin:0 -24px}.contact-row,.diagnostic-row,.task-row{display:flex;gap:12px;align-items:center;min-width:0;padding:14px 24px;border-top:1px solid #eff2f6}.contact-row>div,.diagnostic-row>div,.task-main{flex:1;min-width:0}.contact-row strong,.contact-row small,.diagnostic-row strong,.diagnostic-row small,.task-main p,.task-main small{display:block}.contact-row strong,.diagnostic-row strong{color:#3a4960;font-size:12px}.contact-row small,.diagnostic-row small,.task-main p,.task-main small{margin-top:4px;color:var(--subtle);font-size:10px}.contact-row .text-button{flex:0 0 auto}.task-list{margin:0 -24px}.task-row{align-items:flex-start}.task-title{display:flex;gap:9px;align-items:center}.task-title strong{color:#3a4960;font-size:12px}.task-main p{margin-bottom:0}.task-main small{max-width:70ch;line-height:1.55}.task-meta{display:grid;gap:5px;flex:0 0 auto;color:var(--subtle);font-size:10px;text-align:right}.task-meta time{white-space:nowrap}.read-only-note{display:inline-flex;gap:5px;align-items:center;color:var(--green);font-size:10px;font-weight:800}.workspace-panel select{width:auto;height:34px;color:#68768a;font-size:11px}.settings-layout{display:grid;grid-template-columns:205px minmax(0,1fr);gap:17px;align-items:start}.settings-nav{display:grid;gap:5px}.settings-tab{display:grid;gap:5px;padding:13px 14px;border:1px solid transparent;border-radius:9px;color:#6f7d91;text-align:left;background:transparent}.settings-tab strong{color:inherit;font-size:12px}.settings-tab span{color:#9ca7b7;font-size:10px}.settings-tab:hover,.settings-tab.active{border-color:#dce5fb;color:var(--blue);background:var(--blue-soft)}.settings-content{display:grid;gap:17px;min-width:0}.boundary-list{display:grid;gap:12px;margin:0;padding:0;list-style:none}.boundary-list li{display:flex;gap:8px;align-items:flex-start;color:#627087;font-size:12px;line-height:1.55}.boundary-list li svg{margin-top:2px;color:var(--green)}.client-pills{display:grid;gap:8px}.client-pill{display:flex;gap:12px;align-items:center;justify-content:space-between;padding:11px 12px;border:1px solid var(--line);border-radius:8px;color:#5e6b80;text-align:left;background:#fff}.client-pill:hover,.client-pill.active{border-color:#a8bbed;background:#f8faff}.client-pill>span{overflow:hidden;font-size:11px;font-weight:800;text-overflow:ellipsis;white-space:nowrap}.empty-state,.context-empty,.loading-state{display:grid;justify-items:center;min-height:160px;padding:35px 15px;color:var(--subtle);text-align:center}.empty-state{align-content:center}.empty-state p,.context-empty p{max-width:38ch;margin:11px 0 0;color:var(--muted);font-size:12px;line-height:1.65}.context-empty{align-content:center;flex:1;min-height:350px}.context-empty h3{margin-top:13px;font-size:16px}.context-empty .secondary-button{margin-top:17px}.empty-icon{color:#a2aec0}.loading-state{display:flex;gap:9px;align-items:center;justify-content:center;min-height:130px;font-size:12px}.loader{width:15px;height:15px;border:2px solid #d9e2f8;border-top-color:var(--blue);border-radius:50%;animation:spin .8s linear infinite}.inline-error{justify-content:center;min-height:120px;padding:14px;font-size:12px}.inline-error .text-button,.read-notice .text-button{margin-left:4px}.refresh-hint{padding:6px 2px;color:var(--subtle);font-size:11px}.event-preview{max-width:34ch;overflow:hidden;color:#68768a;font-size:11px;text-overflow:ellipsis;white-space:nowrap}.spin{display:inline-flex;animation:spin .9s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}@media (max-width: 1080px){.topbar,.page-content{padding-right:30px;padding-left:30px}.global-error,.connection-banner{margin-right:30px;margin-left:30px}.footer{padding-right:30px;padding-left:30px}.topbar-actions{flex-wrap:wrap;justify-content:flex-end}.message-workspace{grid-template-columns:minmax(230px,285px) minmax(0,1fr)}}@media (max-width: 780px){.sidebar{position:static;width:100%;padding:15px;border-right:0;border-bottom:1px solid var(--line)}.sidebar-brand{padding-bottom:18px}.sidebar-spacer,.connection-hint{display:none}.sidebar>.nav-item{margin-top:9px}.sidebar nav{grid-template-columns:repeat(4,1fr)}.nav-item{justify-content:center;min-height:38px;padding:0 6px;font-size:11px}.nav-item.active{box-shadow:inset 0 -2px var(--blue)}.nav-item svg{display:none}.app-shell{display:block}.main-area{width:100%;margin-left:0}.topbar{display:block;min-height:auto;padding:23px 18px}.topbar-copy h1{font-size:24px}.topbar-actions{justify-content:flex-start;margin-top:17px}.client-switcher{flex:1}.client-switcher select{flex:1;width:auto}.page-content{padding:20px 18px}.global-error,.connection-banner{margin-right:18px;margin-left:18px}.connection-banner{align-items:flex-start;flex-wrap:wrap}.connection-banner .text-button{margin-left:29px}.message-workspace{display:block;min-height:0}.session-panel{border-right:0;border-bottom:1px solid var(--line);border-radius:12px 12px 0 0}.conversation-panel{min-height:460px;border-radius:0 0 12px 12px}.session-list{max-height:280px;overflow:auto}.session-row{padding-right:21px;padding-left:21px}.workspace-panel,.session-panel,.conversation-panel{padding:17px}.session-list,.contact-list,.diagnostic-list,.task-list{margin-right:-17px;margin-left:-17px}.contact-row,.diagnostic-row,.task-row{padding-right:17px;padding-left:17px}.section-heading{display:block}.section-heading>.heading-actions,.section-heading>.status-badge{margin-top:13px}.heading-actions,.toolbar{align-items:stretch;flex-wrap:wrap}.toolbar .secondary-button{flex:0 0 auto}.gated-panel{display:block;padding:25px}.gated-mark{margin-bottom:19px}.gated-panel .text-button{margin:18px 0 0}.settings-layout{display:block}.settings-nav{grid-template-columns:repeat(2,1fr);margin-bottom:17px}.settings-tab{padding:11px}.task-meta{display:none}.footer{display:block;padding:0 18px 20px;line-height:1.8}.footer span{display:block}}@media (max-width: 470px){.login-panel{padding:28px 22px}.topbar-actions{display:grid;grid-template-columns:1fr auto}.client-switcher{grid-column:1 / -1}.client-switcher select{min-width:0}.status-badge{justify-self:start}.refresh-button{justify-self:end}.composer{display:block}.composer .primary-button{width:100%;margin-top:8px}.step-list{display:grid}} diff --git a/control-plane/web/dist/assets/index-BkrtfWai.css b/control-plane/web/dist/assets/index-BkrtfWai.css deleted file mode 100644 index 83a071c..0000000 --- a/control-plane/web/dist/assets/index-BkrtfWai.css +++ /dev/null @@ -1 +0,0 @@ -:root{font-family:Inter,Noto Sans SC,Segoe UI,sans-serif;color:#172033;background:#f6f7fb;font-synthesis:none;text-rendering:optimizeLegibility;--ink: #172033;--muted: #738096;--subtle: #9aa5b6;--line: #e5e9f0;--line-strong: #d6dce6;--panel: #ffffff;--surface: #f6f7fb;--blue: #315ee8;--blue-soft: #edf2ff;--green: #168568;--green-soft: #e9f8f2;--amber: #a8681b;--amber-soft: #fff5e5;--red: #be4e5c;--red-soft: #fff0f2;--navy: #202b43;--shadow: 0 12px 30px rgba(25, 39, 68, .08)}*{box-sizing:border-box}::selection{color:#fff;background:var(--blue)}html{min-width:320px;background:var(--surface)}body{min-width:320px;min-height:100vh;margin:0;background:var(--surface)}button,input,textarea,select{font:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.52}button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:3px solid rgba(49,94,232,.22);outline-offset:2px}svg{display:block;flex:0 0 auto}.login-page{display:grid;place-items:center;min-height:100vh;padding:24px;background:#f6f7fb}.login-panel{width:min(430px,100%);padding:42px;border:1px solid var(--line);border-radius:18px;background:var(--panel);box-shadow:var(--shadow)}.brand-mark{display:grid;place-items:center;width:46px;height:46px;border-radius:13px;color:#fff;background:var(--navy);font-size:21px;font-weight:800;letter-spacing:-.04em}.brand-mark.small{width:36px;height:36px;border-radius:10px;font-size:17px}.eyebrow{margin:0;color:var(--subtle);font-size:10px;font-weight:800;letter-spacing:.14em;text-transform:uppercase}.login-panel .eyebrow{margin-top:28px}.login-panel h1{margin:11px 0 8px;font-size:32px;letter-spacing:-.045em}.login-intro{max-width:36ch;margin:0;color:var(--muted);font-size:14px;line-height:1.7}.login-form{display:grid;gap:16px;margin-top:30px}label{display:grid;gap:7px;color:#5b687d;font-size:12px;font-weight:700}input,textarea,select{width:100%;border:1px solid var(--line-strong);border-radius:9px;color:var(--ink);background:#fff;transition:border-color .16s ease,box-shadow .16s ease}input,select{height:42px;padding:0 12px}textarea{padding:11px 12px;resize:vertical}input::placeholder,textarea::placeholder{color:#a3adbc}input:focus,textarea:focus,select:focus{border-color:#7d9af0;box-shadow:0 0 0 3px #315ee81a;outline:0}.form-error,.global-error,.inline-error{display:flex;gap:9px;align-items:center;border:1px solid #f0cbd1;border-radius:9px;color:var(--red);background:var(--red-soft)}.form-error{padding:10px 12px;font-size:12px}.login-footnote{display:flex;gap:9px;align-items:center;margin:24px 0 0;color:var(--subtle);font-size:11px}.secure-dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:#28a77d;box-shadow:0 0 0 4px #28a77d1f}.app-shell{display:flex;min-height:100vh}.sidebar{position:fixed;inset:0 auto 0 0;z-index:3;display:flex;flex-direction:column;width:248px;padding:26px 16px 17px;border-right:1px solid var(--line);background:#fff}.sidebar-brand{display:flex;gap:11px;align-items:center;padding:0 9px 40px}.sidebar-brand strong,.sidebar-brand span{display:block}.sidebar-brand strong{color:var(--ink);font-size:15px;letter-spacing:-.02em}.sidebar-brand span{margin-top:4px;color:var(--subtle);font-size:10px}.nav-caption{margin:0;padding:0 12px 10px;color:var(--subtle);font-size:10px;font-weight:800;letter-spacing:.14em;text-transform:uppercase}.sidebar nav{display:grid;gap:4px}.nav-item{display:flex;gap:12px;align-items:center;width:100%;min-height:44px;padding:0 12px;border:0;border-radius:9px;color:#77849a;text-align:left;background:transparent;font-size:13px;font-weight:700}.nav-item:hover{color:var(--ink);background:#f4f6fa}.nav-item.active{color:var(--blue);background:var(--blue-soft)}.nav-item.active svg{color:var(--blue)}.nav-item svg{color:#98a4b7}.sidebar-spacer{flex:1}.connection-hint{display:flex;gap:10px;align-items:center;margin:14px 4px 17px;padding:12px 11px;border:1px solid #e6ecf3;border-radius:10px;background:#fbfcfe}.connection-hint strong,.connection-hint small,.user-name strong,.user-name small{display:block}.connection-hint strong{color:#4c5c72;font-size:11px}.connection-hint small{margin-top:4px;color:var(--subtle);font-size:10px}.user-menu{display:flex;gap:9px;align-items:center;padding:14px 7px 0;border-top:1px solid var(--line)}.avatar,.node-avatar,.session-avatar,.contact-avatar,.message-avatar{display:grid;place-items:center;flex:0 0 auto;width:32px;height:32px;border-radius:10px;color:#3157bf;background:#e8efff;font-size:11px;font-weight:800}.user-name{flex:1;min-width:0}.user-name strong{overflow:hidden;color:#415069;font-size:11px;text-overflow:ellipsis;white-space:nowrap}.user-name small{margin-top:3px;color:var(--subtle);font-size:10px}.user-menu button{display:grid;place-items:center;width:30px;height:30px;padding:0;border:0;border-radius:7px;color:#8d99aa;background:transparent}.user-menu button:hover{color:var(--red);background:var(--red-soft)}.main-area{width:calc(100% - 248px);min-width:0;margin-left:248px}.topbar{display:flex;gap:24px;align-items:flex-end;justify-content:space-between;min-height:126px;padding:32px 48px 28px;border-bottom:1px solid var(--line);background:#fff}.topbar-copy h1{margin:0;color:var(--ink);font-size:28px;letter-spacing:-.045em}.topbar-copy p{margin:7px 0 0;color:var(--muted);font-size:12px}.topbar-actions,.heading-actions,.toolbar{display:flex;gap:9px;align-items:center}.client-switcher{display:flex;gap:7px;align-items:center}.client-switcher span{color:var(--subtle);font-size:10px;font-weight:800;white-space:nowrap}.client-switcher select{width:190px;height:37px;color:#46556b;font-size:11px;font-weight:700}.refresh-button,.secondary-button,.primary-button,.danger-button{min-height:37px;padding:0 13px;border-radius:8px;font-size:11px;font-weight:800}.refresh-button,.secondary-button{border:1px solid var(--line-strong);color:#5e6c81;background:#fff}.refresh-button{display:flex;gap:7px;align-items:center;white-space:nowrap}.refresh-button:hover,.secondary-button:hover:not(:disabled){border-color:#9eb4ed;color:var(--blue);background:#fbfcff}.primary-button{border:1px solid var(--blue);color:#fff;background:var(--blue);box-shadow:0 5px 12px #315ee82e}.primary-button:hover:not(:disabled){background:#234bc7}.full{width:100%;height:44px;margin-top:4px;font-size:12px}.text-button{display:inline-flex;gap:5px;align-items:center;padding:0;border:0;color:var(--blue);background:transparent;font-size:11px;font-weight:800}.text-button:hover:not(:disabled){color:#1f45bb}.global-error{margin:17px 48px 0;padding:10px 13px;font-size:12px}.global-error span{flex:1}.global-error button{display:grid;place-items:center;padding:0;border:0;color:inherit;background:transparent}.connection-banner{display:flex;gap:11px;align-items:center;margin:20px 48px 0;padding:12px 14px;border:1px solid var(--line);border-radius:10px;background:#fff}.connection-banner.warning{border-color:#f1dfba;color:var(--amber);background:var(--amber-soft)}.connection-banner.negative{border-color:#f0cbd1;color:var(--red);background:var(--red-soft)}.connection-banner>div{display:grid;gap:3px;flex:1;min-width:0}.connection-banner strong{color:#4a5363;font-size:12px}.connection-banner span{color:#7c8799;font-size:11px}.page-content{max-width:1500px;padding:28px 48px 24px}.footer{display:flex;justify-content:space-between;gap:20px;padding:0 48px 28px;color:#a0aabb;font-size:10px}.status-badge{display:inline-flex;gap:6px;align-items:center;min-height:24px;padding:0 8px;border-radius:999px;font-size:10px;font-weight:800;white-space:nowrap}.status-badge i{width:5px;height:5px;border-radius:50%;background:currentColor}.status-badge.positive{color:var(--green);background:var(--green-soft)}.status-badge.warning{color:var(--amber);background:var(--amber-soft)}.status-badge.negative{color:var(--red);background:var(--red-soft)}.status-badge.neutral{color:#718096;background:#eef1f5}.workspace-panel,.session-panel,.conversation-panel{min-width:0;border:1px solid var(--line);border-radius:12px;background:var(--panel);box-shadow:0 4px 14px #19274406}.workspace-panel{padding:24px}.section-heading{display:flex;gap:18px;align-items:flex-start;justify-content:space-between;margin-bottom:20px}.section-heading.compact{margin-bottom:15px}.section-heading h2,.section-heading h3,.conversation-head h2,.context-empty h3,.gated-panel h2{margin:0;color:var(--ink);letter-spacing:-.03em}.section-heading h2,.conversation-head h2,.gated-panel h2{font-size:18px}.section-heading h3{font-size:15px}.section-heading p,.conversation-head span,.gated-panel p{margin:6px 0 0;color:var(--muted);font-size:11px;line-height:1.6}.icon-button{display:grid;place-items:center;width:31px;height:31px;padding:0;border:1px solid var(--line-strong);border-radius:8px;color:var(--muted);background:#fff}.icon-button:hover:not(:disabled){color:var(--blue);border-color:#9eb4ed}.message-workspace{display:grid;grid-template-columns:minmax(260px,320px) minmax(0,1fr);min-height:590px;overflow:hidden;border:1px solid var(--line);border-radius:12px;background:var(--panel);box-shadow:0 4px 14px #19274406}.session-panel{border:0;border-right:1px solid var(--line);border-radius:12px 0 0 12px;box-shadow:none}.conversation-panel{display:flex;flex-direction:column;border:0;border-radius:0 12px 12px 0;box-shadow:none}.session-panel,.conversation-panel{padding:21px}.search-box{display:flex;gap:8px;align-items:center;height:36px;padding:0 10px;border:1px solid var(--line);border-radius:8px;color:#96a2b3;background:#fbfcfe}.search-box:focus-within{border-color:#9eb4ed;box-shadow:0 0 0 3px #315ee814}.search-box input{height:100%;padding:0;border:0;background:transparent;box-shadow:none;font-size:11px}.search-box input:focus{outline:0}.search-box.wide{flex:1;min-width:200px}.session-list,.contact-list,.diagnostic-list,.task-list{display:grid}.session-list{margin:12px -21px 0}.session-row{display:flex;gap:10px;align-items:center;width:100%;min-width:0;padding:12px 21px;border:0;border-top:1px solid #f0f2f6;color:inherit;text-align:left;background:transparent}.session-row:hover,.session-row.selected{background:#f7f9ff}.session-row.selected{box-shadow:inset 3px 0 var(--blue)}.session-row b{display:grid;place-items:center;min-width:20px;height:20px;padding:0 5px;border-radius:999px;color:#fff;background:var(--blue);font-size:10px}.session-copy{flex:1;min-width:0}.session-copy strong,.session-copy small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.session-copy strong{color:#334259;font-size:12px}.session-copy small{margin-top:4px;color:var(--subtle);font-size:10px}.conversation-head{display:flex;gap:18px;align-items:flex-start;justify-content:space-between;padding-bottom:19px;border-bottom:1px solid var(--line)}.conversation-head h2{margin-top:6px}.message-list{display:grid;gap:18px;flex:1;align-content:start;min-height:300px;max-height:430px;padding:22px 4px;overflow:auto}.message-row{display:flex;gap:10px;align-items:flex-start}.message-row>div:last-child{min-width:0}.message-avatar{width:28px;height:28px;border-radius:8px;color:#61718b;background:#eef1f6}.message-meta{display:flex;gap:10px;align-items:baseline}.message-meta strong{color:#405069;font-size:11px}.message-meta span{color:var(--subtle);font-size:10px}.message-row p{max-width:65ch;margin:5px 0 0;color:#59677b;font-size:12px;line-height:1.7;white-space:pre-wrap;word-break:break-word}.composer{display:flex;gap:9px;align-items:flex-end;padding-top:15px;border-top:1px solid var(--line)}.composer textarea{flex:1;min-height:54px;color:var(--muted);background:#f8f9fb;resize:none}.composer .primary-button{min-width:66px}.gated-panel{display:grid;grid-template-columns:54px minmax(0,600px);gap:18px;align-items:flex-start;min-height:300px;padding:38px}.gated-mark{display:grid;place-items:center;width:54px;height:54px;border-radius:14px;color:var(--amber);background:var(--amber-soft)}.gated-panel h2{margin-top:4px}.gated-panel p{max-width:62ch;margin-top:12px;font-size:13px}.gated-panel .primary-button{margin-top:25px}.gated-panel .text-button{margin-left:13px}.step-list{display:flex;flex-wrap:wrap;gap:8px;margin-top:22px}.step-list span{display:inline-flex;gap:7px;align-items:center;padding:7px 10px;border:1px solid var(--line);border-radius:7px;color:#68768a;background:#fbfcfe;font-size:11px;font-weight:700}.step-list b{display:grid;place-items:center;width:18px;height:18px;border-radius:50%;color:var(--blue);background:var(--blue-soft);font-size:10px}.toolbar{margin:-3px 0 19px}.check-filter{display:flex;grid-template-columns:none;gap:7px;align-items:center;color:#68768a;font-size:11px;white-space:nowrap}.check-filter input,.checkbox-label input{width:15px;height:15px;margin:0;accent-color:var(--blue)}.contact-list{margin:0 -24px}.contact-row,.diagnostic-row,.task-row{display:flex;gap:12px;align-items:center;min-width:0;padding:14px 24px;border-top:1px solid #eff2f6}.contact-row>div,.diagnostic-row>div,.task-main{flex:1;min-width:0}.contact-row strong,.contact-row small,.diagnostic-row strong,.diagnostic-row small,.task-main p,.task-main small{display:block}.contact-row strong,.diagnostic-row strong{color:#3a4960;font-size:12px}.contact-row small,.diagnostic-row small,.task-main p,.task-main small{margin-top:4px;color:var(--subtle);font-size:10px}.contact-row .text-button{flex:0 0 auto}.task-list{margin:0 -24px}.task-row{align-items:flex-start}.task-title{display:flex;gap:9px;align-items:center}.task-title strong{color:#3a4960;font-size:12px}.task-main p{margin-bottom:0}.task-main small{max-width:70ch;line-height:1.55}.task-meta{display:grid;gap:5px;flex:0 0 auto;color:var(--subtle);font-size:10px;text-align:right}.task-meta time{white-space:nowrap}.read-only-note{display:inline-flex;gap:5px;align-items:center;color:var(--green);font-size:10px;font-weight:800}.workspace-panel select{width:auto;height:34px;color:#68768a;font-size:11px}.settings-layout{display:grid;grid-template-columns:205px minmax(0,1fr);gap:17px;align-items:start}.settings-nav{display:grid;gap:5px}.settings-tab{display:grid;gap:5px;padding:13px 14px;border:1px solid transparent;border-radius:9px;color:#6f7d91;text-align:left;background:transparent}.settings-tab strong{color:inherit;font-size:12px}.settings-tab span{color:#9ca7b7;font-size:10px}.settings-tab:hover,.settings-tab.active{border-color:#dce5fb;color:var(--blue);background:var(--blue-soft)}.settings-content{display:grid;gap:17px;min-width:0}.boundary-list{display:grid;gap:12px;margin:0;padding:0;list-style:none}.boundary-list li{display:flex;gap:8px;align-items:flex-start;color:#627087;font-size:12px;line-height:1.55}.boundary-list li svg{margin-top:2px;color:var(--green)}.client-pills{display:grid;gap:8px}.client-pill{display:flex;gap:12px;align-items:center;justify-content:space-between;padding:11px 12px;border:1px solid var(--line);border-radius:8px;color:#5e6b80;text-align:left;background:#fff}.client-pill:hover,.client-pill.active{border-color:#a8bbed;background:#f8faff}.client-pill>span{overflow:hidden;font-size:11px;font-weight:800;text-overflow:ellipsis;white-space:nowrap}.empty-state,.context-empty,.loading-state{display:grid;justify-items:center;min-height:160px;padding:35px 15px;color:var(--subtle);text-align:center}.empty-state{align-content:center}.empty-state p,.context-empty p{max-width:38ch;margin:11px 0 0;color:var(--muted);font-size:12px;line-height:1.65}.context-empty{align-content:center;flex:1;min-height:350px}.context-empty h3{margin-top:13px;font-size:16px}.context-empty .secondary-button{margin-top:17px}.empty-icon{color:#a2aec0}.loading-state{display:flex;gap:9px;align-items:center;justify-content:center;min-height:130px;font-size:12px}.loader{width:15px;height:15px;border:2px solid #d9e2f8;border-top-color:var(--blue);border-radius:50%;animation:spin .8s linear infinite}.inline-error{justify-content:center;min-height:120px;padding:14px;font-size:12px}.inline-error .text-button{margin-left:4px}.event-preview{max-width:34ch;overflow:hidden;color:#68768a;font-size:11px;text-overflow:ellipsis;white-space:nowrap}.spin{display:inline-flex;animation:spin .9s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}@media (max-width: 1080px){.topbar,.page-content{padding-right:30px;padding-left:30px}.global-error,.connection-banner{margin-right:30px;margin-left:30px}.footer{padding-right:30px;padding-left:30px}.topbar-actions{flex-wrap:wrap;justify-content:flex-end}.message-workspace{grid-template-columns:minmax(230px,285px) minmax(0,1fr)}}@media (max-width: 780px){.sidebar{position:static;width:100%;padding:15px;border-right:0;border-bottom:1px solid var(--line)}.sidebar-brand{padding-bottom:18px}.sidebar-spacer,.connection-hint{display:none}.sidebar>.nav-item{margin-top:9px}.sidebar nav{grid-template-columns:repeat(4,1fr)}.nav-item{justify-content:center;min-height:38px;padding:0 6px;font-size:11px}.nav-item.active{box-shadow:inset 0 -2px var(--blue)}.nav-item svg{display:none}.app-shell{display:block}.main-area{width:100%;margin-left:0}.topbar{display:block;min-height:auto;padding:23px 18px}.topbar-copy h1{font-size:24px}.topbar-actions{justify-content:flex-start;margin-top:17px}.client-switcher{flex:1}.client-switcher select{flex:1;width:auto}.page-content{padding:20px 18px}.global-error,.connection-banner{margin-right:18px;margin-left:18px}.connection-banner{align-items:flex-start;flex-wrap:wrap}.connection-banner .text-button{margin-left:29px}.message-workspace{display:block;min-height:0}.session-panel{border-right:0;border-bottom:1px solid var(--line);border-radius:12px 12px 0 0}.conversation-panel{min-height:460px;border-radius:0 0 12px 12px}.session-list{max-height:280px;overflow:auto}.session-row{padding-right:21px;padding-left:21px}.workspace-panel,.session-panel,.conversation-panel{padding:17px}.session-list,.contact-list,.diagnostic-list,.task-list{margin-right:-17px;margin-left:-17px}.contact-row,.diagnostic-row,.task-row{padding-right:17px;padding-left:17px}.section-heading{display:block}.section-heading>.heading-actions,.section-heading>.status-badge{margin-top:13px}.heading-actions,.toolbar{align-items:stretch;flex-wrap:wrap}.toolbar .secondary-button{flex:0 0 auto}.gated-panel{display:block;padding:25px}.gated-mark{margin-bottom:19px}.gated-panel .text-button{margin:18px 0 0}.settings-layout{display:block}.settings-nav{grid-template-columns:repeat(2,1fr);margin-bottom:17px}.settings-tab{padding:11px}.task-meta{display:none}.footer{display:block;padding:0 18px 20px;line-height:1.8}.footer span{display:block}}@media (max-width: 470px){.login-panel{padding:28px 22px}.topbar-actions{display:grid;grid-template-columns:1fr auto}.client-switcher{grid-column:1 / -1}.client-switcher select{min-width:0}.status-badge{justify-self:start}.refresh-button{justify-self:end}.composer{display:block}.composer .primary-button{width:100%;margin-top:8px}.step-list{display:grid}} diff --git a/control-plane/web/dist/assets/index-C2PP8Qbj.js b/control-plane/web/dist/assets/index-C2PP8Qbj.js deleted file mode 100644 index be33691..0000000 --- a/control-plane/web/dist/assets/index-C2PP8Qbj.js +++ /dev/null @@ -1,40 +0,0 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const i of l)if(i.type==="childList")for(const s of i.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&r(s)}).observe(document,{childList:!0,subtree:!0});function n(l){const i={};return l.integrity&&(i.integrity=l.integrity),l.referrerPolicy&&(i.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?i.credentials="include":l.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(l){if(l.ep)return;l.ep=!0;const i=n(l);fetch(l.href,i)}})();var ru={exports:{}},ul={},lu={exports:{}},M={};/** - * @license React - * react.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var er=Symbol.for("react.element"),xc=Symbol.for("react.portal"),wc=Symbol.for("react.fragment"),Sc=Symbol.for("react.strict_mode"),kc=Symbol.for("react.profiler"),jc=Symbol.for("react.provider"),Cc=Symbol.for("react.context"),Nc=Symbol.for("react.forward_ref"),Ec=Symbol.for("react.suspense"),_c=Symbol.for("react.memo"),zc=Symbol.for("react.lazy"),Ys=Symbol.iterator;function Pc(e){return e===null||typeof e!="object"?null:(e=Ys&&e[Ys]||e["@@iterator"],typeof e=="function"?e:null)}var iu={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},su=Object.assign,ou={};function pn(e,t,n){this.props=e,this.context=t,this.refs=ou,this.updater=n||iu}pn.prototype.isReactComponent={};pn.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};pn.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function uu(){}uu.prototype=pn.prototype;function Ji(e,t,n){this.props=e,this.context=t,this.refs=ou,this.updater=n||iu}var qi=Ji.prototype=new uu;qi.constructor=Ji;su(qi,pn.prototype);qi.isPureReactComponent=!0;var Gs=Array.isArray,au=Object.prototype.hasOwnProperty,bi={current:null},cu={key:!0,ref:!0,__self:!0,__source:!0};function du(e,t,n){var r,l={},i=null,s=null;if(t!=null)for(r in t.ref!==void 0&&(s=t.ref),t.key!==void 0&&(i=""+t.key),t)au.call(t,r)&&!cu.hasOwnProperty(r)&&(l[r]=t[r]);var u=arguments.length-2;if(u===1)l.children=n;else if(1>>1,q=C[Y];if(0>>1;Yl(zl,L))wtl(or,zl)?(C[Y]=or,C[wt]=L,Y=wt):(C[Y]=zl,C[xt]=L,Y=xt);else if(wtl(or,L))C[Y]=or,C[wt]=L,Y=wt;else break e}}return P}function l(C,P){var L=C.sortIndex-P.sortIndex;return L!==0?L:C.id-P.id}if(typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var s=Date,u=s.now();e.unstable_now=function(){return s.now()-u}}var a=[],f=[],g=1,h=null,m=3,x=!1,w=!1,S=!1,z=typeof setTimeout=="function"?setTimeout:null,d=typeof clearTimeout=="function"?clearTimeout:null,c=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function p(C){for(var P=n(f);P!==null;){if(P.callback===null)r(f);else if(P.startTime<=C)r(f),P.sortIndex=P.expirationTime,t(a,P);else break;P=n(f)}}function v(C){if(S=!1,p(C),!w)if(n(a)!==null)w=!0,El(k);else{var P=n(f);P!==null&&_l(v,P.startTime-C)}}function k(C,P){w=!1,S&&(S=!1,d(_),_=-1),x=!0;var L=m;try{for(p(P),h=n(a);h!==null&&(!(h.expirationTime>P)||C&&!ge());){var Y=h.callback;if(typeof Y=="function"){h.callback=null,m=h.priorityLevel;var q=Y(h.expirationTime<=P);P=e.unstable_now(),typeof q=="function"?h.callback=q:h===n(a)&&r(a),p(P)}else r(a);h=n(a)}if(h!==null)var sr=!0;else{var xt=n(f);xt!==null&&_l(v,xt.startTime-P),sr=!1}return sr}finally{h=null,m=L,x=!1}}var N=!1,E=null,_=-1,F=5,R=-1;function ge(){return!(e.unstable_now()-RC||125Y?(C.sortIndex=L,t(f,C),n(a)===null&&C===n(f)&&(S?(d(_),_=-1):S=!0,_l(v,L-Y))):(C.sortIndex=q,t(a,C),w||x||(w=!0,El(k))),C},e.unstable_shouldYield=ge,e.unstable_wrapCallback=function(C){var P=m;return function(){var L=m;m=P;try{return C.apply(this,arguments)}finally{m=L}}}})(vu);mu.exports=vu;var $c=mu.exports;/** - * @license React - * react-dom.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Vc=T,Se=$c;function y(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),ti=Object.prototype.hasOwnProperty,Wc=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,Zs={},Js={};function Bc(e){return ti.call(Js,e)?!0:ti.call(Zs,e)?!1:Wc.test(e)?Js[e]=!0:(Zs[e]=!0,!1)}function Hc(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function Qc(e,t,n,r){if(t===null||typeof t>"u"||Hc(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function de(e,t,n,r,l,i,s){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=s}var re={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){re[e]=new de(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];re[t]=new de(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){re[e]=new de(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){re[e]=new de(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){re[e]=new de(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){re[e]=new de(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){re[e]=new de(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){re[e]=new de(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){re[e]=new de(e,5,!1,e.toLowerCase(),null,!1,!1)});var ts=/[\-:]([a-z])/g;function ns(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(ts,ns);re[t]=new de(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(ts,ns);re[t]=new de(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(ts,ns);re[t]=new de(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){re[e]=new de(e,1,!1,e.toLowerCase(),null,!1,!1)});re.xlinkHref=new de("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){re[e]=new de(e,1,!1,e.toLowerCase(),null,!0,!0)});function rs(e,t,n,r){var l=re.hasOwnProperty(t)?re[t]:null;(l!==null?l.type!==0:r||!(2u||l[s]!==i[u]){var a=` -`+l[s].replace(" at new "," at ");return e.displayName&&a.includes("")&&(a=a.replace("",e.displayName)),a}while(1<=s&&0<=u);break}}}finally{Ll=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Cn(e):""}function Kc(e){switch(e.tag){case 5:return Cn(e.type);case 16:return Cn("Lazy");case 13:return Cn("Suspense");case 19:return Cn("SuspenseList");case 0:case 2:case 15:return e=Rl(e.type,!1),e;case 11:return e=Rl(e.type.render,!1),e;case 1:return e=Rl(e.type,!0),e;default:return""}}function ii(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case At:return"Fragment";case Ut:return"Portal";case ni:return"Profiler";case ls:return"StrictMode";case ri:return"Suspense";case li:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case xu:return(e.displayName||"Context")+".Consumer";case yu:return(e._context.displayName||"Context")+".Provider";case is:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case ss:return t=e.displayName||null,t!==null?t:ii(e.type)||"Memo";case et:t=e._payload,e=e._init;try{return ii(e(t))}catch{}}return null}function Yc(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return ii(t);case 8:return t===ls?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function ht(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Su(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Gc(e){var t=Su(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,i=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(s){r=""+s,i.call(this,s)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(s){r=""+s},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function cr(e){e._valueTracker||(e._valueTracker=Gc(e))}function ku(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Su(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Dr(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function si(e,t){var n=t.checked;return H({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function bs(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=ht(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function ju(e,t){t=t.checked,t!=null&&rs(e,"checked",t,!1)}function oi(e,t){ju(e,t);var n=ht(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?ui(e,t.type,n):t.hasOwnProperty("defaultValue")&&ui(e,t.type,ht(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function eo(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function ui(e,t,n){(t!=="number"||Dr(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Nn=Array.isArray;function Jt(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=dr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Un(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var zn={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Xc=["Webkit","ms","Moz","O"];Object.keys(zn).forEach(function(e){Xc.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),zn[t]=zn[e]})});function _u(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||zn.hasOwnProperty(e)&&zn[e]?(""+t).trim():t+"px"}function zu(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=_u(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var Zc=H({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function di(e,t){if(t){if(Zc[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(y(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(y(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(y(61))}if(t.style!=null&&typeof t.style!="object")throw Error(y(62))}}function fi(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var pi=null;function os(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var hi=null,qt=null,bt=null;function ro(e){if(e=rr(e)){if(typeof hi!="function")throw Error(y(280));var t=e.stateNode;t&&(t=pl(t),hi(e.stateNode,e.type,t))}}function Pu(e){qt?bt?bt.push(e):bt=[e]:qt=e}function Tu(){if(qt){var e=qt,t=bt;if(bt=qt=null,ro(e),t)for(e=0;e>>=0,e===0?32:31-(od(e)/ud|0)|0}var fr=64,pr=4194304;function En(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Vr(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,i=e.pingedLanes,s=n&268435455;if(s!==0){var u=s&~l;u!==0?r=En(u):(i&=s,i!==0&&(r=En(i)))}else s=n&~l,s!==0?r=En(s):i!==0&&(r=En(i));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,i=t&-t,l>=i||l===16&&(i&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function tr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Ie(t),e[t]=n}function fd(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Tn),po=" ",ho=!1;function Zu(e,t){switch(e){case"keyup":return $d.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Ju(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var $t=!1;function Wd(e,t){switch(e){case"compositionend":return Ju(t);case"keypress":return t.which!==32?null:(ho=!0,po);case"textInput":return e=t.data,e===po&&ho?null:e;default:return null}}function Bd(e,t){if($t)return e==="compositionend"||!ms&&Zu(e,t)?(e=Gu(),zr=fs=lt=null,$t=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=yo(n)}}function ta(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?ta(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function na(){for(var e=window,t=Dr();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Dr(e.document)}return t}function vs(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function qd(e){var t=na(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&ta(n.ownerDocument.documentElement,n)){if(r!==null&&vs(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,i=Math.min(r.start,l);r=r.end===void 0?i:Math.min(r.end,l),!e.extend&&i>r&&(l=r,r=i,i=l),l=xo(n,i);var s=xo(n,r);l&&s&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==s.node||e.focusOffset!==s.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),i>r?(e.addRange(t),e.extend(s.node,s.offset)):(t.setEnd(s.node,s.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,Vt=null,wi=null,Rn=null,Si=!1;function wo(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Si||Vt==null||Vt!==Dr(r)||(r=Vt,"selectionStart"in r&&vs(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Rn&&Hn(Rn,r)||(Rn=r,r=Hr(wi,"onSelect"),0Ht||(e.current=_i[Ht],_i[Ht]=null,Ht--)}function U(e,t){Ht++,_i[Ht]=e.current,e.current=t}var mt={},oe=gt(mt),he=gt(!1),Pt=mt;function ln(e,t){var n=e.type.contextTypes;if(!n)return mt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},i;for(i in n)l[i]=t[i];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function me(e){return e=e.childContextTypes,e!=null}function Kr(){$(he),$(oe)}function _o(e,t,n){if(oe.current!==mt)throw Error(y(168));U(oe,t),U(he,n)}function da(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(y(108,Yc(e)||"Unknown",l));return H({},n,r)}function Yr(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||mt,Pt=oe.current,U(oe,e),U(he,he.current),!0}function zo(e,t,n){var r=e.stateNode;if(!r)throw Error(y(169));n?(e=da(e,t,Pt),r.__reactInternalMemoizedMergedChildContext=e,$(he),$(oe),U(oe,e)):$(he),U(he,n)}var He=null,hl=!1,Kl=!1;function fa(e){He===null?He=[e]:He.push(e)}function df(e){hl=!0,fa(e)}function yt(){if(!Kl&&He!==null){Kl=!0;var e=0,t=O;try{var n=He;for(O=1;e>=s,l-=s,Qe=1<<32-Ie(t)+l|n<_?(F=E,E=null):F=E.sibling;var R=m(d,E,p[_],v);if(R===null){E===null&&(E=F);break}e&&E&&R.alternate===null&&t(d,E),c=i(R,c,_),N===null?k=R:N.sibling=R,N=R,E=F}if(_===p.length)return n(d,E),V&&St(d,_),k;if(E===null){for(;__?(F=E,E=null):F=E.sibling;var ge=m(d,E,R.value,v);if(ge===null){E===null&&(E=F);break}e&&E&&ge.alternate===null&&t(d,E),c=i(ge,c,_),N===null?k=ge:N.sibling=ge,N=ge,E=F}if(R.done)return n(d,E),V&&St(d,_),k;if(E===null){for(;!R.done;_++,R=p.next())R=h(d,R.value,v),R!==null&&(c=i(R,c,_),N===null?k=R:N.sibling=R,N=R);return V&&St(d,_),k}for(E=r(d,E);!R.done;_++,R=p.next())R=x(E,d,_,R.value,v),R!==null&&(e&&R.alternate!==null&&E.delete(R.key===null?_:R.key),c=i(R,c,_),N===null?k=R:N.sibling=R,N=R);return e&&E.forEach(function(D){return t(d,D)}),V&&St(d,_),k}function z(d,c,p,v){if(typeof p=="object"&&p!==null&&p.type===At&&p.key===null&&(p=p.props.children),typeof p=="object"&&p!==null){switch(p.$$typeof){case ar:e:{for(var k=p.key,N=c;N!==null;){if(N.key===k){if(k=p.type,k===At){if(N.tag===7){n(d,N.sibling),c=l(N,p.props.children),c.return=d,d=c;break e}}else if(N.elementType===k||typeof k=="object"&&k!==null&&k.$$typeof===et&&Lo(k)===N.type){n(d,N.sibling),c=l(N,p.props),c.ref=Sn(d,N,p),c.return=d,d=c;break e}n(d,N);break}else t(d,N);N=N.sibling}p.type===At?(c=zt(p.props.children,d.mode,v,p.key),c.return=d,d=c):(v=Fr(p.type,p.key,p.props,null,d.mode,v),v.ref=Sn(d,c,p),v.return=d,d=v)}return s(d);case Ut:e:{for(N=p.key;c!==null;){if(c.key===N)if(c.tag===4&&c.stateNode.containerInfo===p.containerInfo&&c.stateNode.implementation===p.implementation){n(d,c.sibling),c=l(c,p.children||[]),c.return=d,d=c;break e}else{n(d,c);break}else t(d,c);c=c.sibling}c=ei(p,d.mode,v),c.return=d,d=c}return s(d);case et:return N=p._init,z(d,c,N(p._payload),v)}if(Nn(p))return w(d,c,p,v);if(vn(p))return S(d,c,p,v);wr(d,p)}return typeof p=="string"&&p!==""||typeof p=="number"?(p=""+p,c!==null&&c.tag===6?(n(d,c.sibling),c=l(c,p),c.return=d,d=c):(n(d,c),c=bl(p,d.mode,v),c.return=d,d=c),s(d)):n(d,c)}return z}var on=va(!0),ga=va(!1),Zr=gt(null),Jr=null,Yt=null,ws=null;function Ss(){ws=Yt=Jr=null}function ks(e){var t=Zr.current;$(Zr),e._currentValue=t}function Ti(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function tn(e,t){Jr=e,ws=Yt=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(pe=!0),e.firstContext=null)}function ze(e){var t=e._currentValue;if(ws!==e)if(e={context:e,memoizedValue:t,next:null},Yt===null){if(Jr===null)throw Error(y(308));Yt=e,Jr.dependencies={lanes:0,firstContext:e}}else Yt=Yt.next=e;return t}var Ct=null;function js(e){Ct===null?Ct=[e]:Ct.push(e)}function ya(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,js(t)):(n.next=l.next,l.next=n),t.interleaved=n,Ze(e,r)}function Ze(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var tt=!1;function Cs(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function xa(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Ye(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function ct(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,I&2){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,Ze(e,n)}return l=r.interleaved,l===null?(t.next=t,js(r)):(t.next=l.next,l.next=t),r.interleaved=t,Ze(e,n)}function Tr(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,as(e,n)}}function Ro(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,i=null;if(n=n.firstBaseUpdate,n!==null){do{var s={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};i===null?l=i=s:i=i.next=s,n=n.next}while(n!==null);i===null?l=i=t:i=i.next=t}else l=i=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:i,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function qr(e,t,n,r){var l=e.updateQueue;tt=!1;var i=l.firstBaseUpdate,s=l.lastBaseUpdate,u=l.shared.pending;if(u!==null){l.shared.pending=null;var a=u,f=a.next;a.next=null,s===null?i=f:s.next=f,s=a;var g=e.alternate;g!==null&&(g=g.updateQueue,u=g.lastBaseUpdate,u!==s&&(u===null?g.firstBaseUpdate=f:u.next=f,g.lastBaseUpdate=a))}if(i!==null){var h=l.baseState;s=0,g=f=a=null,u=i;do{var m=u.lane,x=u.eventTime;if((r&m)===m){g!==null&&(g=g.next={eventTime:x,lane:0,tag:u.tag,payload:u.payload,callback:u.callback,next:null});e:{var w=e,S=u;switch(m=t,x=n,S.tag){case 1:if(w=S.payload,typeof w=="function"){h=w.call(x,h,m);break e}h=w;break e;case 3:w.flags=w.flags&-65537|128;case 0:if(w=S.payload,m=typeof w=="function"?w.call(x,h,m):w,m==null)break e;h=H({},h,m);break e;case 2:tt=!0}}u.callback!==null&&u.lane!==0&&(e.flags|=64,m=l.effects,m===null?l.effects=[u]:m.push(u))}else x={eventTime:x,lane:m,tag:u.tag,payload:u.payload,callback:u.callback,next:null},g===null?(f=g=x,a=h):g=g.next=x,s|=m;if(u=u.next,u===null){if(u=l.shared.pending,u===null)break;m=u,u=m.next,m.next=null,l.lastBaseUpdate=m,l.shared.pending=null}}while(!0);if(g===null&&(a=h),l.baseState=a,l.firstBaseUpdate=f,l.lastBaseUpdate=g,t=l.shared.interleaved,t!==null){l=t;do s|=l.lane,l=l.next;while(l!==t)}else i===null&&(l.shared.lanes=0);Rt|=s,e.lanes=s,e.memoizedState=h}}function Mo(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=Gl.transition;Gl.transition={};try{e(!1),t()}finally{O=n,Gl.transition=r}}function Fa(){return Pe().memoizedState}function mf(e,t,n){var r=ft(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},Da(e))Ua(t,n);else if(n=ya(e,t,n,r),n!==null){var l=ae();Oe(n,e,r,l),Aa(n,t,r)}}function vf(e,t,n){var r=ft(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(Da(e))Ua(t,l);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var s=t.lastRenderedState,u=i(s,n);if(l.hasEagerState=!0,l.eagerState=u,Fe(u,s)){var a=t.interleaved;a===null?(l.next=l,js(t)):(l.next=a.next,a.next=l),t.interleaved=l;return}}catch{}finally{}n=ya(e,t,l,r),n!==null&&(l=ae(),Oe(n,e,r,l),Aa(n,t,r))}}function Da(e){var t=e.alternate;return e===B||t!==null&&t===B}function Ua(e,t){Mn=el=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Aa(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,as(e,n)}}var tl={readContext:ze,useCallback:le,useContext:le,useEffect:le,useImperativeHandle:le,useInsertionEffect:le,useLayoutEffect:le,useMemo:le,useReducer:le,useRef:le,useState:le,useDebugValue:le,useDeferredValue:le,useTransition:le,useMutableSource:le,useSyncExternalStore:le,useId:le,unstable_isNewReconciler:!1},gf={readContext:ze,useCallback:function(e,t){return Ae().memoizedState=[e,t===void 0?null:t],e},useContext:ze,useEffect:Oo,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Rr(4194308,4,La.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Rr(4194308,4,e,t)},useInsertionEffect:function(e,t){return Rr(4,2,e,t)},useMemo:function(e,t){var n=Ae();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ae();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=mf.bind(null,B,e),[r.memoizedState,e]},useRef:function(e){var t=Ae();return e={current:e},t.memoizedState=e},useState:Io,useDebugValue:Rs,useDeferredValue:function(e){return Ae().memoizedState=e},useTransition:function(){var e=Io(!1),t=e[0];return e=hf.bind(null,e[1]),Ae().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=B,l=Ae();if(V){if(n===void 0)throw Error(y(407));n=n()}else{if(n=t(),ee===null)throw Error(y(349));Lt&30||ja(r,t,n)}l.memoizedState=n;var i={value:n,getSnapshot:t};return l.queue=i,Oo(Na.bind(null,r,i,e),[e]),r.flags|=2048,qn(9,Ca.bind(null,r,i,n,t),void 0,null),n},useId:function(){var e=Ae(),t=ee.identifierPrefix;if(V){var n=Ke,r=Qe;n=(r&~(1<<32-Ie(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Zn++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=s.createElement(n,{is:r.is}):(e=s.createElement(n),n==="select"&&(s=e,r.multiple?s.multiple=!0:r.size&&(s.size=r.size))):e=s.createElementNS(e,n),e[$e]=t,e[Yn]=r,Xa(e,t,!1,!1),t.stateNode=e;e:{switch(s=fi(n,r),n){case"dialog":A("cancel",e),A("close",e),l=r;break;case"iframe":case"object":case"embed":A("load",e),l=r;break;case"video":case"audio":for(l=0;l<_n.length;l++)A(_n[l],e);l=r;break;case"source":A("error",e),l=r;break;case"img":case"image":case"link":A("error",e),A("load",e),l=r;break;case"details":A("toggle",e),l=r;break;case"input":bs(e,r),l=si(e,r),A("invalid",e);break;case"option":l=r;break;case"select":e._wrapperState={wasMultiple:!!r.multiple},l=H({},r,{value:void 0}),A("invalid",e);break;case"textarea":to(e,r),l=ai(e,r),A("invalid",e);break;default:l=r}di(n,l),u=l;for(i in u)if(u.hasOwnProperty(i)){var a=u[i];i==="style"?zu(e,a):i==="dangerouslySetInnerHTML"?(a=a?a.__html:void 0,a!=null&&Eu(e,a)):i==="children"?typeof a=="string"?(n!=="textarea"||a!=="")&&Un(e,a):typeof a=="number"&&Un(e,""+a):i!=="suppressContentEditableWarning"&&i!=="suppressHydrationWarning"&&i!=="autoFocus"&&(Dn.hasOwnProperty(i)?a!=null&&i==="onScroll"&&A("scroll",e):a!=null&&rs(e,i,a,s))}switch(n){case"input":cr(e),eo(e,r,!1);break;case"textarea":cr(e),no(e);break;case"option":r.value!=null&&e.setAttribute("value",""+ht(r.value));break;case"select":e.multiple=!!r.multiple,i=r.value,i!=null?Jt(e,!!r.multiple,i,!1):r.defaultValue!=null&&Jt(e,!!r.multiple,r.defaultValue,!0);break;default:typeof l.onClick=="function"&&(e.onclick=Qr)}switch(n){case"button":case"input":case"select":case"textarea":r=!!r.autoFocus;break e;case"img":r=!0;break e;default:r=!1}}r&&(t.flags|=4)}t.ref!==null&&(t.flags|=512,t.flags|=2097152)}return ie(t),null;case 6:if(e&&t.stateNode!=null)Ja(e,t,e.memoizedProps,r);else{if(typeof r!="string"&&t.stateNode===null)throw Error(y(166));if(n=Nt(Xn.current),Nt(We.current),xr(t)){if(r=t.stateNode,n=t.memoizedProps,r[$e]=t,(i=r.nodeValue!==n)&&(e=we,e!==null))switch(e.tag){case 3:yr(r.nodeValue,n,(e.mode&1)!==0);break;case 5:e.memoizedProps.suppressHydrationWarning!==!0&&yr(r.nodeValue,n,(e.mode&1)!==0)}i&&(t.flags|=4)}else r=(n.nodeType===9?n:n.ownerDocument).createTextNode(r),r[$e]=t,t.stateNode=r}return ie(t),null;case 13:if($(W),r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(V&&xe!==null&&t.mode&1&&!(t.flags&128))ma(),sn(),t.flags|=98560,i=!1;else if(i=xr(t),r!==null&&r.dehydrated!==null){if(e===null){if(!i)throw Error(y(318));if(i=t.memoizedState,i=i!==null?i.dehydrated:null,!i)throw Error(y(317));i[$e]=t}else sn(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;ie(t),i=!1}else Me!==null&&(Ki(Me),Me=null),i=!0;if(!i)return t.flags&65536?t:null}return t.flags&128?(t.lanes=n,t):(r=r!==null,r!==(e!==null&&e.memoizedState!==null)&&r&&(t.child.flags|=8192,t.mode&1&&(e===null||W.current&1?J===0&&(J=3):As())),t.updateQueue!==null&&(t.flags|=4),ie(t),null);case 4:return un(),Ui(e,t),e===null&&Qn(t.stateNode.containerInfo),ie(t),null;case 10:return ks(t.type._context),ie(t),null;case 17:return me(t.type)&&Kr(),ie(t),null;case 19:if($(W),i=t.memoizedState,i===null)return ie(t),null;if(r=(t.flags&128)!==0,s=i.rendering,s===null)if(r)kn(i,!1);else{if(J!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(s=br(e),s!==null){for(t.flags|=128,kn(i,!1),r=s.updateQueue,r!==null&&(t.updateQueue=r,t.flags|=4),t.subtreeFlags=0,r=n,n=t.child;n!==null;)i=n,e=r,i.flags&=14680066,s=i.alternate,s===null?(i.childLanes=0,i.lanes=e,i.child=null,i.subtreeFlags=0,i.memoizedProps=null,i.memoizedState=null,i.updateQueue=null,i.dependencies=null,i.stateNode=null):(i.childLanes=s.childLanes,i.lanes=s.lanes,i.child=s.child,i.subtreeFlags=0,i.deletions=null,i.memoizedProps=s.memoizedProps,i.memoizedState=s.memoizedState,i.updateQueue=s.updateQueue,i.type=s.type,e=s.dependencies,i.dependencies=e===null?null:{lanes:e.lanes,firstContext:e.firstContext}),n=n.sibling;return U(W,W.current&1|2),t.child}e=e.sibling}i.tail!==null&&G()>cn&&(t.flags|=128,r=!0,kn(i,!1),t.lanes=4194304)}else{if(!r)if(e=br(s),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),kn(i,!0),i.tail===null&&i.tailMode==="hidden"&&!s.alternate&&!V)return ie(t),null}else 2*G()-i.renderingStartTime>cn&&n!==1073741824&&(t.flags|=128,r=!0,kn(i,!1),t.lanes=4194304);i.isBackwards?(s.sibling=t.child,t.child=s):(n=i.last,n!==null?n.sibling=s:t.child=s,i.last=s)}return i.tail!==null?(t=i.tail,i.rendering=t,i.tail=t.sibling,i.renderingStartTime=G(),t.sibling=null,n=W.current,U(W,r?n&1|2:n&1),t):(ie(t),null);case 22:case 23:return Us(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?ye&1073741824&&(ie(t),t.subtreeFlags&6&&(t.flags|=8192)):ie(t),null;case 24:return null;case 25:return null}throw Error(y(156,t.tag))}function Nf(e,t){switch(ys(t),t.tag){case 1:return me(t.type)&&Kr(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return un(),$(he),$(oe),_s(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return Es(t),null;case 13:if($(W),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(y(340));sn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return $(W),null;case 4:return un(),null;case 10:return ks(t.type._context),null;case 22:case 23:return Us(),null;case 24:return null;default:return null}}var kr=!1,se=!1,Ef=typeof WeakSet=="function"?WeakSet:Set,j=null;function Gt(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){Q(e,t,r)}else n.current=null}function Ai(e,t,n){try{n()}catch(r){Q(e,t,r)}}var Ko=!1;function _f(e,t){if(ki=Wr,e=na(),vs(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,i=r.focusNode;r=r.focusOffset;try{n.nodeType,i.nodeType}catch{n=null;break e}var s=0,u=-1,a=-1,f=0,g=0,h=e,m=null;t:for(;;){for(var x;h!==n||l!==0&&h.nodeType!==3||(u=s+l),h!==i||r!==0&&h.nodeType!==3||(a=s+r),h.nodeType===3&&(s+=h.nodeValue.length),(x=h.firstChild)!==null;)m=h,h=x;for(;;){if(h===e)break t;if(m===n&&++f===l&&(u=s),m===i&&++g===r&&(a=s),(x=h.nextSibling)!==null)break;h=m,m=h.parentNode}h=x}n=u===-1||a===-1?null:{start:u,end:a}}else n=null}n=n||{start:0,end:0}}else n=null;for(ji={focusedElem:e,selectionRange:n},Wr=!1,j=t;j!==null;)if(t=j,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,j=e;else for(;j!==null;){t=j;try{var w=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(w!==null){var S=w.memoizedProps,z=w.memoizedState,d=t.stateNode,c=d.getSnapshotBeforeUpdate(t.elementType===t.type?S:Le(t.type,S),z);d.__reactInternalSnapshotBeforeUpdate=c}break;case 3:var p=t.stateNode.containerInfo;p.nodeType===1?p.textContent="":p.nodeType===9&&p.documentElement&&p.removeChild(p.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(y(163))}}catch(v){Q(t,t.return,v)}if(e=t.sibling,e!==null){e.return=t.return,j=e;break}j=t.return}return w=Ko,Ko=!1,w}function In(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var i=l.destroy;l.destroy=void 0,i!==void 0&&Ai(t,n,i)}l=l.next}while(l!==r)}}function gl(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function $i(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function qa(e){var t=e.alternate;t!==null&&(e.alternate=null,qa(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[$e],delete t[Yn],delete t[Ei],delete t[af],delete t[cf])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function ba(e){return e.tag===5||e.tag===3||e.tag===4}function Yo(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||ba(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Vi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Qr));else if(r!==4&&(e=e.child,e!==null))for(Vi(e,t,n),e=e.sibling;e!==null;)Vi(e,t,n),e=e.sibling}function Wi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Wi(e,t,n),e=e.sibling;e!==null;)Wi(e,t,n),e=e.sibling}var te=null,Re=!1;function be(e,t,n){for(n=n.child;n!==null;)ec(e,t,n),n=n.sibling}function ec(e,t,n){if(Ve&&typeof Ve.onCommitFiberUnmount=="function")try{Ve.onCommitFiberUnmount(al,n)}catch{}switch(n.tag){case 5:se||Gt(n,t);case 6:var r=te,l=Re;te=null,be(e,t,n),te=r,Re=l,te!==null&&(Re?(e=te,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):te.removeChild(n.stateNode));break;case 18:te!==null&&(Re?(e=te,n=n.stateNode,e.nodeType===8?Ql(e.parentNode,n):e.nodeType===1&&Ql(e,n),Wn(e)):Ql(te,n.stateNode));break;case 4:r=te,l=Re,te=n.stateNode.containerInfo,Re=!0,be(e,t,n),te=r,Re=l;break;case 0:case 11:case 14:case 15:if(!se&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var i=l,s=i.destroy;i=i.tag,s!==void 0&&(i&2||i&4)&&Ai(n,t,s),l=l.next}while(l!==r)}be(e,t,n);break;case 1:if(!se&&(Gt(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(u){Q(n,t,u)}be(e,t,n);break;case 21:be(e,t,n);break;case 22:n.mode&1?(se=(r=se)||n.memoizedState!==null,be(e,t,n),se=r):be(e,t,n);break;default:be(e,t,n)}}function Go(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Ef),t.forEach(function(r){var l=Ff.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function Te(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=s),r&=~i}if(r=l,r=G()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Pf(r/1960))-r,10e?16:e,it===null)var r=!1;else{if(e=it,it=null,ll=0,I&6)throw Error(y(331));var l=I;for(I|=4,j=e.current;j!==null;){var i=j,s=i.child;if(j.flags&16){var u=i.deletions;if(u!==null){for(var a=0;aG()-Fs?_t(e,0):Os|=n),ve(e,t)}function uc(e,t){t===0&&(e.mode&1?(t=pr,pr<<=1,!(pr&130023424)&&(pr=4194304)):t=1);var n=ae();e=Ze(e,t),e!==null&&(tr(e,t,n),ve(e,n))}function Of(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),uc(e,n)}function Ff(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(y(314))}r!==null&&r.delete(t),uc(e,n)}var ac;ac=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||he.current)pe=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return pe=!1,jf(e,t,n);pe=!!(e.flags&131072)}else pe=!1,V&&t.flags&1048576&&pa(t,Xr,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Mr(e,t),e=t.pendingProps;var l=ln(t,oe.current);tn(t,n),l=Ps(null,t,r,e,l,n);var i=Ts();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,me(r)?(i=!0,Yr(t)):i=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,Cs(t),l.updater=vl,t.stateNode=l,l._reactInternals=t,Ri(t,r,e,n),t=Oi(null,t,r,!0,i,n)):(t.tag=0,V&&i&&gs(t),ue(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Mr(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=Uf(r),e=Le(r,e),l){case 0:t=Ii(null,t,r,e,n);break e;case 1:t=Bo(null,t,r,e,n);break e;case 11:t=Vo(null,t,r,e,n);break e;case 14:t=Wo(null,t,r,Le(r.type,e),n);break e}throw Error(y(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Le(r,l),Ii(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Le(r,l),Bo(e,t,r,l,n);case 3:e:{if(Ka(t),e===null)throw Error(y(387));r=t.pendingProps,i=t.memoizedState,l=i.element,xa(e,t),qr(t,r,null,n);var s=t.memoizedState;if(r=s.element,i.isDehydrated)if(i={element:r,isDehydrated:!1,cache:s.cache,pendingSuspenseBoundaries:s.pendingSuspenseBoundaries,transitions:s.transitions},t.updateQueue.baseState=i,t.memoizedState=i,t.flags&256){l=an(Error(y(423)),t),t=Ho(e,t,r,n,l);break e}else if(r!==l){l=an(Error(y(424)),t),t=Ho(e,t,r,n,l);break e}else for(xe=at(t.stateNode.containerInfo.firstChild),we=t,V=!0,Me=null,n=ga(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(sn(),r===l){t=Je(e,t,n);break e}ue(e,t,r,n)}t=t.child}return t;case 5:return wa(t),e===null&&Pi(t),r=t.type,l=t.pendingProps,i=e!==null?e.memoizedProps:null,s=l.children,Ci(r,l)?s=null:i!==null&&Ci(r,i)&&(t.flags|=32),Qa(e,t),ue(e,t,s,n),t.child;case 6:return e===null&&Pi(t),null;case 13:return Ya(e,t,n);case 4:return Ns(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=on(t,null,r,n):ue(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Le(r,l),Vo(e,t,r,l,n);case 7:return ue(e,t,t.pendingProps,n),t.child;case 8:return ue(e,t,t.pendingProps.children,n),t.child;case 12:return ue(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,i=t.memoizedProps,s=l.value,U(Zr,r._currentValue),r._currentValue=s,i!==null)if(Fe(i.value,s)){if(i.children===l.children&&!he.current){t=Je(e,t,n);break e}}else for(i=t.child,i!==null&&(i.return=t);i!==null;){var u=i.dependencies;if(u!==null){s=i.child;for(var a=u.firstContext;a!==null;){if(a.context===r){if(i.tag===1){a=Ye(-1,n&-n),a.tag=2;var f=i.updateQueue;if(f!==null){f=f.shared;var g=f.pending;g===null?a.next=a:(a.next=g.next,g.next=a),f.pending=a}}i.lanes|=n,a=i.alternate,a!==null&&(a.lanes|=n),Ti(i.return,n,t),u.lanes|=n;break}a=a.next}}else if(i.tag===10)s=i.type===t.type?null:i.child;else if(i.tag===18){if(s=i.return,s===null)throw Error(y(341));s.lanes|=n,u=s.alternate,u!==null&&(u.lanes|=n),Ti(s,n,t),s=i.sibling}else s=i.child;if(s!==null)s.return=i;else for(s=i;s!==null;){if(s===t){s=null;break}if(i=s.sibling,i!==null){i.return=s.return,s=i;break}s=s.return}i=s}ue(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,tn(t,n),l=ze(l),r=r(l),t.flags|=1,ue(e,t,r,n),t.child;case 14:return r=t.type,l=Le(r,t.pendingProps),l=Le(r.type,l),Wo(e,t,r,l,n);case 15:return Ba(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Le(r,l),Mr(e,t),t.tag=1,me(r)?(e=!0,Yr(t)):e=!1,tn(t,n),$a(t,r,l),Ri(t,r,l,n),Oi(null,t,r,!0,e,n);case 19:return Ga(e,t,n);case 22:return Ha(e,t,n)}throw Error(y(156,t.tag))};function cc(e,t){return Du(e,t)}function Df(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Ee(e,t,n,r){return new Df(e,t,n,r)}function $s(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Uf(e){if(typeof e=="function")return $s(e)?1:0;if(e!=null){if(e=e.$$typeof,e===is)return 11;if(e===ss)return 14}return 2}function pt(e,t){var n=e.alternate;return n===null?(n=Ee(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Fr(e,t,n,r,l,i){var s=2;if(r=e,typeof e=="function")$s(e)&&(s=1);else if(typeof e=="string")s=5;else e:switch(e){case At:return zt(n.children,l,i,t);case ls:s=8,l|=8;break;case ni:return e=Ee(12,n,t,l|2),e.elementType=ni,e.lanes=i,e;case ri:return e=Ee(13,n,t,l),e.elementType=ri,e.lanes=i,e;case li:return e=Ee(19,n,t,l),e.elementType=li,e.lanes=i,e;case wu:return xl(n,l,i,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case yu:s=10;break e;case xu:s=9;break e;case is:s=11;break e;case ss:s=14;break e;case et:s=16,r=null;break e}throw Error(y(130,e==null?e:typeof e,""))}return t=Ee(s,n,t,l),t.elementType=e,t.type=r,t.lanes=i,t}function zt(e,t,n,r){return e=Ee(7,e,r,t),e.lanes=n,e}function xl(e,t,n,r){return e=Ee(22,e,r,t),e.elementType=wu,e.lanes=n,e.stateNode={isHidden:!1},e}function bl(e,t,n){return e=Ee(6,e,null,t),e.lanes=n,e}function ei(e,t,n){return t=Ee(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Af(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Il(0),this.expirationTimes=Il(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Il(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Vs(e,t,n,r,l,i,s,u,a){return e=new Af(e,t,n,u,a),t===1?(t=1,i===!0&&(t|=8)):t=0,i=Ee(3,null,null,t),e.current=i,i.stateNode=e,i.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Cs(i),e}function $f(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(hc)}catch(e){console.error(e)}}hc(),hu.exports=ke;var Qf=hu.exports,mc,nu=Qf;mc=nu.createRoot,nu.hydrateRoot;const vc=[{id:"messages",label:"消息",description:"查找会话、阅读并回复",icon:"message"},{id:"broadcast",label:"群发",description:"创建受控群发任务",icon:"send"},{id:"contacts",label:"通讯录",description:"查找联系人和群聊",icon:"contacts"},{id:"tasks",label:"任务",description:"查看执行结果",icon:"tasks"}],gc={Online:"在线",Degraded:"需要注意",Offline:"已离线",Registered:"已注册",SessionLocked:"桌面已锁定",WechatNotRunning:"微信未运行",WechatNotLoggedIn:"微信未登录",Pending:"待处理",WaitingForClient:"等待 Client",Accepted:"已接收",Running:"执行中",Succeeded:"已完成",Failed:"失败",Cancelled:"已取消",Expired:"已过期",ResultUnconfirmed:"待核对"},Kf=new Set(["Succeeded","Failed","Cancelled","Expired","ResultUnconfirmed"]);class Zt extends Error{constructor(t,n,r="RequestFailed"){super(t),this.status=n,this.code=r}}async function Et(e,{token:t,method:n="GET",body:r}={}){var u,a;const l={};t&&(l.Authorization=`Bearer ${t}`),r!==void 0&&(l["Content-Type"]="application/json");const i=await fetch(e,{method:n,headers:l,body:r===void 0?void 0:JSON.stringify(r)}),s=await i.json().catch(()=>({}));if(!i.ok)throw new Zt(((u=s.error)==null?void 0:u.message)||`请求失败(${i.status})`,i.status,(a=s.error)==null?void 0:a.code);return s}const Yf=e=>new Promise(t=>window.setTimeout(t,e)),Gf={ReportingDisabled:"Client 未启用 Reporting 白名单。请在托盘远程连接页启用并配置白名单。",ReportingConfigInvalid:"Client 的 Reporting 配置无效,请检查托盘远程连接页。",AccountNotAuthorized:"当前账号未加入 Client 的 Reporting 白名单。",ChatNotAuthorized:"当前会话不在 Client 的 Reporting 白名单中。",ChatIdentityUnconfirmed:"当前会话身份尚未确认,暂不能读取。",DataTypeNotAuthorized:"当前读取类型未被 Reporting 白名单授权。"};function Xf(e){var n,r;const t=((n=e.result)==null?void 0:n.error_code)||e.status;return Gf[t]||((r=e.result)==null?void 0:r.message)||`读取任务${gc[e.status]||"未完成"}。`}async function Zf(e,t){var i,s;const n=Date.now()+3e4;let r=250,l={status:"Pending"};for(;Date.now()t?`${e.slice(0,t)}…`:e:"—"}function Jf(e){return["Online","Succeeded","Accepted"].includes(e)?"positive":["Running","Pending","WaitingForClient","Degraded"].includes(e)?"warning":["Failed","Cancelled","Expired","ResultUnconfirmed","Offline","SessionLocked","WechatNotRunning","WechatNotLoggedIn"].includes(e)?"negative":"neutral"}function yc(e){return gc[e]||e||"未知"}function ol(e){return!!(e&&["Online","Degraded","Registered"].includes(e.status))}function Qs(e){var t,n;return e?e.active_account_id?e.active_account_id:((n=(t=e.accounts)==null?void 0:t.find(r=>r.active&&r.verified))==null?void 0:n.account_id)||"":""}function Ks(e,t){if(Array.isArray(e))return e;for(const n of t)if(Array.isArray(e==null?void 0:e[n]))return e[n];return[]}function qf(e){return Ks(e,["items","sessions","chats"]).map((t,n)=>({id:String(t.chat_id??t.chatId??t.session_id??t.sessionId??t.automation_id??t.automationId??t.id??n),title:String(t.name??t.display_name??t.displayName??t.nickname??t.chat_id??t.id??"未命名会话"),preview:String(t.last_message??t.lastMessage??t.preview??"暂无最近消息"),unread:Number(t.unread_count??t.unreadCount??0),type:t.chat_type??t.chatType??"Private"}))}function bf(e){return Ks(e,["items","messages"]).map((t,n)=>({id:String(t.message_id??t.messageId??t.id??n),sender:String(t.sender??t.sender_name??t.senderName??"对方"),text:typeof t=="string"?t:String(t.content??t.text??t.message??""),at:t.occurred_at??t.occurredAt??t.created_at??t.createdAt}))}function ep(e){return Ks(e,["items","contacts","sessions"]).map((t,n)=>({id:String(t.contact_id??t.contactId??t.chat_id??t.chatId??t.id??n),title:String(t.name??t.display_name??t.displayName??t.nickname??t.contact_id??t.id??"未命名联系人"),type:t.chat_type??t.chatType??(t.is_group?"Group":"Private"),detail:String(t.remark??t.alias??t.account_id??t.accountId??"")}))}function K({name:e,size:t=18}){const n={message:o.jsxs(o.Fragment,{children:[o.jsx("path",{d:"M4 5.5A2.5 2.5 0 0 1 6.5 3h11A2.5 2.5 0 0 1 20 5.5v7a2.5 2.5 0 0 1-2.5 2.5H11l-4.5 3v-3h0A2.5 2.5 0 0 1 4 12.5z"}),o.jsx("path",{d:"M8 8h8M8 11h5"})]}),send:o.jsxs(o.Fragment,{children:[o.jsx("path",{d:"m3 4 18 8-18 8 3.5-8z"}),o.jsx("path",{d:"M6.5 12H21"})]}),contacts:o.jsxs(o.Fragment,{children:[o.jsx("circle",{cx:"9",cy:"8",r:"3"}),o.jsx("path",{d:"M3.5 19a5.5 5.5 0 0 1 11 0M17 7v6M14 10h6"})]}),tasks:o.jsxs(o.Fragment,{children:[o.jsx("rect",{x:"4",y:"3",width:"16",height:"18",rx:"2"}),o.jsx("path",{d:"M8 8h8M8 12h8M8 16h5"})]}),settings:o.jsxs(o.Fragment,{children:[o.jsx("path",{d:"M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M18.4 5.6l-2.1 2.1M7.7 16.3l-2.1 2.1"}),o.jsx("circle",{cx:"12",cy:"12",r:"4"})]}),refresh:o.jsxs(o.Fragment,{children:[o.jsx("path",{d:"M20 11a8 8 0 0 0-14.5-4.5L4 8"}),o.jsx("path",{d:"M4 4v4h4M4 13a8 8 0 0 0 14.5 4.5L20 16"}),o.jsx("path",{d:"M20 20v-4h-4"})]}),chevron:o.jsx("path",{d:"m9 6 6 6-6 6"}),search:o.jsxs(o.Fragment,{children:[o.jsx("circle",{cx:"10.5",cy:"10.5",r:"6.5"}),o.jsx("path",{d:"m16 16 4.5 4.5"})]}),shield:o.jsxs(o.Fragment,{children:[o.jsx("path",{d:"M12 3 19 6v5c0 4.2-2.8 7.8-7 9-4.2-1.2-7-4.8-7-9V6z"}),o.jsx("path",{d:"m9 12 2 2 4-4"})]}),alert:o.jsxs(o.Fragment,{children:[o.jsx("path",{d:"M12 4 21 20H3z"}),o.jsx("path",{d:"M12 10v4M12 17v.2"})]}),close:o.jsx(o.Fragment,{children:o.jsx("path",{d:"m6 6 12 12M18 6 6 18"})}),logout:o.jsx(o.Fragment,{children:o.jsx("path",{d:"M10 4H5v16h5M14 8l4 4-4 4M8 12h10"})}),arrow:o.jsx(o.Fragment,{children:o.jsx("path",{d:"M4 12h15M13 6l6 6-6 6"})}),check:o.jsx("path",{d:"m5 12 4 4L19 6"})};return o.jsx("svg",{"aria-hidden":"true",width:t,height:t,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.8",strokeLinecap:"round",strokeLinejoin:"round",children:n[e]||n.settings})}function fn({value:e}){return o.jsxs("span",{className:`status-badge ${Jf(e)}`,children:[o.jsx("i",{}),yc(e)]})}function tp({onLogin:e}){const[t,n]=T.useState(""),[r,l]=T.useState(""),[i,s]=T.useState(""),[u,a]=T.useState(!1),f=async g=>{g.preventDefault(),s(""),a(!0);try{const h=await Et("/v1/auth/login",{method:"POST",body:{username:t,password:r}});e(h.access_token,t)}catch(h){s(h.message)}finally{a(!1)}};return o.jsx("main",{className:"login-page",children:o.jsxs("section",{className:"login-panel","aria-labelledby":"login-title",children:[o.jsx("div",{className:"brand-mark",children:"W"}),o.jsx("p",{className:"eyebrow",children:"WXAGENT WORKSPACE"}),o.jsx("h1",{id:"login-title",children:"进入工作台"}),o.jsx("p",{className:"login-intro",children:"连接已授权的 Client,处理消息、通讯录和任务结果。"}),o.jsxs("form",{onSubmit:f,className:"login-form",children:[o.jsxs("label",{children:["用户名",o.jsx("input",{autoFocus:!0,value:t,onChange:g=>n(g.target.value),autoComplete:"username"})]}),o.jsxs("label",{children:["密码",o.jsx("input",{type:"password",value:r,onChange:g=>l(g.target.value),autoComplete:"current-password"})]}),i&&o.jsx("div",{className:"form-error",role:"alert",children:i}),o.jsx("button",{className:"primary-button full",disabled:u||!t||!r,children:u?"登录中…":"登录工作台"})]}),o.jsxs("p",{className:"login-footnote",children:[o.jsx("span",{className:"secure-dot"})," 会话只保存在当前浏览器标签页"]})]})})}function np({view:e,setView:t,onLogout:n,username:r}){return o.jsxs("aside",{className:"sidebar",children:[o.jsxs("div",{className:"sidebar-brand",children:[o.jsx("div",{className:"brand-mark small",children:"W"}),o.jsxs("div",{children:[o.jsx("strong",{children:"WxAgent"}),o.jsx("span",{children:"用户工作台"})]})]}),o.jsx("p",{className:"nav-caption",children:"工作区"}),o.jsx("nav",{"aria-label":"主导航",children:vc.map(l=>o.jsxs("button",{className:e===l.id?"nav-item active":"nav-item",onClick:()=>t(l.id),children:[o.jsx(K,{name:l.icon,size:18}),o.jsx("span",{children:l.label})]},l.id))}),o.jsx("div",{className:"sidebar-spacer"}),o.jsxs("button",{className:e==="settings"?"nav-item active":"nav-item",onClick:()=>t("settings"),children:[o.jsx(K,{name:"settings",size:18}),o.jsx("span",{children:"设置与诊断"})]}),o.jsxs("div",{className:"connection-hint",children:[o.jsx("span",{className:"secure-dot"}),o.jsxs("div",{children:[o.jsx("strong",{children:"安全连接"}),o.jsx("small",{children:"控制面 API 已认证"})]})]}),o.jsxs("div",{className:"user-menu",children:[o.jsx("div",{className:"avatar",children:(r||"A").slice(0,1).toUpperCase()}),o.jsxs("div",{className:"user-name",children:[o.jsx("strong",{children:r||"管理员"}),o.jsx("small",{children:"已登录"})]}),o.jsx("button",{title:"退出登录","aria-label":"退出登录",onClick:n,children:o.jsx(K,{name:"logout",size:17})})]})]})}function rp({title:e,subtitle:t,nodes:n,selectedClientId:r,onClientChange:l,onRefresh:i,loading:s}){const u=n.find(a=>a.node_id===r);return o.jsxs("header",{className:"topbar",children:[o.jsxs("div",{className:"topbar-copy",children:[o.jsx("h1",{children:e}),o.jsx("p",{children:t})]}),o.jsxs("div",{className:"topbar-actions",children:[o.jsxs("label",{className:"client-switcher",children:[o.jsx("span",{children:"当前 Client"}),o.jsxs("select",{value:r,onChange:a=>l(a.target.value),disabled:!n.length,children:[!n.length&&o.jsx("option",{value:"",children:"暂无 Client"}),n.map(a=>o.jsxs("option",{value:a.node_id,children:[a.node_id," · ",yc(a.status)]},a.node_id))]})]}),u&&o.jsx(fn,{value:u.status}),o.jsxs("button",{className:"refresh-button",onClick:i,disabled:s,"aria-label":"刷新数据",children:[o.jsx("span",{className:s?"spin":"",children:o.jsx(K,{name:"refresh",size:16})}),s?"同步中":"刷新"]})]})]})}function lp({nodes:e,selectedNode:t,onSettings:n}){if(!e.length)return o.jsxs("div",{className:"connection-banner negative",children:[o.jsx(K,{name:"alert",size:18}),o.jsxs("div",{children:[o.jsx("strong",{children:"还没有可用 Client"}),o.jsx("span",{children:"请启动已配置的 Desktop Agent;浏览器不会模拟连接状态。"})]}),o.jsx("button",{className:"text-button",onClick:n,children:"查看连接说明"})]});if(!ol(t)){const r=e.some(ol);return o.jsxs("div",{className:"connection-banner warning",children:[o.jsx(K,{name:"alert",size:18}),o.jsxs("div",{children:[o.jsx("strong",{children:"当前 Client 不可用"}),o.jsx("span",{children:r?"其它 Client 不受影响,请从顶部切换。":"所有 Client 当前都不可用,请先恢复连接。"})]}),o.jsx("button",{className:"text-button",onClick:n,children:"设置与诊断"})]})}return Qs(t)?null:o.jsxs("div",{className:"connection-banner warning",children:[o.jsx(K,{name:"alert",size:18}),o.jsxs("div",{children:[o.jsx("strong",{children:"当前 Client 尚未确认微信账号"}),o.jsx("span",{children:"只读数据需要已验证的活动账号,先完成账号绑定后再操作。"})]}),o.jsx("button",{className:"text-button",onClick:n,children:"查看账号"})]})}function Gi({title:e="没有选中的 Client",text:t="请先连接并选择一个可用 Client。",onSettings:n}){return o.jsxs("div",{className:"context-empty",children:[o.jsx("div",{className:"empty-icon",children:o.jsx(K,{name:"shield",size:28})}),o.jsx("h3",{children:e}),o.jsx("p",{children:t}),n&&o.jsx("button",{className:"secondary-button",onClick:n,children:"打开设置与诊断"})]})}function Xi({text:e="正在读取…"}){return o.jsxs("div",{className:"loading-state",children:[o.jsx("span",{className:"loader"}),e]})}function It({text:e,action:t}){return o.jsxs("div",{className:"empty-state",children:[o.jsx("div",{className:"empty-icon",children:o.jsx(K,{name:"search",size:26})}),o.jsx("p",{children:e}),t]})}function Zi({error:e,onRetry:t}){return o.jsxs("div",{className:"inline-error",role:"alert",children:[o.jsx(K,{name:"alert",size:17}),o.jsx("span",{children:e}),t&&o.jsx("button",{className:"text-button",onClick:t,children:"重试"})]})}function ip({token:e,client:t,onSettings:n}){const r=(t==null?void 0:t.node_id)||"",l=Qs(t),[i,s]=T.useState([]),[u,a]=T.useState({loading:!1,error:""}),[f,g]=T.useState(""),[h,m]=T.useState(null),[x,w]=T.useState([]),[S,z]=T.useState({loading:!1,error:""}),d=T.useRef(0),c=T.useCallback(async()=>{if(!r||!l)return;const v=++d.current;a({loading:!0,error:""});try{const k=await Yi("/v1/reads/sessions",{node_id:r,account_id:l,limit:100},e);if(v!==d.current)return;s(qf(k))}catch(k){v===d.current&&a({loading:!1,error:k.message});return}a({loading:!1,error:""})},[l,r,e]);if(T.useEffect(()=>{m(null),s([]),w([]),!(!r||!l)&&c()},[r,l,c]),T.useEffect(()=>{if(!h||h.clientId!==r||!r||!l)return;const v=++d.current;z({loading:!0,error:""}),Yi("/v1/reads/messages",{node_id:r,account_id:l,chat_id:h.id,limit:100,include_content:!0},e).then(k=>{v===d.current&&(w(bf(k)),z({loading:!1,error:""}))}).catch(k=>{v===d.current&&z({loading:!1,error:k.message})})},[l,r,h,e]),!t)return o.jsx(Gi,{onSettings:n});const p=i.filter(v=>!f||`${v.title} ${v.id}`.toLowerCase().includes(f.toLowerCase()));return o.jsxs("div",{className:"message-workspace",children:[o.jsxs("section",{className:"session-panel",children:[o.jsxs("div",{className:"section-heading compact",children:[o.jsxs("div",{children:[o.jsx("h2",{children:"会话"}),o.jsx("p",{children:i.length?`${i.length} 个会话`:"当前 Client 的会话"})]}),o.jsx("button",{className:"icon-button",onClick:c,disabled:u.loading,"aria-label":"刷新会话",children:o.jsx(K,{name:"refresh",size:16})})]}),o.jsxs("label",{className:"search-box",children:[o.jsx(K,{name:"search",size:16}),o.jsx("input",{value:f,onChange:v=>g(v.target.value),placeholder:"搜索会话"})]}),u.loading?o.jsx(Xi,{text:"正在读取会话…"}):u.error?o.jsx(Zi,{error:u.error,onRetry:c}):p.length===0?o.jsx(It,{text:"暂无会话,或当前账号还没有可见数据。"}):o.jsx("div",{className:"session-list",children:p.map(v=>o.jsxs("button",{className:(h==null?void 0:h.id)===v.id?"session-row selected":"session-row",onClick:()=>m({...v,clientId:r}),children:[o.jsx("span",{className:"session-avatar",children:v.title.slice(0,1).toUpperCase()}),o.jsxs("span",{className:"session-copy",children:[o.jsx("strong",{children:v.title}),o.jsx("small",{children:v.preview})]}),v.unread>0&&o.jsx("b",{children:v.unread})]},v.id))})]}),o.jsx("section",{className:"conversation-panel",children:h?o.jsxs(o.Fragment,{children:[o.jsxs("div",{className:"conversation-head",children:[o.jsxs("div",{children:[o.jsxs("p",{className:"eyebrow",children:["当前 Client · ",r]}),o.jsx("h2",{children:h.title}),o.jsxs("span",{children:[h.type==="Group"?"群聊":"私聊"," · ",Cl(h.id,28)]})]}),o.jsx(fn,{value:t.status})]}),S.loading?o.jsx(Xi,{text:"正在读取消息…"}):S.error?o.jsx(Zi,{error:S.error,onRetry:()=>m({...h})}):x.length===0?o.jsx(It,{text:"这个会话暂时没有可显示的消息。"}):o.jsx("div",{className:"message-list",children:x.map(v=>o.jsxs("article",{className:"message-row",children:[o.jsx("div",{className:"message-avatar",children:v.sender.slice(0,1).toUpperCase()}),o.jsxs("div",{children:[o.jsxs("div",{className:"message-meta",children:[o.jsx("strong",{children:v.sender}),o.jsx("span",{children:dn(v.at)})]}),o.jsx("p",{children:v.text||"(无正文)"})]})]},v.id))}),o.jsxs("div",{className:"composer",children:[o.jsx("textarea",{disabled:!0,rows:2,placeholder:"发送消息暂未开放,需完成 Windows 真机验收"}),o.jsx("button",{className:"primary-button",disabled:!0,title:"写操作尚未通过真机验收",children:"发送"})]})]}):o.jsx(Gi,{title:"选择一个会话",text:"从左侧选择会话后,读取该 Client 的消息。"})})]})}function sp({client:e,onSettings:t}){return o.jsxs("section",{className:"workspace-panel gated-panel",children:[o.jsx("div",{className:"gated-mark",children:o.jsx(K,{name:"shield",size:24})}),o.jsxs("div",{children:[o.jsx("p",{className:"eyebrow",children:"CONTROLLED ACTION"}),o.jsx("h2",{children:"受控群发验证"}),o.jsx("p",{children:"单 Client 的 Client 端已提供 broadcast-text:冻结获准对象名单,逐项串行发送,返回每个对象的结果,并支持停止和幂等重放。"}),o.jsxs("div",{className:"step-list",children:[o.jsxs("span",{children:[o.jsx("b",{children:"1"}),"冻结对象"]}),o.jsxs("span",{children:[o.jsx("b",{children:"2"}),"预览确认"]}),o.jsxs("span",{children:[o.jsx("b",{children:"3"}),"逐项发送"]}),o.jsxs("span",{children:[o.jsx("b",{children:"4"}),"查看结果"]})]}),o.jsx("button",{className:"primary-button",disabled:!0,children:e?"Web 提交仍需单独验收":"请先连接 Client"}),o.jsxs("button",{className:"text-button",onClick:t,children:["查看 Client 验证状态 ",o.jsx(K,{name:"arrow",size:14})]})]})]})}function op({token:e,client:t,onSettings:n}){const r=(t==null?void 0:t.node_id)||"",l=Qs(t),[i,s]=T.useState(!1),[u,a]=T.useState(""),[f,g]=T.useState([]),[h,m]=T.useState({loading:!1,error:""}),x=T.useRef(0),w=T.useCallback(async()=>{if(!r||!l)return;const z=++x.current;m({loading:!0,error:""});try{const d=await Yi("/v1/reads/contacts",{node_id:r,account_id:l,limit:200,groups_only:i,contains:u},e);if(z!==x.current)return;g(ep(d)),m({loading:!1,error:""})}catch(d){z===x.current&&m({loading:!1,error:d.message})}},[l,r,i,u,e]);if(T.useEffect(()=>{g([]),r&&l&&w()},[r,l,i]),!t)return o.jsx(Gi,{onSettings:n});const S=f.filter(z=>!u||`${z.title} ${z.id} ${z.detail}`.toLowerCase().includes(u.toLowerCase()));return o.jsxs("section",{className:"workspace-panel",children:[o.jsxs("div",{className:"section-heading",children:[o.jsxs("div",{children:[o.jsx("h2",{children:"通讯录"}),o.jsx("p",{children:"只读查看当前 Client 已授权的联系人和群聊。"})]}),o.jsxs("div",{className:"heading-actions",children:[o.jsxs("label",{className:"check-filter",children:[o.jsx("input",{type:"checkbox",checked:i,onChange:z=>s(z.target.checked)}),"只看群聊"]}),o.jsx("button",{className:"secondary-button",disabled:!0,title:"添加好友尚未通过真机验收",children:"添加好友"})]})]}),o.jsxs("div",{className:"toolbar",children:[o.jsxs("label",{className:"search-box wide",children:[o.jsx(K,{name:"search",size:16}),o.jsx("input",{value:u,onChange:z=>a(z.target.value),onKeyDown:z=>z.key==="Enter"&&w(),placeholder:"搜索联系人或群聊"})]}),o.jsx("button",{className:"secondary-button",onClick:w,disabled:h.loading,children:h.loading?"读取中…":"刷新"})]}),h.loading?o.jsx(Xi,{text:"正在读取通讯录…"}):h.error?o.jsx(Zi,{error:h.error,onRetry:w}):S.length===0?o.jsx(It,{text:"暂无可见联系人或群聊。"}):o.jsx("div",{className:"contact-list",children:S.map(z=>o.jsxs("div",{className:"contact-row",children:[o.jsx("span",{className:"contact-avatar",children:z.title.slice(0,1).toUpperCase()}),o.jsxs("div",{children:[o.jsx("strong",{children:z.title}),o.jsxs("small",{children:[z.type==="Group"?"群聊":"联系人"," · ",Cl(z.id,28),z.detail?` · ${z.detail}`:""]})]}),o.jsx("button",{className:"text-button",disabled:!0,title:"写操作尚未通过真机验收",children:"添加好友"})]},z.id))})]})}function up({tasks:e,selectedClientId:t}){const[n,r]=T.useState("all"),l=e.filter(i=>i.node_id===t&&(n==="all"||i.status===n));return o.jsxs("section",{className:"workspace-panel",children:[o.jsxs("div",{className:"section-heading",children:[o.jsxs("div",{children:[o.jsx("h2",{children:"任务结果"}),o.jsx("p",{children:"当前 Client 的任务状态和安全边界。"})]}),o.jsxs("div",{className:"heading-actions",children:[o.jsxs("span",{className:"read-only-note",children:[o.jsx(K,{name:"shield",size:14}),"只读视图"]}),o.jsxs("select",{value:n,onChange:i=>r(i.target.value),children:[o.jsx("option",{value:"all",children:"全部状态"}),o.jsx("option",{value:"Pending",children:"待处理"}),o.jsx("option",{value:"WaitingForClient",children:"等待 Client"}),o.jsx("option",{value:"Running",children:"执行中"}),o.jsx("option",{value:"Succeeded",children:"已完成"}),o.jsx("option",{value:"Failed",children:"失败"}),o.jsx("option",{value:"ResultUnconfirmed",children:"待核对"})]})]})]}),l.length===0?o.jsx(It,{text:"当前 Client 暂无任务记录。"}):o.jsx("div",{className:"task-list",children:l.map(i=>{var s,u;return o.jsxs("article",{className:"task-row",children:[o.jsxs("div",{className:"task-main",children:[o.jsxs("div",{className:"task-title",children:[o.jsx("strong",{children:i.kind}),o.jsx(fn,{value:i.status})]}),o.jsxs("p",{children:[i.account_id," · ",Cl(i.task_id,22)]}),o.jsx("small",{children:i.status==="WaitingForClient"?"Client 离线后等待显式恢复;不会自动重放写操作。":((s=i.result)==null?void 0:s.message)||((u=i.result)==null?void 0:u.error_code)||`更新于 ${dn(i.updated_at)}`})]}),o.jsxs("div",{className:"task-meta",children:[o.jsxs("span",{children:["第 ",i.lease_generation||0," 代租约"]}),o.jsx("time",{children:dn(i.updated_at)})]})]},i.task_id)})})]})}function ap({nodes:e}){return o.jsx("div",{className:"diagnostic-list",children:e.length===0?o.jsx(It,{text:"暂无注册 Client。"}):e.map(t=>o.jsxs("article",{className:"diagnostic-row",children:[o.jsxs("div",{className:"diagnostic-title",children:[o.jsx("span",{className:"node-avatar",children:t.node_id.slice(0,1).toUpperCase()}),o.jsxs("div",{children:[o.jsx("strong",{children:t.node_id}),o.jsxs("small",{children:[t.active_account_id||"尚未确认活动账号"," · 最近心跳 ",dn(t.last_heartbeat_at)]})]})]}),o.jsx(fn,{value:t.status})]},t.node_id))})}function cp({events:e,selectedClientId:t}){const n=e.filter(r=>!t||r.node_id===t);return o.jsx("div",{className:"diagnostic-list",children:n.length===0?o.jsx(It,{text:"当前 Client 暂无事件。"}):n.slice(0,30).map(r=>o.jsxs("article",{className:"diagnostic-row",children:[o.jsxs("div",{children:[o.jsx("strong",{children:Cl(r.chat_id,24)}),o.jsxs("small",{children:[r.node_id," · ",r.chat_type==="Group"?"群聊":"私聊"," · ",dn(r.received_at)]})]}),o.jsx("span",{className:"event-preview",children:r.content||"无正文"})]},r.event_id))})}function dp({audit:e}){return o.jsx("div",{className:"diagnostic-list",children:e.length===0?o.jsx(It,{text:"暂无审计记录。"}):e.slice(0,30).map(t=>o.jsxs("article",{className:"diagnostic-row",children:[o.jsxs("div",{children:[o.jsx("strong",{children:t.action}),o.jsxs("small",{children:[t.principal," · ",t.resource]})]}),o.jsx("span",{children:dn(t.at)})]},t.id))})}function fp({nodes:e,events:t,audit:n,selectedClientId:r,onSelectClient:l}){const[i,s]=T.useState("connection");return o.jsxs("section",{className:"settings-layout",children:[o.jsxs("aside",{className:"settings-nav",children:[o.jsxs("button",{className:i==="connection"?"settings-tab active":"settings-tab",onClick:()=>s("connection"),children:[o.jsx("strong",{children:"连接与账号"}),o.jsx("span",{children:"选择 Client、确认状态"})]}),o.jsxs("button",{className:i==="diagnostics"?"settings-tab active":"settings-tab",onClick:()=>s("diagnostics"),children:[o.jsx("strong",{children:"高级诊断"}),o.jsx("span",{children:"节点、事件、AI、审计"})]})]}),o.jsx("div",{className:"settings-content",children:i==="connection"?o.jsxs(o.Fragment,{children:[o.jsxs("div",{className:"workspace-panel",children:[o.jsxs("div",{className:"section-heading",children:[o.jsxs("div",{children:[o.jsx("h2",{children:"连接与账号"}),o.jsx("p",{children:"每个 Client 独立连接,单个 Client 离线不会阻断其它 Client。"})]}),o.jsx(fn,{value:e.some(ol)?"Online":"Offline"})]}),o.jsx(ap,{nodes:e})]}),o.jsxs("div",{className:"workspace-panel",children:[o.jsx("div",{className:"section-heading",children:o.jsxs("div",{children:[o.jsx("h2",{children:"安全边界"}),o.jsx("p",{children:"当前版本只开放真实后端已支持且可验证的只读能力。"})]})}),o.jsxs("ul",{className:"boundary-list",children:[o.jsxs("li",{children:[o.jsx(K,{name:"check",size:15}),"消息、通讯录、任务结果按选定 Client 隔离读取"]}),o.jsxs("li",{children:[o.jsx(K,{name:"check",size:15}),"Client 掉线只影响当前上下文,任务不会自动改投"]}),o.jsxs("li",{children:[o.jsx(K,{name:"alert",size:15}),"发送、群发、添加好友等待 Windows 真机验收"]})]})]})]}):o.jsxs(o.Fragment,{children:[o.jsxs("div",{className:"workspace-panel",children:[o.jsx("div",{className:"section-heading",children:o.jsxs("div",{children:[o.jsx("h2",{children:"Client 诊断"}),o.jsx("p",{children:"技术状态只在这里展示,不干扰普通操作。"})]})}),o.jsx("div",{className:"client-pills",children:e.map(u=>o.jsxs("button",{className:u.node_id===r?"client-pill active":"client-pill",onClick:()=>l(u.node_id),children:[o.jsx("span",{children:u.node_id}),o.jsx(fn,{value:u.status})]},u.node_id))})]}),o.jsx("div",{className:"workspace-panel",children:o.jsxs("div",{className:"section-heading",children:[o.jsxs("div",{children:[o.jsx("h2",{children:"AI 能力"}),o.jsx("p",{children:"AI 入口保留在高级诊断中;当前不自动调用,也不影响消息、通讯录和任务操作。"})]}),o.jsxs("span",{className:"read-only-note",children:[o.jsx(K,{name:"shield",size:14}),"按能力矩阵开放"]})]})}),o.jsxs("div",{className:"workspace-panel",children:[o.jsx("div",{className:"section-heading",children:o.jsxs("div",{children:[o.jsx("h2",{children:"白名单事件"}),o.jsx("p",{children:"当前选中 Client 的已授权事件。"})]})}),o.jsx(cp,{events:t,selectedClientId:r})]}),o.jsxs("div",{className:"workspace-panel",children:[o.jsx("div",{className:"section-heading",children:o.jsxs("div",{children:[o.jsx("h2",{children:"操作审计"}),o.jsx("p",{children:"用于定位连接、任务和权限问题。"})]})}),o.jsx(dp,{audit:n})]})]})})]})}function pp(){const[e,t]=T.useState(()=>sessionStorage.getItem("wxagent-token")||""),[n,r]=T.useState(()=>sessionStorage.getItem("wxagent-user")||""),[l,i]=T.useState("messages"),[s,u]=T.useState([]),[a,f]=T.useState(()=>sessionStorage.getItem("wxagent-client")||""),[g,h]=T.useState([]),[m,x]=T.useState([]),[w,S]=T.useState([]),[z,d]=T.useState(!1),[c,p]=T.useState(""),v=T.useCallback(()=>{sessionStorage.removeItem("wxagent-token"),sessionStorage.removeItem("wxagent-user"),sessionStorage.removeItem("wxagent-client"),t(""),r("")},[]),k=T.useCallback(async()=>{if(e){d(!0),p("");try{const[D,De,ir,Nl]=await Promise.all([Et("/v1/nodes",{token:e}),Et("/v1/tasks?limit=200",{token:e}),Et("/v1/events?limit=200",{token:e}),Et("/v1/audit?limit=200",{token:e})]);u(D.nodes||[]),h(De.tasks||[]),x(ir.events||[]),S(Nl.audit||[])}catch(D){D.status===401?v():p(D.message)}finally{d(!1)}}},[v,e]);T.useEffect(()=>{if(k(),!e)return;const D=window.setInterval(k,15e3);return()=>window.clearInterval(D)},[k,e]),T.useEffect(()=>{if(!s.length){f("");return}if(!s.some(D=>D.node_id===a)){const D=s.find(ol)||s[0];f(D.node_id),sessionStorage.setItem("wxagent-client",D.node_id)}},[s,a]);const N=D=>{f(D),sessionStorage.setItem("wxagent-client",D)},E=(D,De)=>{sessionStorage.setItem("wxagent-token",D),sessionStorage.setItem("wxagent-user",De),t(D),r(De)},_=T.useMemo(()=>s.find(D=>D.node_id===a),[s,a]),F=vc.find(D=>D.id===l),R=(F==null?void 0:F.label)||"设置与诊断",ge=(F==null?void 0:F.description)||"连接、账号和高级诊断信息";return e?o.jsxs("div",{className:"app-shell",children:[o.jsx(np,{view:l,setView:i,onLogout:v,username:n}),o.jsxs("main",{className:"main-area",children:[o.jsx(rp,{title:R,subtitle:ge,nodes:s,selectedClientId:a,onClientChange:N,onRefresh:k,loading:z}),c&&o.jsxs("div",{className:"global-error",role:"alert",children:[o.jsx(K,{name:"alert",size:16}),o.jsx("span",{children:c}),o.jsx("button",{onClick:()=>p(""),"aria-label":"关闭错误",children:o.jsx(K,{name:"close",size:16})})]}),o.jsx(lp,{nodes:s,selectedNode:_,onSettings:()=>i("settings")}),o.jsxs("div",{className:"page-content",children:[l==="messages"&&o.jsx(ip,{token:e,client:_,onSettings:()=>i("settings")}),l==="broadcast"&&o.jsx(sp,{client:_,onSettings:()=>i("settings")}),l==="contacts"&&o.jsx(op,{token:e,client:_,onSettings:()=>i("settings")}),l==="tasks"&&o.jsx(up,{tasks:g,selectedClientId:a}),l==="settings"&&o.jsx(fp,{nodes:s,events:m,audit:w,selectedClientId:a,onSelectClient:N})]}),o.jsxs("footer",{className:"footer",children:[o.jsx("span",{children:"WxAgent 工作台 · Client 独立隔离"}),o.jsx("span",{children:"写操作需通过真实能力与真机验收"})]})]})]}):o.jsx(tp,{onLogin:E})}mc(document.getElementById("root")).render(o.jsx(pp,{})); diff --git a/control-plane/web/dist/assets/index-Ihi-UMyS.js b/control-plane/web/dist/assets/index-Ihi-UMyS.js new file mode 100644 index 0000000..2932b9a --- /dev/null +++ b/control-plane/web/dist/assets/index-Ihi-UMyS.js @@ -0,0 +1,40 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const i of l)if(i.type==="childList")for(const s of i.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&r(s)}).observe(document,{childList:!0,subtree:!0});function n(l){const i={};return l.integrity&&(i.integrity=l.integrity),l.referrerPolicy&&(i.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?i.credentials="include":l.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(l){if(l.ep)return;l.ep=!0;const i=n(l);fetch(l.href,i)}})();var uu={exports:{}},al={},au={exports:{}},M={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var er=Symbol.for("react.element"),_c=Symbol.for("react.portal"),Ec=Symbol.for("react.fragment"),zc=Symbol.for("react.strict_mode"),Pc=Symbol.for("react.profiler"),Tc=Symbol.for("react.provider"),Lc=Symbol.for("react.context"),Rc=Symbol.for("react.forward_ref"),Mc=Symbol.for("react.suspense"),Ic=Symbol.for("react.memo"),Oc=Symbol.for("react.lazy"),Ys=Symbol.iterator;function Fc(e){return e===null||typeof e!="object"?null:(e=Ys&&e[Ys]||e["@@iterator"],typeof e=="function"?e:null)}var cu={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},du=Object.assign,fu={};function pn(e,t,n){this.props=e,this.context=t,this.refs=fu,this.updater=n||cu}pn.prototype.isReactComponent={};pn.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};pn.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function pu(){}pu.prototype=pn.prototype;function qi(e,t,n){this.props=e,this.context=t,this.refs=fu,this.updater=n||cu}var bi=qi.prototype=new pu;bi.constructor=qi;du(bi,pn.prototype);bi.isPureReactComponent=!0;var Gs=Array.isArray,hu=Object.prototype.hasOwnProperty,es={current:null},mu={key:!0,ref:!0,__self:!0,__source:!0};function vu(e,t,n){var r,l={},i=null,s=null;if(t!=null)for(r in t.ref!==void 0&&(s=t.ref),t.key!==void 0&&(i=""+t.key),t)hu.call(t,r)&&!mu.hasOwnProperty(r)&&(l[r]=t[r]);var u=arguments.length-2;if(u===1)l.children=n;else if(1>>1,b=_[G];if(0>>1;Gl(Pl,R))Stl(or,Pl)?(_[G]=or,_[St]=R,G=St):(_[G]=Pl,_[wt]=R,G=wt);else if(Stl(or,R))_[G]=or,_[St]=R,G=St;else break e}}return L}function l(_,L){var R=_.sortIndex-L.sortIndex;return R!==0?R:_.id-L.id}if(typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var s=Date,u=s.now();e.unstable_now=function(){return s.now()-u}}var a=[],f=[],v=1,h=null,m=3,x=!1,k=!1,S=!1,P=typeof setTimeout=="function"?setTimeout:null,d=typeof clearTimeout=="function"?clearTimeout:null,c=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function p(_){for(var L=n(f);L!==null;){if(L.callback===null)r(f);else if(L.startTime<=_)r(f),L.sortIndex=L.expirationTime,t(a,L);else break;L=n(f)}}function g(_){if(S=!1,p(_),!k)if(n(a)!==null)k=!0,El(j);else{var L=n(f);L!==null&&zl(g,L.startTime-_)}}function j(_,L){k=!1,S&&(S=!1,d(w),w=-1),x=!0;var R=m;try{for(p(L),h=n(a);h!==null&&(!(h.expirationTime>L)||_&&!Y());){var G=h.callback;if(typeof G=="function"){h.callback=null,m=h.priorityLevel;var b=G(h.expirationTime<=L);L=e.unstable_now(),typeof b=="function"?h.callback=b:h===n(a)&&r(a),p(L)}else r(a);h=n(a)}if(h!==null)var sr=!0;else{var wt=n(f);wt!==null&&zl(g,wt.startTime-L),sr=!1}return sr}finally{h=null,m=R,x=!1}}var C=!1,E=null,w=-1,I=5,z=-1;function Y(){return!(e.unstable_now()-z_||125<_?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):I=0<_?Math.floor(1e3/_):5},e.unstable_getCurrentPriorityLevel=function(){return m},e.unstable_getFirstCallbackNode=function(){return n(a)},e.unstable_next=function(_){switch(m){case 1:case 2:case 3:var L=3;break;default:L=m}var R=m;m=L;try{return _()}finally{m=R}},e.unstable_pauseExecution=function(){},e.unstable_requestPaint=function(){},e.unstable_runWithPriority=function(_,L){switch(_){case 1:case 2:case 3:case 4:case 5:break;default:_=3}var R=m;m=_;try{return L()}finally{m=R}},e.unstable_scheduleCallback=function(_,L,R){var G=e.unstable_now();switch(typeof R=="object"&&R!==null?(R=R.delay,R=typeof R=="number"&&0G?(_.sortIndex=R,t(f,_),n(a)===null&&_===n(f)&&(S?(d(w),w=-1):S=!0,zl(g,R-G))):(_.sortIndex=b,t(a,_),k||x||(k=!0,El(j))),_},e.unstable_shouldYield=Y,e.unstable_wrapCallback=function(_){var L=m;return function(){var R=m;m=L;try{return _.apply(this,arguments)}finally{m=R}}}})(Su);wu.exports=Su;var Yc=wu.exports;/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Gc=T,ke=Yc;function y(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),ri=Object.prototype.hasOwnProperty,Xc=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,Zs={},Js={};function Zc(e){return ri.call(Js,e)?!0:ri.call(Zs,e)?!1:Xc.test(e)?Js[e]=!0:(Zs[e]=!0,!1)}function Jc(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function qc(e,t,n,r){if(t===null||typeof t>"u"||Jc(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function fe(e,t,n,r,l,i,s){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=s}var le={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){le[e]=new fe(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];le[t]=new fe(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){le[e]=new fe(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){le[e]=new fe(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){le[e]=new fe(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){le[e]=new fe(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){le[e]=new fe(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){le[e]=new fe(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){le[e]=new fe(e,5,!1,e.toLowerCase(),null,!1,!1)});var ns=/[\-:]([a-z])/g;function rs(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(ns,rs);le[t]=new fe(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(ns,rs);le[t]=new fe(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(ns,rs);le[t]=new fe(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){le[e]=new fe(e,1,!1,e.toLowerCase(),null,!1,!1)});le.xlinkHref=new fe("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){le[e]=new fe(e,1,!1,e.toLowerCase(),null,!0,!0)});function ls(e,t,n,r){var l=le.hasOwnProperty(t)?le[t]:null;(l!==null?l.type!==0:r||!(2u||l[s]!==i[u]){var a=` +`+l[s].replace(" at new "," at ");return e.displayName&&a.includes("")&&(a=a.replace("",e.displayName)),a}while(1<=s&&0<=u);break}}}finally{Rl=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Cn(e):""}function bc(e){switch(e.tag){case 5:return Cn(e.type);case 16:return Cn("Lazy");case 13:return Cn("Suspense");case 19:return Cn("SuspenseList");case 0:case 2:case 15:return e=Ml(e.type,!1),e;case 11:return e=Ml(e.type.render,!1),e;case 1:return e=Ml(e.type,!0),e;default:return""}}function oi(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Ut:return"Fragment";case At:return"Portal";case li:return"Profiler";case is:return"StrictMode";case ii:return"Suspense";case si:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Cu:return(e.displayName||"Context")+".Consumer";case ju:return(e._context.displayName||"Context")+".Provider";case ss:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case os:return t=e.displayName||null,t!==null?t:oi(e.type)||"Memo";case tt:t=e._payload,e=e._init;try{return oi(e(t))}catch{}}return null}function ed(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return oi(t);case 8:return t===is?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function mt(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function _u(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function td(e){var t=_u(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,i=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(s){r=""+s,i.call(this,s)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(s){r=""+s},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function cr(e){e._valueTracker||(e._valueTracker=td(e))}function Eu(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=_u(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function $r(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function ui(e,t){var n=t.checked;return Q({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function bs(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=mt(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function zu(e,t){t=t.checked,t!=null&&ls(e,"checked",t,!1)}function ai(e,t){zu(e,t);var n=mt(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?ci(e,t.type,n):t.hasOwnProperty("defaultValue")&&ci(e,t.type,mt(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function eo(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function ci(e,t,n){(t!=="number"||$r(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Nn=Array.isArray;function qt(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=dr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function $n(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var zn={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},nd=["Webkit","ms","Moz","O"];Object.keys(zn).forEach(function(e){nd.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),zn[t]=zn[e]})});function Ru(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||zn.hasOwnProperty(e)&&zn[e]?(""+t).trim():t+"px"}function Mu(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Ru(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var rd=Q({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function pi(e,t){if(t){if(rd[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(y(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(y(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(y(61))}if(t.style!=null&&typeof t.style!="object")throw Error(y(62))}}function hi(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var mi=null;function us(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var vi=null,bt=null,en=null;function ro(e){if(e=rr(e)){if(typeof vi!="function")throw Error(y(280));var t=e.stateNode;t&&(t=hl(t),vi(e.stateNode,e.type,t))}}function Iu(e){bt?en?en.push(e):en=[e]:bt=e}function Ou(){if(bt){var e=bt,t=en;if(en=bt=null,ro(e),t)for(e=0;e>>=0,e===0?32:31-(hd(e)/md|0)|0}var fr=64,pr=4194304;function _n(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Wr(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,i=e.pingedLanes,s=n&268435455;if(s!==0){var u=s&~l;u!==0?r=_n(u):(i&=s,i!==0&&(r=_n(i)))}else s=n&~l,s!==0?r=_n(s):i!==0&&(r=_n(i));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,i=t&-t,l>=i||l===16&&(i&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function tr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Oe(t),e[t]=n}function xd(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Tn),po=" ",ho=!1;function ta(e,t){switch(e){case"keyup":return Yd.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function na(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Vt=!1;function Xd(e,t){switch(e){case"compositionend":return na(t);case"keypress":return t.which!==32?null:(ho=!0,po);case"textInput":return e=t.data,e===po&&ho?null:e;default:return null}}function Zd(e,t){if(Vt)return e==="compositionend"||!vs&&ta(e,t)?(e=bu(),Pr=ps=it=null,Vt=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=yo(n)}}function sa(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?sa(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function oa(){for(var e=window,t=$r();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=$r(e.document)}return t}function gs(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function sf(e){var t=oa(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&sa(n.ownerDocument.documentElement,n)){if(r!==null&&gs(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,i=Math.min(r.start,l);r=r.end===void 0?i:Math.min(r.end,l),!e.extend&&i>r&&(l=r,r=i,i=l),l=xo(n,i);var s=xo(n,r);l&&s&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==s.node||e.focusOffset!==s.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),i>r?(e.addRange(t),e.extend(s.node,s.offset)):(t.setEnd(s.node,s.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,Wt=null,ki=null,Rn=null,ji=!1;function wo(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;ji||Wt==null||Wt!==$r(r)||(r=Wt,"selectionStart"in r&&gs(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Rn&&Hn(Rn,r)||(Rn=r,r=Qr(ki,"onSelect"),0Qt||(e.current=Pi[Qt],Pi[Qt]=null,Qt--)}function $(e,t){Qt++,Pi[Qt]=e.current,e.current=t}var vt={},ue=yt(vt),ve=yt(!1),Pt=vt;function sn(e,t){var n=e.type.contextTypes;if(!n)return vt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},i;for(i in n)l[i]=t[i];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function ge(e){return e=e.childContextTypes,e!=null}function Yr(){U(ve),U(ue)}function Eo(e,t,n){if(ue.current!==vt)throw Error(y(168));$(ue,t),$(ve,n)}function va(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(y(108,ed(e)||"Unknown",l));return Q({},n,r)}function Gr(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||vt,Pt=ue.current,$(ue,e),$(ve,ve.current),!0}function zo(e,t,n){var r=e.stateNode;if(!r)throw Error(y(169));n?(e=va(e,t,Pt),r.__reactInternalMemoizedMergedChildContext=e,U(ve),U(ue),$(ue,e)):U(ve),$(ve,n)}var He=null,ml=!1,Yl=!1;function ga(e){He===null?He=[e]:He.push(e)}function yf(e){ml=!0,ga(e)}function xt(){if(!Yl&&He!==null){Yl=!0;var e=0,t=D;try{var n=He;for(D=1;e>=s,l-=s,Qe=1<<32-Oe(t)+l|n<w?(I=E,E=null):I=E.sibling;var z=m(d,E,p[w],g);if(z===null){E===null&&(E=I);break}e&&E&&z.alternate===null&&t(d,E),c=i(z,c,w),C===null?j=z:C.sibling=z,C=z,E=I}if(w===p.length)return n(d,E),V&&kt(d,w),j;if(E===null){for(;ww?(I=E,E=null):I=E.sibling;var Y=m(d,E,z.value,g);if(Y===null){E===null&&(E=I);break}e&&E&&Y.alternate===null&&t(d,E),c=i(Y,c,w),C===null?j=Y:C.sibling=Y,C=Y,E=I}if(z.done)return n(d,E),V&&kt(d,w),j;if(E===null){for(;!z.done;w++,z=p.next())z=h(d,z.value,g),z!==null&&(c=i(z,c,w),C===null?j=z:C.sibling=z,C=z);return V&&kt(d,w),j}for(E=r(d,E);!z.done;w++,z=p.next())z=x(E,d,w,z.value,g),z!==null&&(e&&z.alternate!==null&&E.delete(z.key===null?w:z.key),c=i(z,c,w),C===null?j=z:C.sibling=z,C=z);return e&&E.forEach(function(O){return t(d,O)}),V&&kt(d,w),j}function P(d,c,p,g){if(typeof p=="object"&&p!==null&&p.type===Ut&&p.key===null&&(p=p.props.children),typeof p=="object"&&p!==null){switch(p.$$typeof){case ar:e:{for(var j=p.key,C=c;C!==null;){if(C.key===j){if(j=p.type,j===Ut){if(C.tag===7){n(d,C.sibling),c=l(C,p.props.children),c.return=d,d=c;break e}}else if(C.elementType===j||typeof j=="object"&&j!==null&&j.$$typeof===tt&&Lo(j)===C.type){n(d,C.sibling),c=l(C,p.props),c.ref=Sn(d,C,p),c.return=d,d=c;break e}n(d,C);break}else t(d,C);C=C.sibling}p.type===Ut?(c=zt(p.props.children,d.mode,g,p.key),c.return=d,d=c):(g=Dr(p.type,p.key,p.props,null,d.mode,g),g.ref=Sn(d,c,p),g.return=d,d=g)}return s(d);case At:e:{for(C=p.key;c!==null;){if(c.key===C)if(c.tag===4&&c.stateNode.containerInfo===p.containerInfo&&c.stateNode.implementation===p.implementation){n(d,c.sibling),c=l(c,p.children||[]),c.return=d,d=c;break e}else{n(d,c);break}else t(d,c);c=c.sibling}c=ti(p,d.mode,g),c.return=d,d=c}return s(d);case tt:return C=p._init,P(d,c,C(p._payload),g)}if(Nn(p))return k(d,c,p,g);if(vn(p))return S(d,c,p,g);wr(d,p)}return typeof p=="string"&&p!==""||typeof p=="number"?(p=""+p,c!==null&&c.tag===6?(n(d,c.sibling),c=l(c,p),c.return=d,d=c):(n(d,c),c=ei(p,d.mode,g),c.return=d,d=c),s(d)):n(d,c)}return P}var un=Sa(!0),ka=Sa(!1),Jr=yt(null),qr=null,Gt=null,Ss=null;function ks(){Ss=Gt=qr=null}function js(e){var t=Jr.current;U(Jr),e._currentValue=t}function Ri(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function nn(e,t){qr=e,Ss=Gt=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(me=!0),e.firstContext=null)}function Pe(e){var t=e._currentValue;if(Ss!==e)if(e={context:e,memoizedValue:t,next:null},Gt===null){if(qr===null)throw Error(y(308));Gt=e,qr.dependencies={lanes:0,firstContext:e}}else Gt=Gt.next=e;return t}var Nt=null;function Cs(e){Nt===null?Nt=[e]:Nt.push(e)}function ja(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,Cs(t)):(n.next=l.next,l.next=n),t.interleaved=n,Je(e,r)}function Je(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var nt=!1;function Ns(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Ca(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Ge(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function dt(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,F&2){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,Je(e,n)}return l=r.interleaved,l===null?(t.next=t,Cs(r)):(t.next=l.next,l.next=t),r.interleaved=t,Je(e,n)}function Lr(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,cs(e,n)}}function Ro(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,i=null;if(n=n.firstBaseUpdate,n!==null){do{var s={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};i===null?l=i=s:i=i.next=s,n=n.next}while(n!==null);i===null?l=i=t:i=i.next=t}else l=i=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:i,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function br(e,t,n,r){var l=e.updateQueue;nt=!1;var i=l.firstBaseUpdate,s=l.lastBaseUpdate,u=l.shared.pending;if(u!==null){l.shared.pending=null;var a=u,f=a.next;a.next=null,s===null?i=f:s.next=f,s=a;var v=e.alternate;v!==null&&(v=v.updateQueue,u=v.lastBaseUpdate,u!==s&&(u===null?v.firstBaseUpdate=f:u.next=f,v.lastBaseUpdate=a))}if(i!==null){var h=l.baseState;s=0,v=f=a=null,u=i;do{var m=u.lane,x=u.eventTime;if((r&m)===m){v!==null&&(v=v.next={eventTime:x,lane:0,tag:u.tag,payload:u.payload,callback:u.callback,next:null});e:{var k=e,S=u;switch(m=t,x=n,S.tag){case 1:if(k=S.payload,typeof k=="function"){h=k.call(x,h,m);break e}h=k;break e;case 3:k.flags=k.flags&-65537|128;case 0:if(k=S.payload,m=typeof k=="function"?k.call(x,h,m):k,m==null)break e;h=Q({},h,m);break e;case 2:nt=!0}}u.callback!==null&&u.lane!==0&&(e.flags|=64,m=l.effects,m===null?l.effects=[u]:m.push(u))}else x={eventTime:x,lane:m,tag:u.tag,payload:u.payload,callback:u.callback,next:null},v===null?(f=v=x,a=h):v=v.next=x,s|=m;if(u=u.next,u===null){if(u=l.shared.pending,u===null)break;m=u,u=m.next,m.next=null,l.lastBaseUpdate=m,l.shared.pending=null}}while(!0);if(v===null&&(a=h),l.baseState=a,l.firstBaseUpdate=f,l.lastBaseUpdate=v,t=l.shared.interleaved,t!==null){l=t;do s|=l.lane,l=l.next;while(l!==t)}else i===null&&(l.shared.lanes=0);Rt|=s,e.lanes=s,e.memoizedState=h}}function Mo(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=Xl.transition;Xl.transition={};try{e(!1),t()}finally{D=n,Xl.transition=r}}function Va(){return Te().memoizedState}function kf(e,t,n){var r=pt(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},Wa(e))Ba(t,n);else if(n=ja(e,t,n,r),n!==null){var l=ce();Fe(n,e,r,l),Ha(n,t,r)}}function jf(e,t,n){var r=pt(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(Wa(e))Ba(t,l);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var s=t.lastRenderedState,u=i(s,n);if(l.hasEagerState=!0,l.eagerState=u,De(u,s)){var a=t.interleaved;a===null?(l.next=l,Cs(t)):(l.next=a.next,a.next=l),t.interleaved=l;return}}catch{}finally{}n=ja(e,t,l,r),n!==null&&(l=ce(),Fe(n,e,r,l),Ha(n,t,r))}}function Wa(e){var t=e.alternate;return e===H||t!==null&&t===H}function Ba(e,t){Mn=tl=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Ha(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,cs(e,n)}}var nl={readContext:Pe,useCallback:ie,useContext:ie,useEffect:ie,useImperativeHandle:ie,useInsertionEffect:ie,useLayoutEffect:ie,useMemo:ie,useReducer:ie,useRef:ie,useState:ie,useDebugValue:ie,useDeferredValue:ie,useTransition:ie,useMutableSource:ie,useSyncExternalStore:ie,useId:ie,unstable_isNewReconciler:!1},Cf={readContext:Pe,useCallback:function(e,t){return Ae().memoizedState=[e,t===void 0?null:t],e},useContext:Pe,useEffect:Oo,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Mr(4194308,4,Fa.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Mr(4194308,4,e,t)},useInsertionEffect:function(e,t){return Mr(4,2,e,t)},useMemo:function(e,t){var n=Ae();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ae();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=kf.bind(null,H,e),[r.memoizedState,e]},useRef:function(e){var t=Ae();return e={current:e},t.memoizedState=e},useState:Io,useDebugValue:Ms,useDeferredValue:function(e){return Ae().memoizedState=e},useTransition:function(){var e=Io(!1),t=e[0];return e=Sf.bind(null,e[1]),Ae().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=H,l=Ae();if(V){if(n===void 0)throw Error(y(407));n=n()}else{if(n=t(),te===null)throw Error(y(349));Lt&30||za(r,t,n)}l.memoizedState=n;var i={value:n,getSnapshot:t};return l.queue=i,Oo(Ta.bind(null,r,i,e),[e]),r.flags|=2048,qn(9,Pa.bind(null,r,i,n,t),void 0,null),n},useId:function(){var e=Ae(),t=te.identifierPrefix;if(V){var n=Ke,r=Qe;n=(r&~(1<<32-Oe(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Zn++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=s.createElement(n,{is:r.is}):(e=s.createElement(n),n==="select"&&(s=e,r.multiple?s.multiple=!0:r.size&&(s.size=r.size))):e=s.createElementNS(e,n),e[Ue]=t,e[Yn]=r,ec(e,t,!1,!1),t.stateNode=e;e:{switch(s=hi(n,r),n){case"dialog":A("cancel",e),A("close",e),l=r;break;case"iframe":case"object":case"embed":A("load",e),l=r;break;case"video":case"audio":for(l=0;ldn&&(t.flags|=128,r=!0,kn(i,!1),t.lanes=4194304)}else{if(!r)if(e=el(s),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),kn(i,!0),i.tail===null&&i.tailMode==="hidden"&&!s.alternate&&!V)return se(t),null}else 2*X()-i.renderingStartTime>dn&&n!==1073741824&&(t.flags|=128,r=!0,kn(i,!1),t.lanes=4194304);i.isBackwards?(s.sibling=t.child,t.child=s):(n=i.last,n!==null?n.sibling=s:t.child=s,i.last=s)}return i.tail!==null?(t=i.tail,i.rendering=t,i.tail=t.sibling,i.renderingStartTime=X(),t.sibling=null,n=W.current,$(W,r?n&1|2:n&1),t):(se(t),null);case 22:case 23:return As(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?xe&1073741824&&(se(t),t.subtreeFlags&6&&(t.flags|=8192)):se(t),null;case 24:return null;case 25:return null}throw Error(y(156,t.tag))}function Rf(e,t){switch(xs(t),t.tag){case 1:return ge(t.type)&&Yr(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return an(),U(ve),U(ue),zs(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return Es(t),null;case 13:if(U(W),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(y(340));on()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return U(W),null;case 4:return an(),null;case 10:return js(t.type._context),null;case 22:case 23:return As(),null;case 24:return null;default:return null}}var kr=!1,oe=!1,Mf=typeof WeakSet=="function"?WeakSet:Set,N=null;function Xt(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){K(e,t,r)}else n.current=null}function Vi(e,t,n){try{n()}catch(r){K(e,t,r)}}var Ko=!1;function If(e,t){if(Ci=Br,e=oa(),gs(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,i=r.focusNode;r=r.focusOffset;try{n.nodeType,i.nodeType}catch{n=null;break e}var s=0,u=-1,a=-1,f=0,v=0,h=e,m=null;t:for(;;){for(var x;h!==n||l!==0&&h.nodeType!==3||(u=s+l),h!==i||r!==0&&h.nodeType!==3||(a=s+r),h.nodeType===3&&(s+=h.nodeValue.length),(x=h.firstChild)!==null;)m=h,h=x;for(;;){if(h===e)break t;if(m===n&&++f===l&&(u=s),m===i&&++v===r&&(a=s),(x=h.nextSibling)!==null)break;h=m,m=h.parentNode}h=x}n=u===-1||a===-1?null:{start:u,end:a}}else n=null}n=n||{start:0,end:0}}else n=null;for(Ni={focusedElem:e,selectionRange:n},Br=!1,N=t;N!==null;)if(t=N,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,N=e;else for(;N!==null;){t=N;try{var k=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(k!==null){var S=k.memoizedProps,P=k.memoizedState,d=t.stateNode,c=d.getSnapshotBeforeUpdate(t.elementType===t.type?S:Re(t.type,S),P);d.__reactInternalSnapshotBeforeUpdate=c}break;case 3:var p=t.stateNode.containerInfo;p.nodeType===1?p.textContent="":p.nodeType===9&&p.documentElement&&p.removeChild(p.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(y(163))}}catch(g){K(t,t.return,g)}if(e=t.sibling,e!==null){e.return=t.return,N=e;break}N=t.return}return k=Ko,Ko=!1,k}function In(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var i=l.destroy;l.destroy=void 0,i!==void 0&&Vi(t,n,i)}l=l.next}while(l!==r)}}function yl(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Wi(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function rc(e){var t=e.alternate;t!==null&&(e.alternate=null,rc(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Ue],delete t[Yn],delete t[zi],delete t[vf],delete t[gf])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function lc(e){return e.tag===5||e.tag===3||e.tag===4}function Yo(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||lc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Bi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Kr));else if(r!==4&&(e=e.child,e!==null))for(Bi(e,t,n),e=e.sibling;e!==null;)Bi(e,t,n),e=e.sibling}function Hi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Hi(e,t,n),e=e.sibling;e!==null;)Hi(e,t,n),e=e.sibling}var ne=null,Me=!1;function et(e,t,n){for(n=n.child;n!==null;)ic(e,t,n),n=n.sibling}function ic(e,t,n){if(Ve&&typeof Ve.onCommitFiberUnmount=="function")try{Ve.onCommitFiberUnmount(cl,n)}catch{}switch(n.tag){case 5:oe||Xt(n,t);case 6:var r=ne,l=Me;ne=null,et(e,t,n),ne=r,Me=l,ne!==null&&(Me?(e=ne,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):ne.removeChild(n.stateNode));break;case 18:ne!==null&&(Me?(e=ne,n=n.stateNode,e.nodeType===8?Kl(e.parentNode,n):e.nodeType===1&&Kl(e,n),Wn(e)):Kl(ne,n.stateNode));break;case 4:r=ne,l=Me,ne=n.stateNode.containerInfo,Me=!0,et(e,t,n),ne=r,Me=l;break;case 0:case 11:case 14:case 15:if(!oe&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var i=l,s=i.destroy;i=i.tag,s!==void 0&&(i&2||i&4)&&Vi(n,t,s),l=l.next}while(l!==r)}et(e,t,n);break;case 1:if(!oe&&(Xt(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(u){K(n,t,u)}et(e,t,n);break;case 21:et(e,t,n);break;case 22:n.mode&1?(oe=(r=oe)||n.memoizedState!==null,et(e,t,n),oe=r):et(e,t,n);break;default:et(e,t,n)}}function Go(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Mf),t.forEach(function(r){var l=Bf.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function Le(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=s),r&=~i}if(r=l,r=X()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Ff(r/1960))-r,10e?16:e,st===null)var r=!1;else{if(e=st,st=null,il=0,F&6)throw Error(y(331));var l=F;for(F|=4,N=e.current;N!==null;){var i=N,s=i.child;if(N.flags&16){var u=i.deletions;if(u!==null){for(var a=0;aX()-Ds?Et(e,0):Fs|=n),ye(e,t)}function pc(e,t){t===0&&(e.mode&1?(t=pr,pr<<=1,!(pr&130023424)&&(pr=4194304)):t=1);var n=ce();e=Je(e,t),e!==null&&(tr(e,t,n),ye(e,n))}function Wf(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),pc(e,n)}function Bf(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(y(314))}r!==null&&r.delete(t),pc(e,n)}var hc;hc=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||ve.current)me=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return me=!1,Tf(e,t,n);me=!!(e.flags&131072)}else me=!1,V&&t.flags&1048576&&ya(t,Zr,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Ir(e,t),e=t.pendingProps;var l=sn(t,ue.current);nn(t,n),l=Ts(null,t,r,e,l,n);var i=Ls();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,ge(r)?(i=!0,Gr(t)):i=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,Ns(t),l.updater=gl,t.stateNode=l,l._reactInternals=t,Ii(t,r,e,n),t=Di(null,t,r,!0,i,n)):(t.tag=0,V&&i&&ys(t),ae(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Ir(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=Qf(r),e=Re(r,e),l){case 0:t=Fi(null,t,r,e,n);break e;case 1:t=Bo(null,t,r,e,n);break e;case 11:t=Vo(null,t,r,e,n);break e;case 14:t=Wo(null,t,r,Re(r.type,e),n);break e}throw Error(y(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),Fi(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),Bo(e,t,r,l,n);case 3:e:{if(Ja(t),e===null)throw Error(y(387));r=t.pendingProps,i=t.memoizedState,l=i.element,Ca(e,t),br(t,r,null,n);var s=t.memoizedState;if(r=s.element,i.isDehydrated)if(i={element:r,isDehydrated:!1,cache:s.cache,pendingSuspenseBoundaries:s.pendingSuspenseBoundaries,transitions:s.transitions},t.updateQueue.baseState=i,t.memoizedState=i,t.flags&256){l=cn(Error(y(423)),t),t=Ho(e,t,r,n,l);break e}else if(r!==l){l=cn(Error(y(424)),t),t=Ho(e,t,r,n,l);break e}else for(we=ct(t.stateNode.containerInfo.firstChild),Se=t,V=!0,Ie=null,n=ka(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(on(),r===l){t=qe(e,t,n);break e}ae(e,t,r,n)}t=t.child}return t;case 5:return Na(t),e===null&&Li(t),r=t.type,l=t.pendingProps,i=e!==null?e.memoizedProps:null,s=l.children,_i(r,l)?s=null:i!==null&&_i(r,i)&&(t.flags|=32),Za(e,t),ae(e,t,s,n),t.child;case 6:return e===null&&Li(t),null;case 13:return qa(e,t,n);case 4:return _s(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=un(t,null,r,n):ae(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),Vo(e,t,r,l,n);case 7:return ae(e,t,t.pendingProps,n),t.child;case 8:return ae(e,t,t.pendingProps.children,n),t.child;case 12:return ae(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,i=t.memoizedProps,s=l.value,$(Jr,r._currentValue),r._currentValue=s,i!==null)if(De(i.value,s)){if(i.children===l.children&&!ve.current){t=qe(e,t,n);break e}}else for(i=t.child,i!==null&&(i.return=t);i!==null;){var u=i.dependencies;if(u!==null){s=i.child;for(var a=u.firstContext;a!==null;){if(a.context===r){if(i.tag===1){a=Ge(-1,n&-n),a.tag=2;var f=i.updateQueue;if(f!==null){f=f.shared;var v=f.pending;v===null?a.next=a:(a.next=v.next,v.next=a),f.pending=a}}i.lanes|=n,a=i.alternate,a!==null&&(a.lanes|=n),Ri(i.return,n,t),u.lanes|=n;break}a=a.next}}else if(i.tag===10)s=i.type===t.type?null:i.child;else if(i.tag===18){if(s=i.return,s===null)throw Error(y(341));s.lanes|=n,u=s.alternate,u!==null&&(u.lanes|=n),Ri(s,n,t),s=i.sibling}else s=i.child;if(s!==null)s.return=i;else for(s=i;s!==null;){if(s===t){s=null;break}if(i=s.sibling,i!==null){i.return=s.return,s=i;break}s=s.return}i=s}ae(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,nn(t,n),l=Pe(l),r=r(l),t.flags|=1,ae(e,t,r,n),t.child;case 14:return r=t.type,l=Re(r,t.pendingProps),l=Re(r.type,l),Wo(e,t,r,l,n);case 15:return Ga(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),Ir(e,t),t.tag=1,ge(r)?(e=!0,Gr(t)):e=!1,nn(t,n),Qa(t,r,l),Ii(t,r,l,n),Di(null,t,r,!0,e,n);case 19:return ba(e,t,n);case 22:return Xa(e,t,n)}throw Error(y(156,t.tag))};function mc(e,t){return Wu(e,t)}function Hf(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Ee(e,t,n,r){return new Hf(e,t,n,r)}function Vs(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Qf(e){if(typeof e=="function")return Vs(e)?1:0;if(e!=null){if(e=e.$$typeof,e===ss)return 11;if(e===os)return 14}return 2}function ht(e,t){var n=e.alternate;return n===null?(n=Ee(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Dr(e,t,n,r,l,i){var s=2;if(r=e,typeof e=="function")Vs(e)&&(s=1);else if(typeof e=="string")s=5;else e:switch(e){case Ut:return zt(n.children,l,i,t);case is:s=8,l|=8;break;case li:return e=Ee(12,n,t,l|2),e.elementType=li,e.lanes=i,e;case ii:return e=Ee(13,n,t,l),e.elementType=ii,e.lanes=i,e;case si:return e=Ee(19,n,t,l),e.elementType=si,e.lanes=i,e;case Nu:return wl(n,l,i,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case ju:s=10;break e;case Cu:s=9;break e;case ss:s=11;break e;case os:s=14;break e;case tt:s=16,r=null;break e}throw Error(y(130,e==null?e:typeof e,""))}return t=Ee(s,n,t,l),t.elementType=e,t.type=r,t.lanes=i,t}function zt(e,t,n,r){return e=Ee(7,e,r,t),e.lanes=n,e}function wl(e,t,n,r){return e=Ee(22,e,r,t),e.elementType=Nu,e.lanes=n,e.stateNode={isHidden:!1},e}function ei(e,t,n){return e=Ee(6,e,null,t),e.lanes=n,e}function ti(e,t,n){return t=Ee(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Kf(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Ol(0),this.expirationTimes=Ol(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Ol(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Ws(e,t,n,r,l,i,s,u,a){return e=new Kf(e,t,n,u,a),t===1?(t=1,i===!0&&(t|=8)):t=0,i=Ee(3,null,null,t),e.current=i,i.stateNode=e,i.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Ns(i),e}function Yf(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(xc)}catch(e){console.error(e)}}xc(),xu.exports=je;var qf=xu.exports,wc,nu=qf;wc=nu.createRoot,nu.hydrateRoot;function Sc(e,t){if(Array.isArray(e))return e;for(const n of t)if(Array.isArray(e==null?void 0:e[n]))return e[n];return[]}function bf(e){return Sc(e,["items","sessions","chats"]).map((t,n)=>({id:String(t.chat_id??t.chatId??t.session_id??t.sessionId??t.automation_id??t.automationId??t.id??n),title:String(t.title??t.name??t.display_name??t.displayName??t.nickname??t.chat_id??t.id??"未命名会话"),preview:String(t.last_message??t.lastMessage??t.preview??"暂无最近消息"),unread:Number(t.unread_count??t.unreadCount??0),type:t.chat_type??t.chatType??"Private"}))}function ep(e){return Sc(e,["items","messages"]).map((t,n)=>{const r=String(t.direction??"").toLowerCase();return{id:String(t.message_id??t.messageId??t.id??n),sender:String(t.sender??t.sender_name??t.senderName??(r==="outgoing"?"我":"对方")),text:typeof t=="string"?t:String(t.content??t.text??t.message??""),at:t.occurred_at??t.occurredAt??t.created_at??t.createdAt??t.source_time??t.sourceTime??t.observed_at??t.observedAt}})}function ru(e,t=0){var r;const n=(e==null?void 0:e.coverage)||((r=e==null?void 0:e.metadata)==null?void 0:r.coverage)||(e==null?void 0:e.sync);return!n||typeof n!="object"?{state:t===0?"unknown":"complete",source:"legacy-result",observedCount:t,authorizedScopeCount:null,matchedScopeCount:null,errorCode:t===0?"CoverageUnavailable":null}:{state:String(n.state||"unknown").toLowerCase(),source:n.source||"unknown",observedCount:Number(n.observedCount??n.observed_count??t),authorizedScopeCount:n.authorizedScopeCount??n.authorized_scope_count??null,matchedScopeCount:n.matchedScopeCount??n.matched_scope_count??null,observedAt:n.observedAt??n.observed_at??n.lastSuccessAt??n.last_success_at??null,lastSuccessAt:n.lastSuccessAt??n.last_success_at??null,backlogCount:n.backlogCount??n.backlog_count??null,errorCode:n.errorCode??n.error_code??null,errorMessage:n.errorMessage??n.error_message??null}}function lu(e,t,n=r=>r.id){const r=[...t],l=new Set(r.map(n));for(const i of e){const s=n(i);l.has(s)||(r.push(i),l.add(s))}return r}function iu(e){return(e==null?void 0:e.state)==="complete"}const kc=[{id:"messages",label:"消息",description:"查找会话、阅读并回复",icon:"message"},{id:"broadcast",label:"群发",description:"创建受控群发任务",icon:"send"},{id:"contacts",label:"通讯录",description:"查找联系人和群聊",icon:"contacts"},{id:"tasks",label:"任务",description:"查看执行结果",icon:"tasks"}],jc={Online:"在线",Degraded:"需要注意",Offline:"已离线",Registered:"已注册",SessionLocked:"桌面已锁定",WechatNotRunning:"微信未运行",WechatNotLoggedIn:"微信未登录",Pending:"待处理",WaitingForClient:"等待 Client",Accepted:"已接收",Running:"执行中",Succeeded:"已完成",Failed:"失败",Cancelled:"已取消",Expired:"已过期",ResultUnconfirmed:"待核对"},tp=new Set(["Succeeded","Failed","Cancelled","Expired","ResultUnconfirmed"]);class Jt extends Error{constructor(t,n,r="RequestFailed"){super(t),this.status=n,this.code=r}}async function Ye(e,{token:t,method:n="GET",body:r}={}){var u,a;const l={};t&&(l.Authorization=`Bearer ${t}`),r!==void 0&&(l["Content-Type"]="application/json");const i=await fetch(e,{method:n,headers:l,body:r===void 0?void 0:JSON.stringify(r)}),s=await i.json().catch(()=>({}));if(!i.ok)throw new Jt(((u=s.error)==null?void 0:u.message)||`请求失败(${i.status})`,i.status,(a=s.error)==null?void 0:a.code);return s}const np=e=>new Promise(t=>window.setTimeout(t,e)),rp={ReportingDisabled:"Client 未启用 Reporting 白名单。请在托盘远程连接页启用并配置白名单。",ReportingConfigInvalid:"Client 的 Reporting 配置无效,请检查托盘远程连接页。",AccountNotAuthorized:"当前账号未加入 Client 的 Reporting 白名单。",ChatNotAuthorized:"当前会话不在 Client 的 Reporting 白名单中。",ChatIdentityUnconfirmed:"当前会话身份尚未确认,暂不能读取。",DataTypeNotAuthorized:"当前读取类型未被 Reporting 白名单授权。"};function lp(e){var n,r;const t=((n=e.result)==null?void 0:n.error_code)||e.status;return rp[t]||((r=e.result)==null?void 0:r.message)||`读取任务${jc[e.status]||"未完成"}。`}async function ip(e,t){var i,s;const n=Date.now()+3e4;let r=250,l={status:"Pending"};for(;Date.now()t?`${e.slice(0,t)}…`:e:"—"}function op(e){return["Online","Succeeded","Accepted"].includes(e)?"positive":["Running","Pending","WaitingForClient","Degraded"].includes(e)?"warning":["Failed","Cancelled","Expired","ResultUnconfirmed","Offline","SessionLocked","WechatNotRunning","WechatNotLoggedIn"].includes(e)?"negative":"neutral"}function Nc(e){return jc[e]||e||"未知"}function ul(e){return!!(e&&["Online","Degraded","Registered"].includes(e.status))}function Ks(e){var t,n;return e?e.active_account_id?e.active_account_id:((n=(t=e.accounts)==null?void 0:t.find(r=>r.active&&r.verified))==null?void 0:n.account_id)||"":""}function up(e){return extractItems(e,["items","contacts","sessions"]).map((t,n)=>({id:String(t.contact_id??t.contactId??t.chat_id??t.chatId??t.id??n),title:String(t.name??t.display_name??t.displayName??t.nickname??t.contact_id??t.id??"未命名联系人"),type:t.chat_type??t.chatType??(t.is_group?"Group":"Private"),detail:String(t.remark??t.alias??t.account_id??t.accountId??"")}))}function B({name:e,size:t=18}){const n={message:o.jsxs(o.Fragment,{children:[o.jsx("path",{d:"M4 5.5A2.5 2.5 0 0 1 6.5 3h11A2.5 2.5 0 0 1 20 5.5v7a2.5 2.5 0 0 1-2.5 2.5H11l-4.5 3v-3h0A2.5 2.5 0 0 1 4 12.5z"}),o.jsx("path",{d:"M8 8h8M8 11h5"})]}),send:o.jsxs(o.Fragment,{children:[o.jsx("path",{d:"m3 4 18 8-18 8 3.5-8z"}),o.jsx("path",{d:"M6.5 12H21"})]}),contacts:o.jsxs(o.Fragment,{children:[o.jsx("circle",{cx:"9",cy:"8",r:"3"}),o.jsx("path",{d:"M3.5 19a5.5 5.5 0 0 1 11 0M17 7v6M14 10h6"})]}),tasks:o.jsxs(o.Fragment,{children:[o.jsx("rect",{x:"4",y:"3",width:"16",height:"18",rx:"2"}),o.jsx("path",{d:"M8 8h8M8 12h8M8 16h5"})]}),settings:o.jsxs(o.Fragment,{children:[o.jsx("path",{d:"M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M18.4 5.6l-2.1 2.1M7.7 16.3l-2.1 2.1"}),o.jsx("circle",{cx:"12",cy:"12",r:"4"})]}),refresh:o.jsxs(o.Fragment,{children:[o.jsx("path",{d:"M20 11a8 8 0 0 0-14.5-4.5L4 8"}),o.jsx("path",{d:"M4 4v4h4M4 13a8 8 0 0 0 14.5 4.5L20 16"}),o.jsx("path",{d:"M20 20v-4h-4"})]}),chevron:o.jsx("path",{d:"m9 6 6 6-6 6"}),search:o.jsxs(o.Fragment,{children:[o.jsx("circle",{cx:"10.5",cy:"10.5",r:"6.5"}),o.jsx("path",{d:"m16 16 4.5 4.5"})]}),shield:o.jsxs(o.Fragment,{children:[o.jsx("path",{d:"M12 3 19 6v5c0 4.2-2.8 7.8-7 9-4.2-1.2-7-4.8-7-9V6z"}),o.jsx("path",{d:"m9 12 2 2 4-4"})]}),alert:o.jsxs(o.Fragment,{children:[o.jsx("path",{d:"M12 4 21 20H3z"}),o.jsx("path",{d:"M12 10v4M12 17v.2"})]}),close:o.jsx(o.Fragment,{children:o.jsx("path",{d:"m6 6 12 12M18 6 6 18"})}),logout:o.jsx(o.Fragment,{children:o.jsx("path",{d:"M10 4H5v16h5M14 8l4 4-4 4M8 12h10"})}),arrow:o.jsx(o.Fragment,{children:o.jsx("path",{d:"M4 12h15M13 6l6 6-6 6"})}),check:o.jsx("path",{d:"m5 12 4 4L19 6"})};return o.jsx("svg",{"aria-hidden":"true",width:t,height:t,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.8",strokeLinecap:"round",strokeLinejoin:"round",children:n[e]||n.settings})}function fn({value:e}){return o.jsxs("span",{className:`status-badge ${op(e)}`,children:[o.jsx("i",{}),Nc(e)]})}function ap({onLogin:e}){const[t,n]=T.useState(""),[r,l]=T.useState(""),[i,s]=T.useState(""),[u,a]=T.useState(!1),f=async v=>{v.preventDefault(),s(""),a(!0);try{const h=await Ye("/v1/auth/login",{method:"POST",body:{username:t,password:r}});e(h.access_token,t)}catch(h){s(h.message)}finally{a(!1)}};return o.jsx("main",{className:"login-page",children:o.jsxs("section",{className:"login-panel","aria-labelledby":"login-title",children:[o.jsx("div",{className:"brand-mark",children:"W"}),o.jsx("p",{className:"eyebrow",children:"WXAGENT WORKSPACE"}),o.jsx("h1",{id:"login-title",children:"进入工作台"}),o.jsx("p",{className:"login-intro",children:"连接已授权的 Client,处理消息、通讯录和任务结果。"}),o.jsxs("form",{onSubmit:f,className:"login-form",children:[o.jsxs("label",{children:["用户名",o.jsx("input",{autoFocus:!0,value:t,onChange:v=>n(v.target.value),autoComplete:"username"})]}),o.jsxs("label",{children:["密码",o.jsx("input",{type:"password",value:r,onChange:v=>l(v.target.value),autoComplete:"current-password"})]}),i&&o.jsx("div",{className:"form-error",role:"alert",children:i}),o.jsx("button",{className:"primary-button full",disabled:u||!t||!r,children:u?"登录中…":"登录工作台"})]}),o.jsxs("p",{className:"login-footnote",children:[o.jsx("span",{className:"secure-dot"})," 会话只保存在当前浏览器标签页"]})]})})}function cp({view:e,setView:t,onLogout:n,username:r}){return o.jsxs("aside",{className:"sidebar",children:[o.jsxs("div",{className:"sidebar-brand",children:[o.jsx("div",{className:"brand-mark small",children:"W"}),o.jsxs("div",{children:[o.jsx("strong",{children:"WxAgent"}),o.jsx("span",{children:"用户工作台"})]})]}),o.jsx("p",{className:"nav-caption",children:"工作区"}),o.jsx("nav",{"aria-label":"主导航",children:kc.map(l=>o.jsxs("button",{className:e===l.id?"nav-item active":"nav-item",onClick:()=>t(l.id),children:[o.jsx(B,{name:l.icon,size:18}),o.jsx("span",{children:l.label})]},l.id))}),o.jsx("div",{className:"sidebar-spacer"}),o.jsxs("button",{className:e==="settings"?"nav-item active":"nav-item",onClick:()=>t("settings"),children:[o.jsx(B,{name:"settings",size:18}),o.jsx("span",{children:"设置与诊断"})]}),o.jsxs("div",{className:"connection-hint",children:[o.jsx("span",{className:"secure-dot"}),o.jsxs("div",{children:[o.jsx("strong",{children:"安全连接"}),o.jsx("small",{children:"控制面 API 已认证"})]})]}),o.jsxs("div",{className:"user-menu",children:[o.jsx("div",{className:"avatar",children:(r||"A").slice(0,1).toUpperCase()}),o.jsxs("div",{className:"user-name",children:[o.jsx("strong",{children:r||"管理员"}),o.jsx("small",{children:"已登录"})]}),o.jsx("button",{title:"退出登录","aria-label":"退出登录",onClick:n,children:o.jsx(B,{name:"logout",size:17})})]})]})}function dp({title:e,subtitle:t,nodes:n,selectedClientId:r,onClientChange:l,onRefresh:i,loading:s}){const u=n.find(a=>a.node_id===r);return o.jsxs("header",{className:"topbar",children:[o.jsxs("div",{className:"topbar-copy",children:[o.jsx("h1",{children:e}),o.jsx("p",{children:t})]}),o.jsxs("div",{className:"topbar-actions",children:[o.jsxs("label",{className:"client-switcher",children:[o.jsx("span",{children:"当前 Client"}),o.jsxs("select",{value:r,onChange:a=>l(a.target.value),disabled:!n.length,children:[!n.length&&o.jsx("option",{value:"",children:"暂无 Client"}),n.map(a=>o.jsxs("option",{value:a.node_id,children:[a.node_id," · ",Nc(a.status)]},a.node_id))]})]}),u&&o.jsx(fn,{value:u.status}),o.jsxs("button",{className:"refresh-button",onClick:i,disabled:s,"aria-label":"刷新数据",children:[o.jsx("span",{className:s?"spin":"",children:o.jsx(B,{name:"refresh",size:16})}),s?"同步中":"刷新"]})]})]})}function fp({nodes:e,selectedNode:t,onSettings:n}){if(!e.length)return o.jsxs("div",{className:"connection-banner negative",children:[o.jsx(B,{name:"alert",size:18}),o.jsxs("div",{children:[o.jsx("strong",{children:"还没有可用 Client"}),o.jsx("span",{children:"请启动已配置的 Desktop Agent;浏览器不会模拟连接状态。"})]}),o.jsx("button",{className:"text-button",onClick:n,children:"查看连接说明"})]});if(!ul(t)){const r=e.some(ul);return o.jsxs("div",{className:"connection-banner warning",children:[o.jsx(B,{name:"alert",size:18}),o.jsxs("div",{children:[o.jsx("strong",{children:"当前 Client 不可用"}),o.jsx("span",{children:r?"其它 Client 不受影响,请从顶部切换。":"所有 Client 当前都不可用,请先恢复连接。"})]}),o.jsx("button",{className:"text-button",onClick:n,children:"设置与诊断"})]})}return Ks(t)?null:o.jsxs("div",{className:"connection-banner warning",children:[o.jsx(B,{name:"alert",size:18}),o.jsxs("div",{children:[o.jsx("strong",{children:"当前 Client 尚未确认微信账号"}),o.jsx("span",{children:"只读数据需要已验证的活动账号,先完成账号绑定后再操作。"})]}),o.jsx("button",{className:"text-button",onClick:n,children:"查看账号"})]})}function Xi({title:e="没有选中的 Client",text:t="请先连接并选择一个可用 Client。",onSettings:n}){return o.jsxs("div",{className:"context-empty",children:[o.jsx("div",{className:"empty-icon",children:o.jsx(B,{name:"shield",size:28})}),o.jsx("h3",{children:e}),o.jsx("p",{children:t}),n&&o.jsx("button",{className:"secondary-button",onClick:n,children:"打开设置与诊断"})]})}function Zi({text:e="正在读取…"}){return o.jsxs("div",{className:"loading-state",children:[o.jsx("span",{className:"loader"}),e]})}function Ot({text:e,action:t}){return o.jsxs("div",{className:"empty-state",children:[o.jsx("div",{className:"empty-icon",children:o.jsx(B,{name:"search",size:26})}),o.jsx("p",{children:e}),t]})}function Ji({error:e,onRetry:t}){return o.jsxs("div",{className:"inline-error",role:"alert",children:[o.jsx(B,{name:"alert",size:17}),o.jsx("span",{children:e}),t&&o.jsx("button",{className:"text-button",onClick:t,children:"重试"})]})}function Nr({text:e,onRetry:t}){return o.jsxs("div",{className:"read-notice",role:"status",children:[o.jsx(B,{name:"alert",size:16}),o.jsx("span",{children:e}),t&&o.jsx("button",{className:"text-button",onClick:t,children:"重试"})]})}function ou(e,t){if(!t)return"";const n=t.lastSuccessAt?`最后同步 ${It(t.lastSuccessAt)}`:"尚未成功同步",r=t.backlogCount==null?"积压未知":`积压 ${t.backlogCount}`,l=`${n} · ${r}`;return t.state==="complete"?t.source==="platform-cache"?`${e}来自平台副本,${l}。`:"":t.state==="partial"?`${e}同步不完整,已保留已有数据;${l}。`:`${e}同步状态未知,未用本次结果清除已有数据;${l}${t.errorMessage?` · ${t.errorMessage}`:""}。`}function pp({token:e,client:t,onSettings:n}){const r=(t==null?void 0:t.node_id)||"",l=Ks(t),[i,s]=T.useState([]),[u,a]=T.useState({loading:!1,error:"",notice:""}),[f,v]=T.useState(""),[h,m]=T.useState(null),[x,k]=T.useState([]),[S,P]=T.useState({loading:!1,error:"",notice:""}),d=T.useRef(0),c=T.useRef(0),p=T.useRef(0),g=T.useRef(0),j=T.useCallback(async()=>{if(!l||!r)return;const w=++g.current;try{await Ye(ni(l,"refresh"),{token:e,method:"POST"})}catch(I){w===g.current&&a(z=>({...z,notice:z.notice||`后台同步未受理:${I.message}`}))}},[l,r,e]),C=T.useCallback(async()=>{if(!r||!l)return;const w=++c.current,I=d.current;a(z=>({...z,loading:!0,error:""}));try{const z=await su({storedPath:ni(l,"conversations","?limit=100&offset=0"),legacyPath:"/v1/reads/sessions",legacyBody:{node_id:r,account_id:l,limit:100},token:e});if(w!==c.current||I!==d.current)return;const Y=bf(z);z.sync&&z.sync.state!=="complete"&&j();const O=ru(z,Y.length);s(pe=>iu(O)?Y:lu(pe,Y)),a({loading:!1,error:"",notice:ou("会话",O)})}catch(z){w===c.current&&I===d.current&&a({loading:!1,error:z.message,notice:""})}},[l,r,j,e]);if(T.useEffect(()=>{d.current+=1,c.current+=1,p.current+=1,m(null),s([]),k([]),a({loading:!1,error:"",notice:""}),P({loading:!1,error:"",notice:""}),!(!r||!l)&&C()},[r,l,C]),T.useEffect(()=>{if(!h||h.clientId!==r||h.accountId!==l||!r||!l)return;const w=++p.current,I=d.current;P(z=>({...z,loading:!0,error:""})),su({storedPath:ni(l,"messages",`?chat_id=${encodeURIComponent(h.id)}&limit=100&offset=0`),legacyPath:"/v1/reads/messages",legacyBody:{node_id:r,account_id:l,chat_id:h.id,limit:100,include_content:!0},token:e}).then(z=>{if(w!==p.current||I!==d.current)return;const Y=ep(z),O=ru(z,Y.length);k(pe=>iu(O)?Y:lu(pe,Y)),P({loading:!1,error:"",notice:ou("消息",O)})}).catch(z=>{w===p.current&&I===d.current&&P({loading:!1,error:z.message,notice:""})})},[l,r,h,e]),!t)return o.jsx(Xi,{onSettings:n});const E=i.filter(w=>!f||`${w.title} ${w.id}`.toLowerCase().includes(f.toLowerCase()));return o.jsxs("div",{className:"message-workspace",children:[o.jsxs("section",{className:"session-panel",children:[o.jsxs("div",{className:"section-heading compact",children:[o.jsxs("div",{children:[o.jsx("h2",{children:"会话"}),o.jsx("p",{children:i.length?`${i.length} 个会话`:"当前 Client 的会话"})]}),o.jsx("button",{className:"icon-button",onClick:C,disabled:u.loading,"aria-label":"刷新会话",children:o.jsx(B,{name:"refresh",size:16})})]}),o.jsxs("label",{className:"search-box",children:[o.jsx(B,{name:"search",size:16}),o.jsx("input",{value:f,onChange:w=>v(w.target.value),placeholder:"搜索会话"})]}),u.loading&&i.length===0?o.jsx(Zi,{text:"正在读取会话…"}):u.error&&i.length===0?o.jsx(Ji,{error:u.error,onRetry:C}):o.jsxs(o.Fragment,{children:[u.error&&o.jsx(Nr,{text:`读取会话失败,继续显示上次数据:${u.error}`,onRetry:C}),!u.error&&u.notice&&o.jsx(Nr,{text:u.notice,onRetry:C}),u.loading&&i.length>0&&o.jsx("div",{className:"refresh-hint",children:"正在刷新,会话列表保持可用…"}),E.length===0?o.jsx(Ot,{text:"暂无会话,或当前账号还没有可见数据。"}):o.jsx("div",{className:"session-list",children:E.map(w=>o.jsxs("button",{className:(h==null?void 0:h.id)===w.id?"session-row selected":"session-row",onClick:()=>m({...w,clientId:r,accountId:l}),children:[o.jsx("span",{className:"session-avatar",children:w.title.slice(0,1).toUpperCase()}),o.jsxs("span",{className:"session-copy",children:[o.jsx("strong",{children:w.title}),o.jsx("small",{children:w.preview})]}),w.unread>0&&o.jsx("b",{children:w.unread})]},w.id))})]})]}),o.jsx("section",{className:"conversation-panel",children:h?o.jsxs(o.Fragment,{children:[o.jsxs("div",{className:"conversation-head",children:[o.jsxs("div",{children:[o.jsxs("p",{className:"eyebrow",children:["当前 Client · ",r]}),o.jsx("h2",{children:h.title}),o.jsxs("span",{children:[h.type==="Group"?"群聊":"私聊"," · ",Nl(h.id,28)]})]}),o.jsx(fn,{value:t.status})]}),S.loading&&x.length===0?o.jsx(Zi,{text:"正在读取消息…"}):S.error&&x.length===0?o.jsx(Ji,{error:S.error,onRetry:()=>m({...h})}):o.jsxs(o.Fragment,{children:[S.error&&o.jsx(Nr,{text:`读取消息失败,继续显示上次数据:${S.error}`,onRetry:()=>m({...h})}),!S.error&&S.notice&&o.jsx(Nr,{text:S.notice,onRetry:()=>m({...h})}),S.loading&&x.length>0&&o.jsx("div",{className:"refresh-hint",children:"正在刷新,消息历史保持可用…"}),x.length===0?o.jsx(Ot,{text:"这个会话暂时没有可显示的消息。"}):o.jsx("div",{className:"message-list",children:x.map(w=>o.jsxs("article",{className:"message-row",children:[o.jsx("div",{className:"message-avatar",children:w.sender.slice(0,1).toUpperCase()}),o.jsxs("div",{children:[o.jsxs("div",{className:"message-meta",children:[o.jsx("strong",{children:w.sender}),o.jsx("span",{children:It(w.at)})]}),o.jsx("p",{children:w.text||"(无正文)"})]})]},w.id))})]}),o.jsxs("div",{className:"composer",children:[o.jsx("textarea",{disabled:!0,rows:2,placeholder:"发送消息暂未开放,需完成 Windows 真机验收"}),o.jsx("button",{className:"primary-button",disabled:!0,title:"写操作尚未通过真机验收",children:"发送"})]})]}):o.jsx(Xi,{title:"选择一个会话",text:"从左侧选择会话后,读取该 Client 的消息。"})})]})}function hp({client:e,onSettings:t}){return o.jsxs("section",{className:"workspace-panel gated-panel",children:[o.jsx("div",{className:"gated-mark",children:o.jsx(B,{name:"shield",size:24})}),o.jsxs("div",{children:[o.jsx("p",{className:"eyebrow",children:"CONTROLLED ACTION"}),o.jsx("h2",{children:"受控群发验证"}),o.jsx("p",{children:"单 Client 的 Client 端已提供 broadcast-text:冻结获准对象名单,逐项串行发送,返回每个对象的结果,并支持停止和幂等重放。"}),o.jsxs("div",{className:"step-list",children:[o.jsxs("span",{children:[o.jsx("b",{children:"1"}),"冻结对象"]}),o.jsxs("span",{children:[o.jsx("b",{children:"2"}),"预览确认"]}),o.jsxs("span",{children:[o.jsx("b",{children:"3"}),"逐项发送"]}),o.jsxs("span",{children:[o.jsx("b",{children:"4"}),"查看结果"]})]}),o.jsx("button",{className:"primary-button",disabled:!0,children:e?"Web 提交仍需单独验收":"请先连接 Client"}),o.jsxs("button",{className:"text-button",onClick:t,children:["查看 Client 验证状态 ",o.jsx(B,{name:"arrow",size:14})]})]})]})}function mp({token:e,client:t,onSettings:n}){const r=(t==null?void 0:t.node_id)||"",l=Ks(t),[i,s]=T.useState(!1),[u,a]=T.useState(""),[f,v]=T.useState([]),[h,m]=T.useState({loading:!1,error:""}),x=T.useRef(0),k=T.useCallback(async()=>{if(!r||!l)return;const P=++x.current;m({loading:!0,error:""});try{const d=await Cc("/v1/reads/contacts",{node_id:r,account_id:l,limit:200,groups_only:i,contains:u},e);if(P!==x.current)return;v(up(d)),m({loading:!1,error:""})}catch(d){P===x.current&&m({loading:!1,error:d.message})}},[l,r,i,u,e]);if(T.useEffect(()=>{v([]),r&&l&&k()},[r,l,i]),!t)return o.jsx(Xi,{onSettings:n});const S=f.filter(P=>!u||`${P.title} ${P.id} ${P.detail}`.toLowerCase().includes(u.toLowerCase()));return o.jsxs("section",{className:"workspace-panel",children:[o.jsxs("div",{className:"section-heading",children:[o.jsxs("div",{children:[o.jsx("h2",{children:"通讯录"}),o.jsx("p",{children:"只读查看当前 Client 已授权的联系人和群聊。"})]}),o.jsxs("div",{className:"heading-actions",children:[o.jsxs("label",{className:"check-filter",children:[o.jsx("input",{type:"checkbox",checked:i,onChange:P=>s(P.target.checked)}),"只看群聊"]}),o.jsx("button",{className:"secondary-button",disabled:!0,title:"添加好友尚未通过真机验收",children:"添加好友"})]})]}),o.jsxs("div",{className:"toolbar",children:[o.jsxs("label",{className:"search-box wide",children:[o.jsx(B,{name:"search",size:16}),o.jsx("input",{value:u,onChange:P=>a(P.target.value),onKeyDown:P=>P.key==="Enter"&&k(),placeholder:"搜索联系人或群聊"})]}),o.jsx("button",{className:"secondary-button",onClick:k,disabled:h.loading,children:h.loading?"读取中…":"刷新"})]}),h.loading?o.jsx(Zi,{text:"正在读取通讯录…"}):h.error?o.jsx(Ji,{error:h.error,onRetry:k}):S.length===0?o.jsx(Ot,{text:"暂无可见联系人或群聊。"}):o.jsx("div",{className:"contact-list",children:S.map(P=>o.jsxs("div",{className:"contact-row",children:[o.jsx("span",{className:"contact-avatar",children:P.title.slice(0,1).toUpperCase()}),o.jsxs("div",{children:[o.jsx("strong",{children:P.title}),o.jsxs("small",{children:[P.type==="Group"?"群聊":"联系人"," · ",Nl(P.id,28),P.detail?` · ${P.detail}`:""]})]}),o.jsx("button",{className:"text-button",disabled:!0,title:"写操作尚未通过真机验收",children:"添加好友"})]},P.id))})]})}function vp({tasks:e,selectedClientId:t}){const[n,r]=T.useState("all"),l=e.filter(i=>i.node_id===t&&(n==="all"||i.status===n));return o.jsxs("section",{className:"workspace-panel",children:[o.jsxs("div",{className:"section-heading",children:[o.jsxs("div",{children:[o.jsx("h2",{children:"任务结果"}),o.jsx("p",{children:"当前 Client 的任务状态和安全边界。"})]}),o.jsxs("div",{className:"heading-actions",children:[o.jsxs("span",{className:"read-only-note",children:[o.jsx(B,{name:"shield",size:14}),"只读视图"]}),o.jsxs("select",{value:n,onChange:i=>r(i.target.value),children:[o.jsx("option",{value:"all",children:"全部状态"}),o.jsx("option",{value:"Pending",children:"待处理"}),o.jsx("option",{value:"WaitingForClient",children:"等待 Client"}),o.jsx("option",{value:"Running",children:"执行中"}),o.jsx("option",{value:"Succeeded",children:"已完成"}),o.jsx("option",{value:"Failed",children:"失败"}),o.jsx("option",{value:"ResultUnconfirmed",children:"待核对"})]})]})]}),l.length===0?o.jsx(Ot,{text:"当前 Client 暂无任务记录。"}):o.jsx("div",{className:"task-list",children:l.map(i=>{var s,u;return o.jsxs("article",{className:"task-row",children:[o.jsxs("div",{className:"task-main",children:[o.jsxs("div",{className:"task-title",children:[o.jsx("strong",{children:i.kind}),o.jsx(fn,{value:i.status})]}),o.jsxs("p",{children:[i.account_id," · ",Nl(i.task_id,22)]}),o.jsx("small",{children:i.status==="WaitingForClient"?"Client 离线后等待显式恢复;不会自动重放写操作。":((s=i.result)==null?void 0:s.message)||((u=i.result)==null?void 0:u.error_code)||`更新于 ${It(i.updated_at)}`})]}),o.jsxs("div",{className:"task-meta",children:[o.jsxs("span",{children:["第 ",i.lease_generation||0," 代租约"]}),o.jsx("time",{children:It(i.updated_at)})]})]},i.task_id)})})]})}function gp({nodes:e}){return o.jsx("div",{className:"diagnostic-list",children:e.length===0?o.jsx(Ot,{text:"暂无注册 Client。"}):e.map(t=>o.jsxs("article",{className:"diagnostic-row",children:[o.jsxs("div",{className:"diagnostic-title",children:[o.jsx("span",{className:"node-avatar",children:t.node_id.slice(0,1).toUpperCase()}),o.jsxs("div",{children:[o.jsx("strong",{children:t.node_id}),o.jsxs("small",{children:[t.active_account_id||"尚未确认活动账号"," · 最近心跳 ",It(t.last_heartbeat_at)]})]})]}),o.jsx(fn,{value:t.status})]},t.node_id))})}function yp({events:e,selectedClientId:t}){const n=e.filter(r=>!t||r.node_id===t);return o.jsx("div",{className:"diagnostic-list",children:n.length===0?o.jsx(Ot,{text:"当前 Client 暂无事件。"}):n.slice(0,30).map(r=>o.jsxs("article",{className:"diagnostic-row",children:[o.jsxs("div",{children:[o.jsx("strong",{children:Nl(r.chat_id,24)}),o.jsxs("small",{children:[r.node_id," · ",r.chat_type==="Group"?"群聊":"私聊"," · ",It(r.received_at)]})]}),o.jsx("span",{className:"event-preview",children:r.content||"无正文"})]},r.event_id))})}function xp({audit:e}){return o.jsx("div",{className:"diagnostic-list",children:e.length===0?o.jsx(Ot,{text:"暂无审计记录。"}):e.slice(0,30).map(t=>o.jsxs("article",{className:"diagnostic-row",children:[o.jsxs("div",{children:[o.jsx("strong",{children:t.action}),o.jsxs("small",{children:[t.principal," · ",t.resource]})]}),o.jsx("span",{children:It(t.at)})]},t.id))})}function wp({nodes:e,events:t,audit:n,selectedClientId:r,onSelectClient:l}){const[i,s]=T.useState("connection");return o.jsxs("section",{className:"settings-layout",children:[o.jsxs("aside",{className:"settings-nav",children:[o.jsxs("button",{className:i==="connection"?"settings-tab active":"settings-tab",onClick:()=>s("connection"),children:[o.jsx("strong",{children:"连接与账号"}),o.jsx("span",{children:"选择 Client、确认状态"})]}),o.jsxs("button",{className:i==="diagnostics"?"settings-tab active":"settings-tab",onClick:()=>s("diagnostics"),children:[o.jsx("strong",{children:"高级诊断"}),o.jsx("span",{children:"节点、事件、AI、审计"})]})]}),o.jsx("div",{className:"settings-content",children:i==="connection"?o.jsxs(o.Fragment,{children:[o.jsxs("div",{className:"workspace-panel",children:[o.jsxs("div",{className:"section-heading",children:[o.jsxs("div",{children:[o.jsx("h2",{children:"连接与账号"}),o.jsx("p",{children:"每个 Client 独立连接,单个 Client 离线不会阻断其它 Client。"})]}),o.jsx(fn,{value:e.some(ul)?"Online":"Offline"})]}),o.jsx(gp,{nodes:e})]}),o.jsxs("div",{className:"workspace-panel",children:[o.jsx("div",{className:"section-heading",children:o.jsxs("div",{children:[o.jsx("h2",{children:"安全边界"}),o.jsx("p",{children:"当前版本只开放真实后端已支持且可验证的只读能力。"})]})}),o.jsxs("ul",{className:"boundary-list",children:[o.jsxs("li",{children:[o.jsx(B,{name:"check",size:15}),"消息、通讯录、任务结果按选定 Client 隔离读取"]}),o.jsxs("li",{children:[o.jsx(B,{name:"check",size:15}),"Client 掉线只影响当前上下文,任务不会自动改投"]}),o.jsxs("li",{children:[o.jsx(B,{name:"alert",size:15}),"发送、群发、添加好友等待 Windows 真机验收"]})]})]})]}):o.jsxs(o.Fragment,{children:[o.jsxs("div",{className:"workspace-panel",children:[o.jsx("div",{className:"section-heading",children:o.jsxs("div",{children:[o.jsx("h2",{children:"Client 诊断"}),o.jsx("p",{children:"技术状态只在这里展示,不干扰普通操作。"})]})}),o.jsx("div",{className:"client-pills",children:e.map(u=>o.jsxs("button",{className:u.node_id===r?"client-pill active":"client-pill",onClick:()=>l(u.node_id),children:[o.jsx("span",{children:u.node_id}),o.jsx(fn,{value:u.status})]},u.node_id))})]}),o.jsx("div",{className:"workspace-panel",children:o.jsxs("div",{className:"section-heading",children:[o.jsxs("div",{children:[o.jsx("h2",{children:"AI 能力"}),o.jsx("p",{children:"AI 入口保留在高级诊断中;当前不自动调用,也不影响消息、通讯录和任务操作。"})]}),o.jsxs("span",{className:"read-only-note",children:[o.jsx(B,{name:"shield",size:14}),"按能力矩阵开放"]})]})}),o.jsxs("div",{className:"workspace-panel",children:[o.jsx("div",{className:"section-heading",children:o.jsxs("div",{children:[o.jsx("h2",{children:"白名单事件"}),o.jsx("p",{children:"当前选中 Client 的已授权事件。"})]})}),o.jsx(yp,{events:t,selectedClientId:r})]}),o.jsxs("div",{className:"workspace-panel",children:[o.jsx("div",{className:"section-heading",children:o.jsxs("div",{children:[o.jsx("h2",{children:"操作审计"}),o.jsx("p",{children:"用于定位连接、任务和权限问题。"})]})}),o.jsx(xp,{audit:n})]})]})})]})}function Sp(){const[e,t]=T.useState(()=>sessionStorage.getItem("wxagent-token")||""),[n,r]=T.useState(()=>sessionStorage.getItem("wxagent-user")||""),[l,i]=T.useState("messages"),[s,u]=T.useState([]),[a,f]=T.useState(()=>sessionStorage.getItem("wxagent-client")||""),[v,h]=T.useState([]),[m,x]=T.useState([]),[k,S]=T.useState([]),[P,d]=T.useState(!1),[c,p]=T.useState(""),g=T.useCallback(()=>{sessionStorage.removeItem("wxagent-token"),sessionStorage.removeItem("wxagent-user"),sessionStorage.removeItem("wxagent-client"),t(""),r("")},[]),j=T.useCallback(async()=>{if(e){d(!0),p("");try{const[O,pe,ir,_l]=await Promise.all([Ye("/v1/nodes",{token:e}),Ye("/v1/tasks?limit=200",{token:e}),Ye("/v1/events?limit=200",{token:e}),Ye("/v1/audit?limit=200",{token:e})]);u(O.nodes||[]),h(pe.tasks||[]),x(ir.events||[]),S(_l.audit||[])}catch(O){O.status===401?g():p(O.message)}finally{d(!1)}}},[g,e]);T.useEffect(()=>{if(j(),!e)return;const O=window.setInterval(j,15e3);return()=>window.clearInterval(O)},[j,e]),T.useEffect(()=>{if(!s.length){f("");return}if(!s.some(O=>O.node_id===a)){const O=s.find(ul)||s[0];f(O.node_id),sessionStorage.setItem("wxagent-client",O.node_id)}},[s,a]);const C=O=>{f(O),sessionStorage.setItem("wxagent-client",O)},E=(O,pe)=>{sessionStorage.setItem("wxagent-token",O),sessionStorage.setItem("wxagent-user",pe),t(O),r(pe)},w=T.useMemo(()=>s.find(O=>O.node_id===a),[s,a]),I=kc.find(O=>O.id===l),z=(I==null?void 0:I.label)||"设置与诊断",Y=(I==null?void 0:I.description)||"连接、账号和高级诊断信息";return e?o.jsxs("div",{className:"app-shell",children:[o.jsx(cp,{view:l,setView:i,onLogout:g,username:n}),o.jsxs("main",{className:"main-area",children:[o.jsx(dp,{title:z,subtitle:Y,nodes:s,selectedClientId:a,onClientChange:C,onRefresh:j,loading:P}),c&&o.jsxs("div",{className:"global-error",role:"alert",children:[o.jsx(B,{name:"alert",size:16}),o.jsx("span",{children:c}),o.jsx("button",{onClick:()=>p(""),"aria-label":"关闭错误",children:o.jsx(B,{name:"close",size:16})})]}),o.jsx(fp,{nodes:s,selectedNode:w,onSettings:()=>i("settings")}),o.jsxs("div",{className:"page-content",children:[l==="messages"&&o.jsx(pp,{token:e,client:w,onSettings:()=>i("settings")}),l==="broadcast"&&o.jsx(hp,{client:w,onSettings:()=>i("settings")}),l==="contacts"&&o.jsx(mp,{token:e,client:w,onSettings:()=>i("settings")}),l==="tasks"&&o.jsx(vp,{tasks:v,selectedClientId:a}),l==="settings"&&o.jsx(wp,{nodes:s,events:m,audit:k,selectedClientId:a,onSelectClient:C})]}),o.jsxs("footer",{className:"footer",children:[o.jsx("span",{children:"WxAgent 工作台 · Client 独立隔离"}),o.jsx("span",{children:"写操作需通过真实能力与真机验收"})]})]})]}):o.jsx(ap,{onLogin:E})}wc(document.getElementById("root")).render(o.jsx(Sp,{})); diff --git a/control-plane/web/dist/index.html b/control-plane/web/dist/index.html index 9886a08..ae941bf 100644 --- a/control-plane/web/dist/index.html +++ b/control-plane/web/dist/index.html @@ -6,8 +6,8 @@ WxAgent 工作台 - - + +
diff --git a/control-plane/web/package.json b/control-plane/web/package.json index 2f4b1ad..75d82ff 100644 --- a/control-plane/web/package.json +++ b/control-plane/web/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "vite", "build": "vite build", + "test": "node --test src/readState.test.js", "preview": "vite preview" }, "dependencies": { diff --git a/control-plane/web/src/main.jsx b/control-plane/web/src/main.jsx index a061528..4799b8f 100644 --- a/control-plane/web/src/main.jsx +++ b/control-plane/web/src/main.jsx @@ -1,6 +1,13 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { createRoot } from "react-dom/client"; import "./styles.css"; +import { + mergeStableItems, + normalizeMessages, + normalizeSessions, + resolveReadCoverage, + shouldReplaceReadState, +} from "./readState.js"; const NAV_ITEMS = [ { id: "messages", label: "消息", description: "查找会话、阅读并回复", icon: "message" }, @@ -118,6 +125,23 @@ async function readWithTask(path, body, token) { return waitForTask(token, created.task_id); } +function dataPath(accountId, resource, query = "") { + return `/v1/data/accounts/${encodeURIComponent(accountId)}/${resource}${query}`; +} + +function canUseLegacyReadFallback(reason) { + return ["AccountDataNotFound", "AccountDataUnavailable", "DataStatusFailed"].includes(reason.code); +} + +async function readStoredOrFallback({ storedPath, legacyPath, legacyBody, token }) { + try { + return await api(storedPath, { token }); + } catch (reason) { + if (!canUseLegacyReadFallback(reason)) throw reason; + return readWithTask(legacyPath, legacyBody, token); + } +} + function formatTime(value) { if (!value) return "—"; const date = new Date(value); @@ -164,33 +188,6 @@ function activeAccount(node) { return node.accounts?.find((account) => account.active && account.verified)?.account_id || ""; } -function extractItems(content, keys) { - if (Array.isArray(content)) return content; - for (const key of keys) { - if (Array.isArray(content?.[key])) return content[key]; - } - return []; -} - -function normalizeSessions(content) { - return extractItems(content, ["items", "sessions", "chats"]).map((item, index) => ({ - id: String(item.chat_id ?? item.chatId ?? item.session_id ?? item.sessionId ?? item.automation_id ?? item.automationId ?? item.id ?? index), - title: String(item.name ?? item.display_name ?? item.displayName ?? item.nickname ?? item.chat_id ?? item.id ?? "未命名会话"), - preview: String(item.last_message ?? item.lastMessage ?? item.preview ?? "暂无最近消息"), - unread: Number(item.unread_count ?? item.unreadCount ?? 0), - type: item.chat_type ?? item.chatType ?? "Private", - })); -} - -function normalizeMessages(content) { - return extractItems(content, ["items", "messages"]).map((item, index) => ({ - id: String(item.message_id ?? item.messageId ?? item.id ?? index), - sender: String(item.sender ?? item.sender_name ?? item.senderName ?? "对方"), - text: typeof item === "string" ? item : String(item.content ?? item.text ?? item.message ?? ""), - at: item.occurred_at ?? item.occurredAt ?? item.created_at ?? item.createdAt, - })); -} - function normalizeContacts(content) { return extractItems(content, ["items", "contacts", "sessions"]).map((item, index) => ({ id: String(item.contact_id ?? item.contactId ?? item.chat_id ?? item.chatId ?? item.id ?? index), @@ -351,52 +348,105 @@ function ReadError({ error, onRetry }) { return
{error}{onRetry && }
; } +function ReadNotice({ text, onRetry }) { + return
{text}{onRetry && }
; +} + +function coverageNotice(label, coverage) { + if (!coverage) return ""; + const freshness = coverage.lastSuccessAt ? `最后同步 ${formatTime(coverage.lastSuccessAt)}` : "尚未成功同步"; + const backlog = coverage.backlogCount == null ? "积压未知" : `积压 ${coverage.backlogCount}`; + const suffix = `${freshness} · ${backlog}`; + if (coverage.state === "complete") return coverage.source === "platform-cache" ? `${label}来自平台副本,${suffix}。` : ""; + if (coverage.state === "partial") return `${label}同步不完整,已保留已有数据;${suffix}。`; + return `${label}同步状态未知,未用本次结果清除已有数据;${suffix}${coverage.errorMessage ? ` · ${coverage.errorMessage}` : ""}。`; +} + function MessagesView({ token, client, onSettings }) { const clientId = client?.node_id || ""; const accountId = activeAccount(client); const [sessions, setSessions] = useState([]); - const [sessionsState, setSessionsState] = useState({ loading: false, error: "" }); + const [sessionsState, setSessionsState] = useState({ loading: false, error: "", notice: "" }); const [query, setQuery] = useState(""); const [selectedChat, setSelectedChat] = useState(null); const [messages, setMessages] = useState([]); - const [messagesState, setMessagesState] = useState({ loading: false, error: "" }); - const requestRef = useRef(0); + const [messagesState, setMessagesState] = useState({ loading: false, error: "", notice: "" }); + const contextGenerationRef = useRef(0); + const sessionsRequestRef = useRef(0); + const messagesRequestRef = useRef(0); + const refreshRequestRef = useRef(0); + + const requestDataRefresh = useCallback(async () => { + if (!accountId || !clientId) return; + const requestId = ++refreshRequestRef.current; + try { + await api(dataPath(accountId, "refresh"), { token, method: "POST" }); + } catch (reason) { + if (requestId === refreshRequestRef.current) { + setSessionsState((current) => ({ ...current, notice: current.notice || `后台同步未受理:${reason.message}` })); + } + } + }, [accountId, clientId, token]); const loadSessions = useCallback(async () => { if (!clientId || !accountId) return; - const requestId = ++requestRef.current; - setSessionsState({ loading: true, error: "" }); + const requestId = ++sessionsRequestRef.current; + const generation = contextGenerationRef.current; + setSessionsState((current) => ({ ...current, loading: true, error: "" })); try { - const content = await readWithTask("/v1/reads/sessions", { node_id: clientId, account_id: accountId, limit: 100 }, token); - if (requestId !== requestRef.current) return; - setSessions(normalizeSessions(content)); + const content = await readStoredOrFallback({ + storedPath: dataPath(accountId, "conversations", "?limit=100&offset=0"), + legacyPath: "/v1/reads/sessions", + legacyBody: { node_id: clientId, account_id: accountId, limit: 100 }, + token, + }); + if (requestId !== sessionsRequestRef.current || generation !== contextGenerationRef.current) return; + const next = normalizeSessions(content); + if (content.sync && content.sync.state !== "complete") void requestDataRefresh(); + const coverage = resolveReadCoverage(content, next.length); + setSessions((previous) => shouldReplaceReadState(coverage) ? next : mergeStableItems(previous, next)); + setSessionsState({ loading: false, error: "", notice: coverageNotice("会话", coverage) }); } catch (reason) { - if (requestId === requestRef.current) setSessionsState({ loading: false, error: reason.message }); - return; + if (requestId === sessionsRequestRef.current && generation === contextGenerationRef.current) { + setSessionsState({ loading: false, error: reason.message, notice: "" }); + } } - setSessionsState({ loading: false, error: "" }); - }, [accountId, clientId, token]); + }, [accountId, clientId, requestDataRefresh, token]); useEffect(() => { + contextGenerationRef.current += 1; + sessionsRequestRef.current += 1; + messagesRequestRef.current += 1; setSelectedChat(null); setSessions([]); setMessages([]); + setSessionsState({ loading: false, error: "", notice: "" }); + setMessagesState({ loading: false, error: "", notice: "" }); if (!clientId || !accountId) return; loadSessions(); }, [clientId, accountId, loadSessions]); useEffect(() => { - if (!selectedChat || selectedChat.clientId !== clientId || !clientId || !accountId) return; - const requestId = ++requestRef.current; - setMessagesState({ loading: true, error: "" }); - readWithTask("/v1/reads/messages", { node_id: clientId, account_id: accountId, chat_id: selectedChat.id, limit: 100, include_content: true }, token) - .then((content) => { - if (requestId !== requestRef.current) return; - setMessages(normalizeMessages(content)); - setMessagesState({ loading: false, error: "" }); + if (!selectedChat || selectedChat.clientId !== clientId || selectedChat.accountId !== accountId || !clientId || !accountId) return; + const requestId = ++messagesRequestRef.current; + const generation = contextGenerationRef.current; + setMessagesState((current) => ({ ...current, loading: true, error: "" })); + readStoredOrFallback({ + storedPath: dataPath(accountId, "messages", `?chat_id=${encodeURIComponent(selectedChat.id)}&limit=100&offset=0`), + legacyPath: "/v1/reads/messages", + legacyBody: { node_id: clientId, account_id: accountId, chat_id: selectedChat.id, limit: 100, include_content: true }, + token, + }).then((content) => { + if (requestId !== messagesRequestRef.current || generation !== contextGenerationRef.current) return; + const next = normalizeMessages(content); + const coverage = resolveReadCoverage(content, next.length); + setMessages((previous) => shouldReplaceReadState(coverage) ? next : mergeStableItems(previous, next)); + setMessagesState({ loading: false, error: "", notice: coverageNotice("消息", coverage) }); }) .catch((reason) => { - if (requestId === requestRef.current) setMessagesState({ loading: false, error: reason.message }); + if (requestId === messagesRequestRef.current && generation === contextGenerationRef.current) { + setMessagesState({ loading: false, error: reason.message, notice: "" }); + } }); }, [accountId, clientId, selectedChat, token]); @@ -407,10 +457,10 @@ function MessagesView({ token, client, onSettings }) {

会话

{sessions.length ? `${sessions.length} 个会话` : "当前 Client 的会话"}

- {sessionsState.loading ? : sessionsState.error ? : visibleSessions.length === 0 ? :
{visibleSessions.map((session) => )}
} + {sessionsState.loading && sessions.length === 0 ? : sessionsState.error && sessions.length === 0 ? : <>{sessionsState.error && }{!sessionsState.error && sessionsState.notice && }{sessionsState.loading && sessions.length > 0 &&
正在刷新,会话列表保持可用…
}{visibleSessions.length === 0 ? :
{visibleSessions.map((session) => )}
}}
- {!selectedChat ? : <>

当前 Client · {clientId}

{selectedChat.title}

{selectedChat.type === "Group" ? "群聊" : "私聊"} · {shortId(selectedChat.id, 28)}
{messagesState.loading ? : messagesState.error ? setSelectedChat({ ...selectedChat })} /> : messages.length === 0 ? :
{messages.map((message) =>
{message.sender.slice(0, 1).toUpperCase()}
{message.sender}{formatTime(message.at)}

{message.text || "(无正文)"}

)}
}