diff --git a/.env.example b/.env.example index f807d5f..8e33b98 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,27 @@ OPENCLAW_GATEWAY_PORT=18789 # Development Configuration VITE_DEV_SERVER_PORT=5173 +# Zhinian service connection +# Required for real login. The desktop app no longer falls back to demo login +# unless YINIAN_CONTROL_PLANE_MODE=mock is explicitly set. +YINIAN_API_BASE_URL=https://onefeel.brother7.cn/ingress +YINIAN_AUTH_CLIENT_ID=customPC +YINIAN_AUTH_SCOPE=server +# Optional, depending on your server OAuth client settings: +# YINIAN_AUTH_CLIENT_SECRET=customPC +# YINIAN_AUTH_BASIC=Basic Y3VzdG9tUEM6Y3VzdG9tUEM= +# Optional enterprise-space/application endpoints. Template variables are supported: +# {workspaceId}, {workspace_id}, {hotelId}, {hotel_id}, {tenantId}, {tenant_id} +# YINIAN_CONFIG_SYNC_PATH=/config/sync +# YINIAN_SKILLS_MANIFEST_PATH=/skills/manifest +# Optional OpenClaw cloud sync plugin service. Defaults to YINIAN_API_BASE_URL +# with a trailing /ingress removed, then https://onefeel.brother7.cn. +# YINIAN_CLOUD_SYNC_SERVER_URL=https://onefeel.brother7.cn +# YINIAN_CLOUD_SYNC_ENABLED=1 + +# Local demo mode, for visual QA or offline demos only. +# YINIAN_CONTROL_PLANE_MODE=mock + # Release Configuration (CI/CD) # Apple Developer Credentials APPLE_ID=your@email.com diff --git a/design-system/智念助手/MASTER.md b/design-system/智念助手/MASTER.md new file mode 100644 index 0000000..8229329 --- /dev/null +++ b/design-system/智念助手/MASTER.md @@ -0,0 +1,275 @@ +# Design System Master File + +> **LOGIC:** When building a specific page, first check `design-system/智念助手/pages/[page-name].md`. +> If that file exists, its rules **override** this Master file. +> If not, strictly follow the rules below. + +--- + +**Project:** 智念助手 +**Generated:** 2026-04-26 16:21:54 +**Category:** Micro SaaS + +--- + +## Global Rules + +### Color Palette + +| Role | Hex | CSS Variable | +|------|-----|--------------| +| Primary | `#1E3A8A` | `--color-primary` | +| Secondary | `#3B82F6` | `--color-secondary` | +| CTA/Accent | `#CA8A04` | `--color-cta` | +| Background | `#F8FAFC` | `--color-background` | +| Text | `#1E40AF` | `--color-text` | + +**Color Notes:** Luxury navy + gold service + +### Typography + +- **Preferred Implementation:** System font stack first for desktop reliability and China-network/offline friendliness. +- **Heading Font:** `-apple-system`, `BlinkMacSystemFont`, `PingFang SC`, `Microsoft YaHei`, `Segoe UI`, sans-serif +- **Body Font:** `-apple-system`, `BlinkMacSystemFont`, `PingFang SC`, `Microsoft YaHei`, `Segoe UI`, sans-serif +- **Reference Pairing:** Rubik + Nunito Sans can inform proportions, but should not be required at runtime. +- **Mood:** mature B-end operations, precise desktop tooling, calm AI assistant + +**Optional Reference Import:** +```css +@import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@300;400;500;600;700&family=Rubik:wght@300;400;500;600;700&display=swap'); +``` + +### Spacing Variables + +| Token | Value | Usage | +|-------|-------|-------| +| `--space-xs` | `4px` / `0.25rem` | Tight gaps | +| `--space-sm` | `8px` / `0.5rem` | Icon gaps, inline spacing | +| `--space-md` | `16px` / `1rem` | Standard padding | +| `--space-lg` | `24px` / `1.5rem` | Section padding | +| `--space-xl` | `32px` / `2rem` | Large gaps | +| `--space-2xl` | `48px` / `3rem` | Section margins | +| `--space-3xl` | `64px` / `4rem` | Hero padding | + +### Shadow Depths + +| Level | Value | Usage | +|-------|-------|-------| +| `--shadow-sm` | `0 1px 2px rgba(0,0,0,0.05)` | Subtle lift | +| `--shadow-md` | `0 4px 6px rgba(0,0,0,0.1)` | Cards, buttons | +| `--shadow-lg` | `0 10px 15px rgba(0,0,0,0.1)` | Modals, dropdowns | +| `--shadow-xl` | `0 20px 25px rgba(0,0,0,0.15)` | Hero images, featured cards | + +--- + +## Component Specs + +### Buttons + +```css +/* Primary Button */ +.btn-primary { + background: #CA8A04; + color: white; + padding: 12px 24px; + border-radius: 8px; + font-weight: 600; + transition: all 200ms ease; + cursor: pointer; +} + +.btn-primary:hover { + opacity: 0.9; + transform: translateY(-1px); +} + +/* Secondary Button */ +.btn-secondary { + background: transparent; + color: #1E3A8A; + border: 2px solid #1E3A8A; + padding: 12px 24px; + border-radius: 8px; + font-weight: 600; + transition: all 200ms ease; + cursor: pointer; +} +``` + +### Cards + +```css +.card { + background: #F8FAFC; + border-radius: 12px; + padding: 24px; + box-shadow: var(--shadow-md); + transition: all 200ms ease; + cursor: pointer; +} + +.card:hover { + box-shadow: var(--shadow-lg); + transform: translateY(-2px); +} +``` + +### Inputs + +```css +.input { + padding: 12px 16px; + border: 1px solid #E2E8F0; + border-radius: 8px; + font-size: 16px; + transition: border-color 200ms ease; +} + +.input:focus { + border-color: #1E3A8A; + outline: none; + box-shadow: 0 0 0 3px #1E3A8A20; +} +``` + +### Modals + +```css +.modal-overlay { + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); +} + +.modal { + background: white; + border-radius: 16px; + padding: 32px; + box-shadow: var(--shadow-xl); + max-width: 500px; + width: 90%; +} +``` + +--- + +## Style Guidelines + +**Style:** Flat Design + +**Keywords:** 2D, minimalist, bold colors, no shadows, clean lines, simple shapes, typography-focused, modern, icon-heavy + +**Best For:** Web apps, mobile apps, cross-platform, startup MVPs, user-friendly, SaaS, dashboards, corporate + +**Key Effects:** No gradients/shadows, simple hover (color/opacity shift), fast loading, clean transitions (150-200ms ease), minimal icons + +### Product Identity + +- **Product Name:** 智念助手 +- **Desktop App ID:** `app.zhinian.assistant` +- **Primary Mark:** Rounded navy app tile with a blue conversation panel, white intelligence loop, and restrained gold service line. +- **Brand Assets:** + - App icon source: `resources/icons/icon.svg` + - Plain app mark: `resources/icons/icon-plain.svg` + - Tray template source: `resources/icons/tray-icon-template.svg` + - Renderer wordmark: `src/assets/logo.svg` +- **Generated Desktop Assets:** + - `resources/icons/icon.png` + - `resources/icons/icon.icns` + - `resources/icons/icon.ico` + - `resources/icons/16x16.png` through `512x512.png` + - `resources/icons/tray-icon-Template.png` + +### Product Shell Rules + +- Desktop shell must say `智念助手` in customer-facing places. +- Product language should describe the selected tenant as a `工作空间` or `业务对象`, not as a hotel. +- Keep `ClawX` and `OpenClaw` only in technical contexts, developer documentation, compatibility tests, or advanced implementation surfaces. +- Login, Sidebar, Windows title bar, tray menu, installer metadata, and desktop shortcuts should all use the 智念助手 brand. +- The design should feel like a mature operations product, not a marketing landing page. + +### Implemented UI Primitives + +The renderer design kit lives in `src/components/yinian/ui.tsx`. + +- `YinianPageShell` +- `YinianPageHeader` +- `YinianHeaderActions` +- `YinianKicker` +- `YinianPanel` +- `YinianSectionPanel` +- `YinianMetricCard` +- `YinianListItem` +- `YinianInfoRow` +- `YinianNotice` +- `YinianEmptyState` +- `YinianStatusDot` + +Use these before adding page-local card/list/notice styling. New primitives should stay thin and composable over existing React/Tailwind/shadcn foundations. + +### Implemented Page Direction + +- **Login:** enterprise desktop entry with brand narrative, reliability cues, and a focused organization account form. +- **Today:** operations cockpit using section panels, metric cards, task queue rows, and clear skill state labels. +- **Skills Manager:** capability management surface with clear entitlement/local registry/version state separation. +- **Sidebar Service Context:** one account maps to one B-end service for now; show the current service, region, role, and user inside the left sidebar instead of a top switcher. The service card is also the Dashboard/Today entry. +- **Sidebar:** production navigation is intentionally narrow: `快速使用` for Skills/定时任务/知识库, `对话` for New Chat/History, and a footer Settings entry. Do not expose Models, Agents, Channels, or Dev Console in normal customer navigation. +- **Chat History:** history should not be a permanently expanded side list. Use a compact `历史会话` row that opens a hover popover on pointer devices and clicks through to the latest session. +- **Knowledge Base:** knowledge upload is a first-class quick-use tool. The v0 UI may be local-only, but it should present as a service-scoped document library that will later connect to indexing and permissions. +- **Settings:** account actions, service config sync, logout, local preferences, and the "configuration is service-managed" explanation belong in Settings. +- **Service-Managed Configuration:** model/provider strategy, Agent orchestration, notification channels, and low-level scheduling policy are issued by the service side for the current account/service. Local editing controls for these areas should stay disabled or hidden unless an explicit admin/developer mode is added. +- **Inherited Chinese Surfaces:** customer-visible Chinese strings in Chat, Settings, and Setup should say 智念助手; keep ClawX/OpenClaw only where the user is explicitly in technical or compatibility context. +- **Inherited Page Typography:** inherited dashboard pages should not use oversized serif page titles. Use system-font, semibold headings with compact desktop-dashboard scale. +- **Customer-Visible Chinese Copy:** zh locale should avoid ClawX unless describing a technical compatibility or upstream/open-source context. + +### Visual QA Baselines + +The production 智念 flow has a repeatable screenshot smoke test in `tests/e2e/yinian-visual-smoke.spec.ts`. + +- Login baseline: `test-results/yinian-visual/01-login.png` +- Today baseline: `test-results/yinian-visual/02-today.png` +- Skills baseline: `test-results/yinian-visual/03-skills.png` +- Knowledge baseline: `test-results/yinian-visual/04-knowledge.png` +- Settings baseline: `test-results/yinian-visual/05-settings.png` + +Run `pnpm run build:vite` before `pnpm exec playwright test tests/e2e/yinian-visual-smoke.spec.ts` so Electron loads the latest built renderer. + +### Page Pattern + +**Pattern Name:** Product Demo + Features + +- **Conversion Strategy:** Embedded product demo increases engagement. Use interactive mockup if possible. Auto-play video muted. +- **CTA Placement:** Video center + CTA right/bottom +- **Section Order:** 1. Hero, 2. Product video/mockup (center), 3. Feature breakdown per section, 4. Comparison (optional), 5. CTA + +--- + +## Anti-Patterns (Do NOT Use) + +- ❌ Complex onboarding flow +- ❌ Cluttered layout + +### Additional Forbidden Patterns + +- ❌ **Emojis as icons** — Use SVG icons (Heroicons, Lucide, Simple Icons) +- ❌ **Missing cursor:pointer** — All clickable elements must have cursor:pointer +- ❌ **Layout-shifting hovers** — Avoid scale transforms that shift layout +- ❌ **Low contrast text** — Maintain 4.5:1 minimum contrast ratio +- ❌ **Instant state changes** — Always use transitions (150-300ms) +- ❌ **Invisible focus states** — Focus states must be visible for a11y + +--- + +## Pre-Delivery Checklist + +Before delivering any UI code, verify: + +- [ ] No emojis used as icons (use SVG instead) +- [ ] All icons from consistent icon set (Heroicons/Lucide) +- [ ] `cursor-pointer` on all clickable elements +- [ ] Hover states with smooth transitions (150-300ms) +- [ ] Light mode: text contrast 4.5:1 minimum +- [ ] Focus states visible for keyboard navigation +- [ ] `prefers-reduced-motion` respected +- [ ] Responsive: 375px, 768px, 1024px, 1440px +- [ ] No content hidden behind fixed navbars +- [ ] No horizontal scroll on mobile diff --git a/docs/M1_HANDOFF.md b/docs/M1_HANDOFF.md new file mode 100644 index 0000000..7623eea --- /dev/null +++ b/docs/M1_HANDOFF.md @@ -0,0 +1,200 @@ +# YINIAN Desktop M1 Handoff + +> Updated: 2026-04-26 +> Scope: M1 foundation closure after ClawX fork adaptation. See `docs/PILOT_QA.md` for the current M2 pilot demo and QA gate. + +## 1. What M1 Delivers + +M1 turns the ClawX fork into a YINIAN-ready desktop foundation without removing the original ClawX/OpenClaw capabilities. + +Delivered: + +- YINIAN login-first product flow. +- Auth session restore and logout cleanup. +- Hotel tenant context and hotel switching. +- Config snapshot sync from mock or HTTP control plane. +- Local skill registry grouped by `hotelId`. +- Skills sync v0 from manifest to registry. +- Today page as the new production home route. +- Skills page replaced with YINIAN skill control surface. +- Gateway auto-start deferred until YINIAN authentication. +- Legacy ClawX E2E compatibility preserved. +- Workspace packages added for future kernel/skill/UI boundaries. + +Not delivered in M1: + +- Real skill bundle download. +- Signature verification and unpacking. +- Real OTA skill execution. +- Final backend API contract. +- Customer-ready pilot dashboard depth. + +## 2. Main Behavior + +### Production Flow + +1. App starts. +2. Renderer calls `window.yinian.auth.restoreSession()`. +3. If no valid session exists, user is redirected to `/login`. +4. Login succeeds through mock control plane by default, or HTTP mode when configured. +5. App loads config and local skill registry for the current hotel. +6. App navigates to `/today`. +7. Gateway initializes and starts only after authenticated hotel context exists. + +### E2E Compatibility Flow + +When Electron is launched with `CLAWX_E2E=1`, main process appends `e2e=1` to the renderer URL. + +In E2E mode: + +- Legacy setup flow remains available. +- `/` renders the original Chat page, not Today. +- Default language is English unless the test changes it. +- Gateway store is initialized after setup, but gateway is not auto-started. + +This keeps the inherited ClawX regression suite valid while production behavior moves to YINIAN. + +## 3. Environment Switches + +| Variable | Values | Default | Purpose | +|---|---|---:|---| +| `YINIAN_API_BASE_URL` | URL | unset | Enables HTTP control plane. When unset, app uses mock mode. | +| `CLAWX_LEGACY_AUTOSTART` | `1` / unset | unset | Restores old main-process gateway auto-start for debugging. Production YINIAN keeps gateway deferred until login. | +| `CLAWX_E2E` | `1` / unset | unset | Enables E2E compatibility mode. Used by Playwright fixtures. | +| `CLAWX_E2E_SKIP_SETUP` | `1` / unset | unset | Adds `e2eSkipSetup=1` for tests that bypass setup. | + +## 4. Storage Boundary + +YINIAN uses a separate Electron store namespace: + +- Store name: `yinian` +- Helper: `electron/yinian/storage.ts` + +Stored data: + +- `session`: persisted YINIAN session metadata. +- `configs`: config snapshots keyed by hotel id. +- `skillRegistryByHotel`: local skill registries keyed by hotel id. + +Important constraints: + +- Renderer never reads tokens directly. +- Renderer accesses auth/config/skill data only through `window.yinian`. +- Mock mode persists enough session data to restore the local demo. +- HTTP mode keeps `accessToken` in memory and reserves persistence for `refreshToken`. +- Logout clears session, current hotel config, and current hotel skill registry through the control plane. + +## 5. Public Renderer API + +The preload exposes `window.yinian`: + +```ts +window.yinian.auth.restoreSession() +window.yinian.auth.getSessionState() +window.yinian.auth.loginWithSms(input) +window.yinian.auth.loginWithPassword(input) +window.yinian.auth.logout() + +window.yinian.app.getConfig() +window.yinian.app.switchHotel(hotelId) + +window.yinian.skills.sync() +window.yinian.skills.listLocal() +window.yinian.skills.getRegistry(hotelId?) +``` + +Shared types live in `shared/yinian.ts`. + +## 6. Module Inventory + +### Electron Main + +- `electron/main/ipc/yinian.ts` + - IPC handlers for auth, config, hotel switching, skill sync, and registry reads. +- `electron/yinian/control-plane.ts` + - Chooses mock or HTTP control plane. +- `electron/yinian/mock-control-plane.ts` + - Local demo implementation with persisted session/config/registry. +- `electron/yinian/http-control-plane.ts` + - HTTP implementation scaffold for server integration. +- `electron/yinian/storage.ts` + - YINIAN-specific storage namespace. +- `electron/main/index.ts` + - Defers gateway auto-start. + - Adds E2E renderer query parameters. +- `electron/preload/index.ts` + - Exposes `window.yinian`. + +### Renderer + +- `src/stores/yinian.ts` + - Auth/session/config store. + - Restores session on boot. + - Refreshes config and skill registry after login and hotel switch. +- `src/stores/yinian-skills.ts` + - Local registry and skill sync store. +- `src/pages/YinianLogin/` + - Login UI. +- `src/pages/Today/` + - M1 hotel home surface. +- `src/pages/YinianSkills/` + - Skill registry and sync surface. +- `src/components/layout/YinianTenantBar.tsx` + - Current hotel switcher and logout. +- `src/App.tsx` + - YINIAN auth gate, production `/today`, E2E compatibility paths. + +### Workspace Packages + +- `packages/kernel-core` +- `packages/kernel-context` +- `packages/kernel-adapter-openclaw` +- `packages/skill-spec` +- `packages/skills-hotel-core` +- `packages/ui-kit` + +These are M1 boundary scaffolds. They are intentionally thin and should harden during M2/M3. + +### Tests + +- `tests/unit/yinian-control-plane.test.ts` +- `tests/unit/yinian-store.test.ts` +- `tests/unit/yinian-skills-store.test.ts` + +Inherited ClawX unit and E2E tests are still expected to pass. + +## 7. Verification Baseline + +Last known green run: + +```bash +pnpm run typecheck +pnpm run test +pnpm run test:e2e +``` + +Results: + +- Typecheck: passed. +- Unit tests: 89 files, 572 tests passed. +- E2E: 26 passed, 1 skipped. + +Known non-blocking warnings: + +- Vitest `MaxListenersExceededWarning`. +- Vite dynamic/static import chunk warnings. +- Vite large chunk warning. +- Playwright/Electron `NO_COLOR` ignored because `FORCE_COLOR` is set. + +## 8. M2 Entry Points + +Recommended next steps: + +1. Define real server contract v0. +2. Upgrade Today page into a pilot operations cockpit. +3. Expand Skills Manager status model and UI. +4. Start design system consolidation across Login, Today, Skills, and tenant switcher. + +See `task_plan.md` for the active phased plan. + +The first draft of the server contract now lives in `docs/SERVER_CONTRACT_V0.md`. diff --git a/docs/PILOT_QA.md b/docs/PILOT_QA.md new file mode 100644 index 0000000..3990a53 --- /dev/null +++ b/docs/PILOT_QA.md @@ -0,0 +1,256 @@ +# YINIAN Desktop Pilot QA + +> Updated: 2026-04-26 +> Scope: M1/M2 pilot demo package for B-end business desktop flow + +## 1. Pilot Scope + +This pilot validates the desktop product loop that is already implemented: + +- Login-first YINIAN entry. +- Session restore and logout cleanup. +- Single workspace/service context in the sidebar. +- Customer-facing sidebar focused on the service Dashboard card, `快速使用`, `对话`, and `设置`. +- Knowledge Base v0 upload/list/search surface. +- Service-managed model/Agent/channel/schedule configuration notice in Settings. +- Config sync in mock or HTTP mode. +- Today operations cockpit. +- Workspace-scoped local skill registry. +- Skills Manager sync/status flow. +- Gateway start after authenticated workspace context. +- Legacy ClawX/OpenClaw compatibility in E2E mode. + +Out of scope for this pilot: + +- Real skill bundle download. +- Signature verification and unpacking. +- Real OTA/PMS task execution. +- Real payment, provisioning, or account administration. +- Customer-specific branding beyond current YINIAN visual system pass. + +## 2. Environment Modes + +### Mock Mode + +Default mode. Do not set `YINIAN_API_BASE_URL`. + +Use this for product walkthroughs, local QA, and stakeholder demos before the NIANXX server is ready. + +### HTTP Mode + +Set `YINIAN_API_BASE_URL` to enable server-backed control plane calls: + +```bash +YINIAN_API_BASE_URL=https://api.example.com pnpm run dev +``` + +HTTP mode expects the contract documented in `docs/SERVER_CONTRACT_V0.md`. + +### E2E Compatibility Mode + +Playwright uses `CLAWX_E2E=1` through test fixtures. In this mode, legacy ClawX setup/main flows stay available so inherited regression tests remain meaningful. + +## 3. Demo Script + +Use mock mode unless the server is explicitly being validated. + +1. Start the app: + + ```bash + pnpm run dev + ``` + +2. Confirm first screen is YINIAN login. + +3. Login with mock SMS credentials: + + - Phone: `13800000000` + - Code: `123456` + +4. Confirm the app lands on `/today`. + +5. Review Today cockpit: + + - Service name and login user are visible. + - Clicking the sidebar service card returns to Today/Dashboard. + - Application readiness cards render. + - Status queue explains app sync/update/failed states. + - Application status board merges server entitlements and local registry. + +6. Open `/skills`. + +7. Confirm Skills Manager: + + - Entitlement count is visible. + - Local registry count is visible. + - Empty registry warning appears before sync when relevant. + - Each skill explains its current state. + +8. Click `同步 Skills`. + +9. Confirm: + + - Sync completes without leaving the page. + - Skill cards update to installed/skipped/updated states. + - Today page reflects local registry status after returning. + +10. Open `知识库`. + +11. Confirm: + + - Upload entry is visible. + - Empty state is readable before upload. + - Search and document list area are present. + +12. Hover `历史会话`. + +13. Confirm: + + - History popover opens. + - Clicking `历史会话` opens the latest session when history exists, otherwise opens Chat. + +14. Open Settings. + +15. Confirm: + + - `账号与组织` shows the current service and user. + - `同步组织配置` refreshes config for the current service. + - Model, Agent, channel, and scheduling-policy configuration are shown as server-managed capabilities. + - Today and Skills stay scoped to the same workspace context. + +16. Click Settings `退出登录`. + +13. Confirm: + + - App returns to `/login`. + - Current workspace config and local registry context are cleaned for the logged-out session. + +## 4. QA Checklist + +### Auth And Session + +- [ ] Login succeeds in mock mode. +- [ ] App restores a valid mock session after relaunch. +- [ ] Logout returns to `/login`. +- [ ] Logout clears current workspace config and current business skill registry context. +- [ ] HTTP mode restore calls `/auth/refresh` when a persisted refresh token exists. +- [ ] HTTP mode does not persist access tokens. + +### Workspace Context + +- [ ] Sidebar service context appears only after authentication. +- [ ] No workspace switcher is visible in the product shell. +- [ ] Settings sync refreshes config for the current service. +- [ ] Today and Skills stay scoped to the current workspace registry. + +### Sidebar And Settings IA + +- [ ] Production sidebar service card opens Today/Dashboard. +- [ ] Production sidebar quick-use area shows `Skills`, `定时任务`, and `知识库`. +- [ ] Chat area shows `新对话` and `历史会话`. +- [ ] Hovering `历史会话` reveals the history popover. +- [ ] Models, Agents, Channels, and Dev Console are not visible in normal customer navigation. +- [ ] Account logout lives in Settings, not as a primary sidebar action. +- [ ] Settings explains that model/provider strategy, Agent orchestration, channels, and scheduling policy are managed by the service side. + +### Knowledge Base + +- [ ] Knowledge page opens from the sidebar. +- [ ] Upload button opens the system file picker. +- [ ] Uploaded files appear in the local document list. +- [ ] Search filters the visible document list. +- [ ] Empty state distinguishes "no files yet" from "no search matches". + +### Gateway Boundary + +- [ ] Gateway does not start before login in production mode. +- [ ] Gateway starts once after authenticated workspace context exists. +- [ ] `CLAWX_LEGACY_AUTOSTART=1` restores the old ClawX startup behavior for debugging. +- [ ] E2E compatibility mode keeps legacy setup and Chat defaults available. + +### Today + +- [ ] Today renders without config errors after login. +- [ ] Empty states are visible when there are no pending items, skills, or actions. +- [ ] Skill status labels are clear: + - `已开通未同步` + - `已安装` + - `已更新` + - `已是最新` + - `有更新` + - `已禁用` + - `同步失败` +- [ ] Refresh button updates config without triggering duplicate gateway starts. + +### Skills Manager + +- [ ] Empty entitlement state appears when an organization has no opened skills. +- [ ] Empty local registry state is distinct from no entitlements. +- [ ] Sync first install marks skills as installed/updated. +- [ ] Syncing the same manifest again marks same-version skills as skipped. +- [ ] Failed sync surfaces an error and retry affordance. +- [ ] Disabled entitlements cannot run. +- [ ] Version drift is shown as `有更新`. + +### Visual System + +- [ ] Login, Today, Skills, and sidebar feel like one product. +- [ ] Primary actions use the YINIAN navy treatment. +- [ ] Status colors are restrained and readable. +- [ ] Panels use 8px radius or less. +- [ ] Text does not overflow buttons, cards, or status badges at common desktop widths. +- [ ] No marketing-style hero page appears after authentication. +- [ ] Visual smoke screenshots are refreshed for Login, Today, Skills, Knowledge, and Settings. + +## 5. Verification Commands + +Run this full gate before a guided demo: + +```bash +pnpm run typecheck +pnpm run test +pnpm run build:vite +pnpm run test:e2e +``` + +Last known green verification: + +- Typecheck: passed. +- Unit tests: 89 files, 572 tests passed. +- Vite build: passed. +- E2E: 26 passed, 1 skipped. +- Visual smoke: Login, Today, Skills, Knowledge, and Settings passed. + +Known non-blocking warnings: + +- Vitest `MaxListenersExceededWarning`. +- Vite dynamic/static import chunk warnings. +- Vite large chunk warning. +- Playwright/Electron `NO_COLOR` ignored because `FORCE_COLOR` is set. + +## 6. Packaging Notes + +For local macOS pilot packaging, prefer: + +```bash +pnpm run package:mac:local +``` + +This skips preinstalled skills and is more suitable for a fast local artifact. Full release packaging should use the platform-specific `package:*` scripts after dependency and signing decisions are settled. + +Do not treat the current pilot build as production-ready until: + +- Real server auth is validated end to end. +- Real bundle download and signature verification are implemented. +- The installer signing/notarization path is finalized. +- Customer-specific privacy and logging requirements are reviewed. + +## 7. Known Issues And Product Gaps + +- Today uses config and local registry as its data source; real PMS/OTA/task data is not connected yet. +- Skills sync consumes manifest metadata only; it does not download, verify, or execute real bundles. +- Knowledge Base v0 is local UI state only; persistence, indexing, permissions, and service sync are not connected yet. +- HTTP control plane contract is still draft v0. +- Device identity is still a placeholder and should become a stable installation id. +- Legacy ClawX configuration pages remain available for compatibility/E2E routes, but should stay hidden from production customer navigation until an admin/developer mode is explicitly designed. +- Bundle size warnings remain from the inherited app structure. diff --git a/docs/PRD.md b/docs/PRD.md new file mode 100644 index 0000000..a2296b5 --- /dev/null +++ b/docs/PRD.md @@ -0,0 +1,2311 @@ +# 智念桌面端 PRD(YINIAN Hotel Desktop) + +> **版本:** v0.3(完整草案) +> **状态:** 第一至九章与附录已补齐,进入评审与拆票阶段 +> **上次更新:** 2026-04-26 +> **代号:** YINIAN · 内部用 yinian-desktop + +--- + +## 目录与推进议程 + +| 章节 | 主题 | 状态 | +|---|---|---| +| 第一章 | 产品概述 | ✅ 已成稿 | +| 第二章 | 系统架构 | ✅ 已成稿 | +| 第三章 | 领域模型与端口契约(`kernel-core`)| ✅ 已成稿 | +| 第四章 | YINIAN Skill Spec v0.1 | ✅ 已成稿 | +| 第五章 | 客户端模块设计(Main / Renderer 各模块)| ✅ 草案已补齐 | +| 第六章 | 服务端 API 契约 | ✅ 草案已补齐 | +| 第七章 | Skills v1 详细设计 | ✅ 草案已补齐 | +| 第八章 | 非功能性要求(安全 / 性能 / 端口检测 / 离线 / 可观测性)| ✅ 草案已补齐 | +| 第九章 | 里程碑、交付计划与风险登记册 | ✅ 草案已补齐 | +| 附录 A | 术语表 | ✅ 草案已补齐 | +| 附录 B | OpenClaw 上游同步策略 | ✅ 草案已补齐 | +| 附录 C | CI 强制规则清单 | ✅ 草案已补齐 | + +--- + +# 第一章 · 产品概述 + +## 1.1 产品定位 + +**智念桌面端**(YINIAN Hotel Desktop)是面向中国酒店运营场景的 AI Agent 桌面客户端。它把 AI Agent 的能力封装成"装在前台 / 财务 / 运营那台电脑里的工具"——零配置、零学习曲线、登录即用。 + +技术上采用 **壳-核-能力包** 三段式架构: + +- **壳(Shell)** = 桌面 UI + 登录态 + 本地存储 + 通知 +- **核(Kernel)** = Agent runtime(当前 OpenClaw,可替换) +- **能力包(Skills)** = 酒店行业 know-how 编码而成的可执行单元 + +三段之间通过明确的端口契约解耦,确保 **核可以替换、能力包可以跨平台复用、UI 可以独立演进**。 + +## 1.2 目标用户 + +| 角色 | 核心特征 | 与本产品的关系 | +|---|---|---| +| 酒店前台 / 运营 | 普通办公能力、不懂技术、工作高频重复 | 日常使用者,主要交互对象 | +| 酒店店长 / 老板 | 看数据做决策、不会每天打开 | 价值确认者,看每日 / 每周推送 | +| NIANXX 实施 / 客成 | 需要远程开通、远程诊断 | 间接用户,运维入口 | + +## 1.3 核心价值主张 + +**对酒店:** 把"原本需要 1 名运营每天 2-3 小时的 OTA 巡检 / 早报整理 / 客评回复"压缩到 **5 分钟**(看推送 + 处理异常)。 + +**对 NIANXX:** 每个酒店客户的运营 know-how 沉淀为 **可复用、可升级、可跨内核移植** 的 skill 资产,构成长期护城河。Skills 是产品的真正 IP,桌面壳只是当下的最佳载体。 + +## 1.4 与原 ClawX 的本质区别 + +智念桌面端基于 ClawX fork,但产品形态是另一个东西: + +| 维度 | ClawX(上游) | 智念桌面端 | +|---|---|---| +| 用户画像 | 开发者 / 极客 | 酒店运营 / 老板 | +| 配置心智 | 自填 API key + 装 skill | 登录即用、零配置 | +| Skill 来源 | 用户自由浏览安装 | 服务端按客户等级远程下发 | +| Provider / Key | 用户自管 | NIANXX 服务端代理,前端无 key | +| 数据归属 | 全部本地 | 本地优先 + 云端选择性同步 | +| 内核耦合 | UI 直接调 OpenClaw RPC | 通过 Adapter 隔离,可替换 | +| 视觉调性 | 通用 dev tool | 高端 SaaS(白底 + 单一品牌蓝 #1A56DB)| +| 通知通道 | 50+ 全开放 | 继承 OpenClaw 全部通道 + NIANXX 自建渠道 | + +## 1.5 不做什么(防 scope creep) + +- **不做酒店 PMS** —— 不与西软、绿云正面冲突,专注 AI Agent 工具层 +- **不做客户私有部署** —— v1 阶段统一 SaaS 模式 +- **不做开发者 Marketplace** —— skill 由 NIANXX 集中维护与下发 +- **不做手机端** —— 单点突破桌面办公场景,移动端通过企微 / 钉钉接收推送 +- **不做通用对话助手** —— 所有能力围绕"酒店运营任务"组织 +- **不暴露 API Key 配置** —— 所有 LLM 调用走服务端代理,客户端永不持有 key + +--- + +# 第二章 · 系统架构 + +## 2.1 四层洋葱模型 + +``` +┌──────────────────────────────────────────────┐ +│ Vertical Layer · 业务垂直层 │ +│ 酒店 skill / 报表 / 数据回流 │ +├──────────────────────────────────────────────┤ +│ Shell Layer · 桌面壳 / UI │ +│ React 渲染层,零内核依赖 │ +├──────────────────────────────────────────────┤ +│ Adapter Layer · 适配器(关键解耦点) │ +│ 把领域指令翻译成内核 RPC │ +├──────────────────────────────────────────────┤ +│ Kernel Layer · Agent 内核 │ +│ 当前 OpenClaw,可替换 │ +└──────────────────────────────────────────────┘ +``` + +**核心原则:每一层只能依赖更深的层,不能反向依赖。** + +Adapter 是这套架构最关键的一层——它的存在意味着:UI 写成什么样,跟内核换不换没关系;内核换了,UI 不用动。 + +## 2.2 四个核心端口(Port) + +智念桌面端的所有能力围绕四个端口组织。Shell 只与端口对话,端口由 Adapter 提供具体实现。 + +### Port 1 · ConversationPort +对话与消息流。Shell 操作:发消息、订阅消息流、订阅工具调用、订阅 artifact 输出。 + +### Port 2 · SkillPort +技能注册与执行。Shell 操作:列出已开通技能、触发技能、订阅技能执行进度、查看历史执行记录。 + +### Port 3 · SchedulerPort +定时与触发。Shell 操作:增删改查 cron 任务、查看执行历史、临时禁用 / 启用。 + +### Port 4 · NotificationPort +对外通知派发。Shell 操作:发现可用通道、发送通知、订阅送达状态。**采用"通道发现 + 派发"模式**——通道列表由内核动态提供,`kind` 字段为开放字符串,Shell 不写死任何通道枚举。 + +> 详细的 TypeScript 接口签名见 **第三章**。 + +## 2.3 进程模型 + +``` +┌──────────────────────────────────────────────────────────────┐ +│ 智念桌面端 (Electron App) │ +│ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ Electron Main Process │ │ +│ │ • 窗口与生命周期 │ │ +│ │ • Auth 模块(token 管理 / keychain 存储) │ │ +│ │ • Config Sync 模块(与 NIANXX 服务端通信) │ │ +│ │ • Skill Manager(拉取 / 写入 skill bundle) │ │ +│ │ • Kernel Lifecycle(OpenClaw Gateway 子进程管理) │ │ +│ │ • Port Detection(端口冲突检测与处理) │ │ +│ │ • Notification Bridge(系统托盘 + OS 通知) │ │ +│ └─────────────────────┬──────────────────────────────────┘ │ +│ │ IPC (contextBridge) │ +│ ┌─────────────────────▼──────────────────────────────────┐ │ +│ │ Renderer Process (React 19) │ │ +│ │ • 全部 UI │ │ +│ │ • 通过 kernel-context 注入的 Port 访问能力 │ │ +│ │ • 不直接 import OpenClaw 任何 API 或类型 │ │ +│ └────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────┬──────────────────────────┘ + │ WebSocket (JSON-RPC) + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ OpenClaw Gateway(子进程) · 监听 127.0.0.1:18928 │ +│ Agent runtime / Skill 执行 / Cron / Sandbox │ +└──────────────────────────────────┬──────────────────────────┘ + │ HTTPS + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ NIANXX 服务端 │ +│ • Auth(登录 / token 刷新) │ +│ • Config Sync(用户 / 酒店 / agent 配置) │ +│ • Skill Manifest(已开通技能清单 + bundle 下载链接) │ +│ • LLM Proxy(代理 OpenAI / Anthropic 调用 + 计费 + 脱敏) │ +│ • Data Sink(数据回流:执行结果、报表) │ +│ • Custom Notification Channels(自建企微 / 钉钉 / 自定义) │ +└──────────────────────────────────────────────────────────────┘ +``` + +### 关键设计点 + +1. **Gateway 由 Main 拉起子进程**,生命周期由 Main 管理。Main 退出时确保 Gateway 优雅关闭。 +2. **Gateway 端口固定 127.0.0.1:18928**,与上游 ClawX 默认端口(18789)刻意错开,避免与原版 ClawX 共存冲突。Main 启动前执行端口检测协议(详见 §2.7)。 +3. **Renderer 通过 WebSocket 连接 Gateway**(同机回环,性能 OK),连接地址与短期会话凭证由 Main 通过 `contextBridge` 下发,所有调用必须经过 Adapter 包装层。 +4. **所有外部网络通信原则上由 Main 发起**(NIANXX 服务端、第三方 API),Renderer 不直接发出公网请求。 +5. **LLM 调用全部走 NIANXX 服务端代理**:Gateway 配置中的 `OPENAI_BASE_URL` 指向 NIANXX 代理网关,**密钥永远不下发到客户端**。这一条是硬约束,不留客户自带 key 的口子。 + +## 2.4 Monorepo 包结构 + +使用 pnpm workspace: + +``` +yinian-desktop/ +├── apps/ +│ └── desktop/ # 应用本体(原 ClawX 改造而来) +│ ├── electron/ # Main 进程代码 +│ └── src/ # Renderer 代码 +├── packages/ +│ ├── kernel-core/ # 领域类型 + Port 接口(零内核依赖) +│ ├── kernel-adapter-openclaw/ # OpenClaw 适配器 +│ ├── kernel-context/ # 适配器注入与切换的统一入口 +│ ├── skill-spec/ # YINIAN Skill Spec 类型 + 编译器 +│ ├── skills-hotel-core/ # 通用酒店 skill 基类与共享逻辑 +│ ├── skills/ +│ │ ├── ctrip-price-monitor/ +│ │ ├── meituan-price-monitor/ +│ │ ├── daily-report/ +│ │ └── review-reply-helper/ +│ └── ui-kit/ # 设计 tokens + 公共组件 +├── vendor/ +│ └── openclaw/ # OpenClaw submodule +├── docs/ +│ └── PRD.md # 本文档 +└── tools/ + └── eslint-rules/ # 强制依赖方向的自定义规则 +``` + +## 2.5 严格的依赖方向(CI 强制) + +依赖图是单向的,由 ESLint `no-restricted-imports` + 自定义规则在 CI 强制: + +| 包 / 路径 | 允许 import | +|---|---| +| `apps/desktop/src/`(renderer)| `@yinian/kernel-core`、`@yinian/kernel-context`、`@yinian/ui-kit`、`@yinian/skill-spec`;仅 `bootstrap.tsx` 可 import `@yinian/kernel-adapter-openclaw` | +| `apps/desktop/electron/`(main)| 上述 + `@yinian/kernel-adapter-openclaw` | +| `@yinian/kernel-core` | 仅纯工具库(zod、date-fns 等),**禁止** 任何内核相关依赖 | +| `@yinian/kernel-adapter-openclaw` | `@yinian/kernel-core` + OpenClaw SDK | +| `@yinian/kernel-context` | `@yinian/kernel-core` + 由调用方注入的具体 adapter | +| `@yinian/skills/*` | `@yinian/skill-spec` + `@yinian/skills-hotel-core` | + +**违规视为 PR 红灯,不允许合并。** 这条规则比文档约定有效得多——它把"层级纪律"从一个口号变成了 CI 红灯。 + +## 2.6 Renderer 如何拿到 Port 实现:唯一的依赖注入点 + +整个系统中,**只有一个文件** 把"具体内核"和"应用"绑在一起: + +```ts +// apps/desktop/src/bootstrap.tsx +import { createKernelContext } from '@yinian/kernel-context'; +import { createOpenClawAdapter } from '@yinian/kernel-adapter-openclaw'; + +const endpoint = await window.yinian.app.getKernelEndpoint(); + +const kernel = createKernelContext({ + adapter: createOpenClawAdapter({ + wsUrl: endpoint.wsUrl, + token: endpoint.token, + }), +}); + +// 之后整个 React 树通过 useKernel() Hook 拿所有 Port +// 例如:const { conversation } = useKernel(); conversation.send(...) +``` + +**未来更换内核**只需要改这一个文件——把 `createOpenClawAdapter` 换成 `createXxxAdapter`,整个 UI 树不用动。 + +这是端口与适配器模式落地最关键的实操细节。所有"换内核"的承诺,最终都要兑现在这一个文件能不能干净地切换。 + +## 2.7 启动时序与端口检测 + +``` +1. 用户打开应用 +2. Electron Main 启动 +3. Main 检查 keychain 中 token + ├─ 无 / 过期 → 渲染 LoginPage(Gateway 暂不启动) + └─ 有效 → 继续步骤 4 +4. Main 调用 NIANXX `/auth/me` 验证 token +5. Main 拉取 `/config/sync`:用户 + 酒店 + agent 配置 +6. Main 拉取 `/skills/manifest`,比对本地版本 +7. Main 下载 / 更新需要的 skill bundle,写入 OpenClaw skills 目录 +8. Main 执行端口检测协议(127.0.0.1:18928) + ├─ 未占用 → 启动 Gateway + ├─ 占用且握手通过 → 是上次崩溃残留的我们的 Gateway,复用或 kill+重启 + └─ 占用但握手失败 → 进入"端口冲突"失败态 UI,引导用户处理 +9. Main 启动 OpenClaw Gateway 子进程(注入 NIANXX LLM proxy endpoint) +10. Main 健康检查 Gateway(轮询直到 ready) +11. Renderer 通过 kernel-context 建立连接,进入 MainAppPage +``` + +### 端口检测协议 + +为区分"占用 18928 的进程是不是我们自己",定义两个本地端点: + +- `GET http://127.0.0.1:18928/__yinian_probe__`:无需 token,只返回最小服务身份,用于端口占用识别 +- `GET http://127.0.0.1:18928/__yinian_health__`:需要 Main 注入的 `healthToken`,返回详细健康状态 + +probe 响应: + + ```json + { "service": "yinian-kernel-gateway", "version": "x.y.z", "started_at": } + ``` + +- Main 启动时先 GET probe 端点,3 秒超时 + - 200 + 正确 service 字符串 → 是我们的 Gateway,直接复用(或 kill 重启,按用户配置) + - 任意其他响应 / 超时 / 连接拒绝 → 视为"被陌生进程占用" +- Main 启动或复用 Gateway 后,再用 `healthToken` 访问 health 端点确认 ready +- 失败态 UI 提供:识别占用进程(用 `lsof` / `netstat` 风格的系统命令)、一键终止、或选择临时使用 18929 备用端口 + +这个协议成本极低(两个 HTTP 端点 + 几十行检测逻辑),但能解决"用户机器上同时装了原版 ClawX"或"上次没干净退出"两个最常见的体验雷点。 + +## 2.8 此架构需要诚实承认的泄漏点 + +完美的内核无关性不存在。以下两处会有泄漏,提前画好红线: + +### 泄漏点 1:Sandbox / 安全策略 +OpenClaw 的 Docker sandbox 模式如果换到无 sandbox 的内核就没了。短期对策:sandbox 配置作为 Adapter 内部细节,不暴露给 Shell;长期看入风险登记册(第九章)。 + +### 泄漏点 2:流式输出语义差异 +不同内核的 token 流式输出语义会有细微差异(中断恢复点、tool call 内嵌位置、reasoning block 格式等)。Adapter 必须把所有内核的输出归一到统一的 `ConversationStreamEvent` 类型,由 Adapter 吃掉差异。这是 Adapter 实现复杂度最高的部分,也是 Adapter 真正的价值所在。 + +> **关于通知通道**:v0.1 设计阶段曾考虑把通道收口为四类抽象,最终采用"通道发现 + 派发"模式(详见 §3.6)——继承内核全部通道 + 允许 NIANXX 服务端动态注册自建通道。这不算泄漏点,是有意设计。 + +--- + +# 第三章 · 领域模型与端口契约(kernel-core) + +## 3.1 设计原则 + +`kernel-core` 是整个系统中**唯一不知道 OpenClaw 存在**的代码包。任何 PR 试图在 `kernel-core` 里 import OpenClaw、使用 OpenClaw 类型、或硬编码 OpenClaw 的 RPC 方法名,都直接 CI 红灯。 + +这一章定义的所有类型与接口,未来无论换到哪个内核,都不会改变。换内核 = 写一个新的 Adapter 包实现这些接口,仅此而已。 + +设计准则: + +1. **领域语言优先** —— 用"对话"、"技能"、"任务"、"通知",不用 agent / channel / session 等内核术语 +2. **流式优先** —— 所有可能耗时的操作返回 `AsyncIterable`,不返回单个 Promise +3. **不可变快照** —— 跨进程边界的数据全部是 plain object(可被 JSON 序列化),不带类方法 +4. **错误是值** —— 错误以 `KernelError` 类型显式建模,不依赖 throw / catch 跨进程 +5. **零运行时依赖** —— 除 `zod`(schema 验证)、`date-fns`(日期处理)外不允许其他依赖 + +## 3.2 共享核心类型 + +```typescript +// === 标识与身份(Branded Types 防误用) === + +export type UserId = string & { readonly __brand: 'UserId' }; +export type HotelId = string & { readonly __brand: 'HotelId' }; +export type ConversationId = string & { readonly __brand: 'ConversationId' }; +export type MessageId = string & { readonly __brand: 'MessageId' }; +export type SkillId = string & { readonly __brand: 'SkillId' }; +export type TaskId = string & { readonly __brand: 'TaskId' }; +export type ExecutionId = string & { readonly __brand: 'ExecutionId' }; + +// === 用户与酒店 === + +export interface User { + id: UserId; + name: string; + avatar?: string; + email?: string; + phone?: string; + hotels: HotelMembership[]; +} + +export interface HotelMembership { + hotelId: HotelId; + role: 'owner' | 'manager' | 'staff' | 'viewer'; +} + +export interface Hotel { + id: HotelId; + name: string; + brand?: string; + city: string; + ota: HotelOTABinding[]; +} + +export interface HotelOTABinding { + ota: 'ctrip' | 'meituan' | 'booking' | 'agoda' | 'qunar' | string; + externalId: string; // 平台侧的酒店 ID + enabled: boolean; +} + +// === 消息与 Artifact === + +export type MessageRole = 'user' | 'assistant' | 'system' | 'tool'; + +export interface Message { + id: MessageId; + conversationId: ConversationId; + role: MessageRole; + blocks: ContentBlock[]; + createdAt: number; // unix ms +} + +export type ContentBlock = + | TextBlock + | ToolCallBlock + | ToolResultBlock + | ArtifactBlock + | ReasoningBlock; + +export interface TextBlock { type: 'text'; text: string } +export interface ReasoningBlock { type: 'reasoning'; text: string } + +export interface ToolCallBlock { + type: 'tool_call'; + toolCallId: string; + name: string; + input: Record; +} + +export interface ToolResultBlock { + type: 'tool_result'; + toolCallId: string; + status: 'success' | 'failed'; + output: unknown; + error?: KernelError; +} + +export interface ArtifactBlock { type: 'artifact'; artifact: Artifact } + +export type Artifact = + | { kind: 'markdown'; id: string; title?: string; content: string } + | { kind: 'table'; id: string; title?: string; columns: string[]; rows: unknown[][] } + | { kind: 'chart'; id: string; title?: string; spec: unknown } // vega-lite spec + | { kind: 'image'; id: string; title?: string; url: string } + | { kind: 'file'; id: string; title?: string; url: string; mime: string }; + +// === 错误模型 === + +export interface KernelError { + code: KernelErrorCode; + message: string; + retryable: boolean; + cause?: unknown; +} + +export type KernelErrorCode = + | 'kernel_unavailable' + | 'kernel_timeout' + | 'kernel_aborted' + | 'auth_required' + | 'permission_denied' + | 'skill_not_found' + | 'skill_execution_failed' + | 'invalid_input' + | 'rate_limited' + | 'unknown'; +``` + +## 3.3 ConversationPort + +```typescript +export interface ConversationPort { + list(query?: { hotelId?: HotelId; limit?: number }): Promise; + + get(id: ConversationId): Promise; + + create(input: CreateConversationInput): Promise; + + delete(id: ConversationId): Promise; + + /** 发送消息并订阅响应流 */ + send(input: SendMessageInput): AsyncIterable; + + /** 中止正在进行的消息流(流会以 error 事件结束,code = kernel_aborted) */ + abort(conversationId: ConversationId): Promise; + + history( + conversationId: ConversationId, + opts?: { before?: MessageId; limit?: number } + ): Promise; +} + +export interface Conversation { + id: ConversationId; + title: string; + hotelId: HotelId; + createdAt: number; + updatedAt: number; + messageCount: number; +} + +export interface CreateConversationInput { + hotelId: HotelId; + title?: string; +} + +export interface SendMessageInput { + conversationId: ConversationId; + content: + | TextBlock + | { type: 'invoke_skill'; skillId: SkillId; input: Record }; +} + +export type ConversationStreamEvent = + | { type: 'message_start'; message: Pick } + | { type: 'text_delta'; messageId: MessageId; delta: string } + | { type: 'reasoning_delta'; messageId: MessageId; delta: string } + | { type: 'tool_call'; messageId: MessageId; block: ToolCallBlock } + | { type: 'tool_result'; messageId: MessageId; block: ToolResultBlock } + | { type: 'artifact'; messageId: MessageId; artifact: Artifact } + | { type: 'message_complete'; messageId: MessageId; message: Message } + | { type: 'error'; error: KernelError }; +``` + +## 3.4 SkillPort + +```typescript +export interface SkillPort { + list(hotelId: HotelId): Promise; + + get(id: SkillId): Promise; + + invoke(input: InvokeSkillInput): AsyncIterable; + + abort(executionId: ExecutionId): Promise; + + history(skillId: SkillId, opts?: { hotelId?: HotelId; limit?: number }): Promise; + + getExecution(executionId: ExecutionId): Promise; +} + +export interface InstalledSkill { + id: SkillId; + spec: SkillSpec; // 见第四章 + enabled: boolean; + installedAt: number; + lastInvokedAt?: number; +} + +export interface InvokeSkillInput { + skillId: SkillId; + hotelId: HotelId; + input: Record; + triggeredBy: 'user' | 'schedule' | 'webhook' | 'reply'; +} + +export type SkillExecutionEvent = + | { type: 'started'; executionId: ExecutionId; startedAt: number } + | { type: 'progress'; executionId: ExecutionId; phase: string; ratio?: number; message?: string } + | { type: 'log'; executionId: ExecutionId; level: 'info' | 'warn' | 'error'; message: string } + | { type: 'artifact'; executionId: ExecutionId; artifact: Artifact } + | { type: 'completed'; executionId: ExecutionId; output: Record; finishedAt: number } + | { type: 'failed'; executionId: ExecutionId; error: KernelError; finishedAt: number }; + +export interface SkillExecution { + id: ExecutionId; + skillId: SkillId; + hotelId: HotelId; + input: Record; + output?: Record; + artifacts: Artifact[]; + status: 'running' | 'success' | 'failed' | 'aborted'; + triggeredBy: InvokeSkillInput['triggeredBy']; + startedAt: number; + finishedAt?: number; + error?: KernelError; +} +``` + +## 3.5 SchedulerPort + +```typescript +export interface SchedulerPort { + list(hotelId?: HotelId): Promise; + get(id: TaskId): Promise; + create(input: CreateTaskInput): Promise; + update(id: TaskId, patch: UpdateTaskPatch): Promise; + delete(id: TaskId): Promise; + pause(id: TaskId): Promise; + resume(id: TaskId): Promise; + history(id: TaskId, opts?: { limit?: number }): Promise; +} + +export interface ScheduledTask { + id: TaskId; + hotelId: HotelId; + name: string; + cron: string; // 标准 5 字段 cron 表达式 + timezone: string; // IANA 时区,如 'Asia/Shanghai' + target: TaskTarget; + enabled: boolean; + createdAt: number; + updatedAt: number; + lastRunAt?: number; + nextRunAt: number; +} + +export type TaskTarget = + | { kind: 'skill'; skillId: SkillId; input: Record } + | { kind: 'prompt'; conversationId?: ConversationId; prompt: string }; + +export interface CreateTaskInput { + hotelId: HotelId; + name: string; + cron: string; + timezone: string; + target: TaskTarget; +} + +export type UpdateTaskPatch = Partial>; + +export interface TaskExecutionRecord { + id: ExecutionId; + taskId: TaskId; + startedAt: number; + finishedAt?: number; + status: 'running' | 'success' | 'failed' | 'skipped'; + error?: KernelError; + /** 关联的 SkillExecution 或 Conversation,前端可点击跳转 */ + link?: + | { kind: 'skill_execution'; id: ExecutionId } + | { kind: 'conversation'; id: ConversationId }; +} +``` + +## 3.6 NotificationPort(通道发现 + 派发模式) + +```typescript +export interface NotificationPort { + /** 列出当前可用的通知通道(来自内核 + NIANXX 服务端注册) */ + channels(hotelId: HotelId): Promise; + + /** 派发通知到一个或多个通道 */ + send(input: SendNotificationInput): Promise; + + /** 查询发送状态 */ + status(dispatchId: string): Promise; +} + +export interface NotificationChannel { + id: string; + /** 开放字符串而非枚举:'email' | 'sms' | 'wecom' | 'dingtalk' | + * 'whatsapp' | 'telegram' | 'slack' | | ... */ + kind: string; + label: string; // 用户可见名称,如"前台微信群" + recipient: string; // 形态依 kind 而定(邮箱、群 ID、手机号、URL 等) + enabled: boolean; + source: 'kernel' | 'nianxx'; // 区分内核自带还是 NIANXX 服务端注册 + iconUrl?: string; +} + +export interface SendNotificationInput { + hotelId: HotelId; + channelIds: string[]; + content: NotificationContent; +} + +export type NotificationContent = + | { kind: 'text'; text: string } + | { kind: 'markdown'; markdown: string } + | { kind: 'card'; title: string; body: string; actions?: NotificationAction[] }; + +export interface NotificationAction { + label: string; + url?: string; + reply?: string; // 用户点击后回写到对应 conversation +} + +export interface NotificationDispatch { + id: string; + perChannel: { channelId: string; status: 'pending' | 'sent' | 'failed'; error?: KernelError }[]; + createdAt: number; +} +``` + +> **关于通道**:`kind` 是开放字符串而非枚举。Shell 拿到 channel 列表后,根据 `kind` 决定渲染方式(图标、是否需要二次确认等),但**不需要**为每种 kind 写专属代码。OpenClaw 内核自带 50+ 种通道全部继承可用;NIANXX 后续在服务端实现自建通道(如企微群机器人、钉钉自定义机器人),通过 `source: 'nianxx'` 注入到 `channels()` 返回结果中。 + +## 3.7 Adapter 契约 + +每个内核适配器导出一个工厂函数,返回 `Adapter` 实例: + +```typescript +export interface Adapter { + readonly info: { name: string; kernelVersion: string }; + + /** 启动连接(建立 WebSocket 等) */ + connect(): Promise; + disconnect(): Promise; + health(): Promise<{ ready: boolean; details?: Record }>; + + /** 四个 Port 的实现 */ + readonly conversation: ConversationPort; + readonly skill: SkillPort; + readonly scheduler: SchedulerPort; + readonly notification: NotificationPort; +} + +export type AdapterFactory = (config: Config) => Adapter; +``` + +OpenClaw 适配器实现: + +```typescript +// packages/kernel-adapter-openclaw/src/index.ts +export interface OpenClawAdapterConfig { + wsUrl: string; + token: string; + reconnect?: { maxAttempts: number; backoffMs: number }; +} + +export const createOpenClawAdapter: AdapterFactory = (config) => { + // 内部建立 WebSocket,订阅事件,把 OpenClaw RPC 包装为 Port 实现 + // 流式事件做 OpenClaw → ConversationStreamEvent 的归一化映射 + return { + info: { name: 'openclaw', kernelVersion: '...' }, + connect: /* ... */, + disconnect: /* ... */, + health: /* ... */, + conversation: createOpenClawConversationPort(config), + skill: createOpenClawSkillPort(config), + scheduler: createOpenClawSchedulerPort(config), + notification: createOpenClawNotificationPort(config), + }; +}; +``` + +## 3.8 流式语义统一(Adapter 的核心职责) + +不同内核的流式输出(OpenClaw / Anthropic / 自研)在底层格式上有差异,Adapter 必须把它们归一化到 `ConversationStreamEvent`。约定: + +1. 每个 assistant 消息以 `message_start` 开始,以 `message_complete` 结束 +2. `text_delta` 是增量文本,前端做拼接 +3. `tool_call` 一次性给出完整工具调用(不切片下发);`tool_result` 同理 +4. `artifact` 独立事件,不嵌套在 text 中 +5. 任何错误都用 `error` 事件结束流(不抛异常) +6. `abort()` 后内核必须发出 `error` 事件,code = `kernel_aborted` + +这套语义是 Adapter 的核心工作,也是它复杂度最高的部分。把这一层做好,整个 Shell 层就再也不需要关心"我在跟哪个内核说话"。 + +--- + +# 第四章 · YINIAN Skill Spec v0.1 + +## 4.1 设计目标 + +YINIAN Skill 是产品的核心 IP——它把酒店行业的 know-how(OTA 巡检逻辑、价格异常判定规则、客评回复话术等)编码为可执行单元。Skill Spec 决定了这些 IP 资产的形态。 + +设计目标按优先级: + +1. **跨内核可移植** —— Skill 描述层不耦合任何内核的具体 API +2. **声明式优先** —— 元信息、输入输出、权限都用静态描述,能被工具静态校验、自动生成 UI +3. **可组合** —— skill 可以调用 skill,构成工作流 +4. **可版本化** —— 单个 skill 自身有版本,Skill Spec 自身也有版本(v0.1 → v0.2 …) +5. **可远程下发** —— 每个 skill 是一个独立目录,能打包、签名、远程拉取 + +## 4.2 Skill 包目录结构 + +``` +yinian-skill-ctrip-price-monitor/ +├── manifest.yaml # 必须:声明性元数据 +├── README.md # 推荐:人读说明 +├── prompts/ # 推荐:提示词文件(与执行环境无关) +│ ├── system.md +│ └── analysis.md +├── logic/ # 必须:业务逻辑(TS / Python) +│ ├── index.ts # 入口 +│ ├── parsers/ +│ └── rules/ +├── templates/ # 推荐:通知 / 报告模板(Handlebars) +│ ├── daily-report.md.hbs +│ └── anomaly-alert.md.hbs +├── fixtures/ # 推荐:测试用例 +│ └── sample-response.json +├── adapters/ # 必须:适配各内核的编译产物源 +│ ├── openclaw.ts +│ └── (mcp.ts、claude-skill.ts 后续按需添加) +└── tests/ # 推荐:单元 / 集成测试 +``` + +## 4.3 manifest.yaml 完整 schema + +```yaml +spec_version: "0.1" # YINIAN Skill Spec 版本 + +# === 身份 === +id: "ctrip-price-monitor" # 全局唯一 slug +name: "携程酒店价格巡检" +version: "1.0.0" # skill 自身版本,semver +author: "NIANXX" +category: "ota-monitoring" # ota-monitoring | reporting | guest-comm | ops-automation +icon: "ctrip" # ui-kit 已注册的 icon 名,或 url + +# === 描述 === +description: | + 每日巡检指定酒店在携程的房型与报价; + 对异常价格(如低于成本价、相邻日期价差过大)触发告警。 + +# === 输入契约 === +inputs: + - name: hotelId + type: hotel_id # YINIAN 自定义类型,UI 自动渲染酒店选择器 + required: true + label: "酒店" + + - name: dateRange + type: date_range + required: false + label: "巡检日期范围" + default: { kind: "next_n_days", n: 7 } + + - name: priceFloor + type: number + required: false + label: "成本价下限(用于异常判定)" + unit: "CNY" + +# === 输出契约 === +outputs: + - name: snapshot + type: object + schema: # zod-style 子集 + properties: + rooms: + type: array + item: + properties: + roomType: { type: string } + date: { type: string, format: date } + price: { type: number } + available: { type: boolean } + + - name: anomalies + type: array + item: + properties: + roomType: { type: string } + date: { type: string, format: date } + kind: { type: string, enum: [below_floor, large_gap, unavailable] } + details: { type: object } + +# === 触发器 === +triggers: + - kind: manual # 用户手动点击 + - kind: scheduled # 可被 cron 触发 + - kind: webhook # 服务端可远程触发 + - kind: reply # 用户回复某条通知触发 + +# === 运行时能力需求(Adapter 据此分配工具) === +required_capabilities: + - browser # 浏览器自动化 + - http # HTTP 请求 + - llm # LLM 推理(用于异常的语义判定) + +# === 权限 / 沙箱声明 === +permissions: + network: + - "*.ctrip.com" + - "*.ctrip.com.cn" + filesystem: none + shell: false + +# === 通知模板 === +notifications: + on_success: + template: "templates/daily-report.md.hbs" + suggested_channels: ["wecom", "email"] # 渲染建议,用户可改 + on_anomaly: + template: "templates/anomaly-alert.md.hbs" + suggested_channels: ["wecom"] + +# === 执行入口 === +entrypoint: + type: "logic" # logic | prompt-only | composite + path: "./logic/index.ts" + function: "run" + +# === 资源限制 === +limits: + timeout_seconds: 300 + max_llm_tokens: 50000 + max_concurrent: 1 # 同酒店同 skill 不允许并发执行 +``` + +## 4.4 入口函数签名 + +skill 的执行入口是一个标准化函数,签名固定: + +```typescript +// logic/index.ts +import type { SkillRunContext, SkillRunResult } from '@yinian/skill-spec'; + +export async function run( + input: { hotelId: string; dateRange?: DateRange; priceFloor?: number }, + ctx: SkillRunContext +): Promise { + const html = await ctx.browser.fetch('https://hotels.ctrip.com/...'); + const rooms = parseRooms(html); + + const anomalies = await ctx.llm.classify({ + prompt: ctx.prompts.load('analysis.md'), + input: { rooms, priceFloor: input.priceFloor }, + schema: AnomalySchema, + }); + + ctx.emit.artifact({ + kind: 'table', + columns: ['房型', '日期', '价格'], + rows: rooms.map(/* ... */), + }); + ctx.emit.progress({ phase: 'done', ratio: 1 }); + + return { + output: { snapshot: { rooms }, anomalies }, + notifications: anomalies.length + ? [{ template: 'on_anomaly', vars: { anomalies } }] + : [{ template: 'on_success', vars: { rooms } }], + }; +} +``` + +`SkillRunContext` 是 skill 与 Adapter 之间的接口——这是整个 Skill 系统能跨内核移植的关键。Skill 代码只 import `SkillRunContext`,不关心具体内核: + +```typescript +export interface SkillRunContext { + hotel: Hotel; + user: User; + browser: BrowserCapability; + http: HttpCapability; + llm: LlmCapability; + prompts: { load(path: string): string }; + emit: { + progress(e: { phase: string; ratio?: number; message?: string }): void; + log(level: 'info' | 'warn' | 'error', message: string): void; + artifact(a: Artifact): void; + }; + abortSignal: AbortSignal; +} + +export interface BrowserCapability { + fetch(url: string, opts?: { waitFor?: string; userAgent?: string }): Promise; + click(selector: string): Promise; + // ... +} + +export interface LlmCapability { + complete(input: { prompt: string; system?: string; maxTokens?: number }): Promise; + classify(input: { prompt: string; input: unknown; schema: ZodSchema }): Promise; + // ... +} +``` + +每种 capability 由 Adapter 实现并注入。Skill 代码不知道 browser 是 Playwright 还是 Puppeteer 还是 OpenClaw 自带 browser tool。换内核时 capability 的实现换,但 skill 代码不动——这是整个 Skill IP 跨平台变现的基础。 + +## 4.5 Adapter 编译流程 + +每个 skill 的 `adapters/.ts` 文件负责把 Skill 包翻译成目标内核能消费的格式: + +```typescript +// adapters/openclaw.ts +import { defineSkillAdapter } from '@yinian/skill-spec/adapters/openclaw'; + +export default defineSkillAdapter({ + // manifest 字段映射到 OpenClaw skill 配置 + toOpenClawConfig(manifest) { /* ... */ }, + + // 把 entrypoint logic 包装成 OpenClaw skill 可执行形态 + // (注入 ctx → 提供 browser/http/llm 实现) + buildRuntime(manifest, logic) { /* ... */ }, + + // 把 manifest.permissions 翻译成 OpenClaw sandbox 配置 + toSandboxPolicy(permissions) { /* ... */ }, +}); +``` + +构建时(`@yinian/skill-spec/cli`)扫描所有 skill 目录,产出可下发的 bundle: + +``` +ctrip-price-monitor-1.0.0.tgz +├── manifest.yaml +├── manifest.lock # 校验和 +├── logic-bundled.js # 已 bundle 的 JS +├── prompts/ +├── templates/ +└── for-openclaw/ # OpenClaw 适配产物 + └── skill.yaml # OpenClaw 原生格式 +``` + +服务端只下发 bundle 包;客户端 Skill Manager 把它解包到 `vendor/openclaw/skills/` 目录,OpenClaw Gateway 加载即用。未来支持新内核时,bundle 包里多一个 `for-/` 子目录即可。 + +## 4.6 Skill 与四个 Port 的关系 + +| Port | 与 Skill 的关系 | +|---|---| +| ConversationPort | 用户在对话里 `@skill` 触发;返回流中包含 `tool_call` 表示正在调技能 | +| SkillPort | 直接调用入口;提供独立于对话的执行视图(卡片式 UI) | +| SchedulerPort | `target.kind = 'skill'` 即"定时跑技能" | +| NotificationPort | skill 输出的 `notifications` 字段最终由 NotificationPort 投递 | + +四个 Port 都和 Skill 有交集,但 **Skill 自身只依赖 `SkillRunContext`,不直接 import 任何 Port**——这是关键的解耦点。Skill 通过 `ctx.emit.*` 上报状态,由内核+Adapter 把这些事件映射到对应 Port 的事件流。 + +## 4.7 Skill 组合(v0.1 仅声明,不实现) + +v0.1 不要求实现 skill 调 skill,但 manifest 字段结构预留: + +```yaml +# 未来 v0.2:composite skill +entrypoint: + type: "composite" + steps: + - call: ctrip-price-monitor + input: { hotelId: "${input.hotelId}" } + bind: ctripResult + - call: meituan-price-monitor + input: { hotelId: "${input.hotelId}" } + bind: meituanResult + - call: daily-report-writer + input: + ctrip: "${ctripResult.snapshot}" + meituan: "${meituanResult.snapshot}" +``` + +v0.1 阶段 composite 可以在 logic 里手写——下个 spec 版本再做声明式编排。 + +## 4.8 版本演进策略 + +- `spec_version` 遵循 semver,向后兼容字段加 minor,破坏性改动加 major +- `@yinian/skill-spec` 包提供 `migrate(manifest, fromVersion, toVersion)`,老 skill 自动迁移到新 spec +- Adapter 同时支持当前和上一版 spec(N - 1 兼容) +- 客户端运行时校验 `spec_version` 与 Adapter 能力匹配,不匹配则拒绝加载并报错 + +## 4.9 v1 必做的 4 个 Skill(详细设计放第七章) + +| Skill ID | 名称 | 角色 | +|---|---|---| +| `ctrip-price-monitor` | 携程价格巡检 | 标杆 skill,作为模板复制到其他 OTA | +| `meituan-price-monitor` | 美团价格巡检 | 复用同套 logic 框架,仅 parser 不同 | +| `daily-report` | 早间日报生成 | 组合型 skill,调用上述巡检 + LLM 写日报 | +| `review-reply-helper` | 客评回复助手 | 不带 cron,用户对话触发 | + +--- + +# 第五章 · 客户端模块设计(Main / Renderer) + +## 5.1 改造原则 + +智念桌面端基于 ClawX fork,但 v1 阶段不追求一次性把上游完全重构为理想 monorepo。工程策略分两步: + +1. **先保留 ClawX 可运行主体**:窗口、OpenClaw Gateway 生命周期、原有 IPC、构建发布链路先不大拆。 +2. **再建立 YINIAN 边界层**:新增 Auth、Config Sync、Skill Manager、Kernel Adapter、UI Kit 等模块,把酒店产品逻辑逐步迁出原 ClawX 页面与配置模型。 +3. **最后做目录迁移**:等登录、服务端配置、skill 下发、核心 UI 跑通后,再把工程结构迁移到第二章定义的 workspace 形态。 + +这样做的目标是降低 fork 初期风险:第一版产品先跑起来,后续再把架构原则逐步固化为工程纪律。 + +## 5.2 Main 进程模块 + +Main 进程负责所有系统级能力、外部网络通信和敏感状态。Renderer 只拿到受限 API,不直接读取 token、不直接访问服务端、不直接管理 Gateway 子进程。 + +| 模块 | 职责 | 关键输入 | 关键输出 | +|---|---|---|---| +| `AppLifecycle` | 应用启动、退出、单实例锁、窗口恢复 | Electron lifecycle | 主窗口、托盘、退出钩子 | +| `AuthManager` | 登录、刷新 token、退出、keychain 存储 | 手机号/验证码、账号密码、refresh token | `AuthSession` | +| `DeviceManager` | 设备注册、设备 ID、设备撤销 | 本机信息、登录态 | `device_id`、设备绑定状态 | +| `TenantContext` | 当前用户、酒店、角色、权限 | `/auth/me`、`/config/sync` | 当前酒店上下文 | +| `ConfigSync` | 拉取服务端配置、合并本地配置 | token、hotelId、appVersion | `ClientConfigSnapshot` | +| `SkillManager` | 拉取 manifest、下载 bundle、验签、解包、回滚 | `/skills/manifest` | 本地 skill registry | +| `KernelLifecycle` | 启动、健康检查、停止 OpenClaw Gateway | config、skill 目录、端口 | Gateway ready 状态 | +| `PortDetection` | 检测 18928 端口占用与握手 | 端口、health endpoint | 可用端口或失败态 | +| `NotificationBridge` | 系统通知、托盘提醒、点击回调 | skill 执行结果、服务端推送 | OS 通知、应用内事件 | +| `UpdateManager` | 客户端更新检查与灰度 | appVersion、channel | 更新提示或静默更新 | +| `Diagnostics` | 日志打包、健康检查、远程诊断 | 日志、配置快照、Gateway 状态 | 诊断报告 | +| `Telemetry` | 事件上报、错误上报、性能采样 | 匿名事件、错误 | `/events/ingest` | + +### 5.2.1 AuthManager + +AuthManager 是唯一可以读写 token 的模块。token 存储规则: + +- `access_token` 只保存在内存,应用重启后通过 `refresh_token` 换取 +- `refresh_token` 存入系统 keychain,不落普通文件 +- Renderer 永远拿不到 token 字符串,只能调用 `window.yinian.auth.*` +- 退出登录时清理 token、本地租户缓存、Gateway 会话凭证 + +登录方式 v1 支持两种: + +| 方式 | 用途 | +|---|---| +| 手机号 + 验证码 | 酒店员工主路径 | +| 账号 + 密码 | NIANXX 内部实施、客成和管理员 | + +后续可扩展企业微信扫码、钉钉扫码,但 v1 不做。 + +### 5.2.2 ConfigSync + +ConfigSync 拉取服务端控制面配置,并生成客户端可消费的快照: + +```typescript +export interface ClientConfigSnapshot { + user: User; + hotels: Hotel[]; + currentHotelId: HotelId; + entitlements: SkillEntitlement[]; + llmProxy: { + baseUrl: string; + modelPolicyId: string; + }; + notificationChannels: NotificationChannel[]; + featureFlags: Record; + updatedAt: number; +} +``` + +合并规则: + +- 服务端配置优先级高于本地默认配置 +- 用户本地 UI 偏好可以覆盖服务端默认值,例如侧栏折叠、主题密度 +- skill entitlement、LLM proxy、通知通道、权限必须以服务端为准 +- 配置快照写入本地加密存储,离线时可只读使用 + +### 5.2.3 SkillManager + +SkillManager 是远程下发能力包的安全边界。启动流程: + +1. 请求 `/skills/manifest?hotel_id=...` +2. 对比本地 `skill_registry.json` +3. 下载新增或升级的 bundle +4. 校验服务端签名、公钥指纹、bundle hash、manifest hash +5. 解包到 staging 目录 +6. 调用 skill-spec 校验 schema 与权限 +7. 原子替换到 active 目录 +8. 更新本地 registry +9. 通知 KernelLifecycle 重载 Gateway 或延迟到下次启动 + +本地目录建议: + +``` +~/Library/Application Support/YINIAN/ +├── config/ +├── skills/ +│ ├── active/ +│ ├── staging/ +│ └── rollback/ +├── kernel/ +├── logs/ +└── diagnostics/ +``` + +安全规则: + +- 未签名 bundle 拒绝加载 +- 签名通过但权限超出 manifest 声明,拒绝加载 +- 同一 hotel 同一 skill 只保留当前版本和上一版本 +- 服务端可下发 `disabled: true` 作为 kill switch +- bundle 不允许覆盖客户端任意路径,只能解包到 skill sandbox 目录 + +### 5.2.4 KernelLifecycle + +KernelLifecycle 负责把 OpenClaw Gateway 作为受控子进程启动。启动参数由 Main 生成: + +```typescript +export interface KernelLaunchConfig { + port: number; + healthToken: string; + wsSessionToken: string; + skillDir: string; + llmProxyBaseUrl: string; + modelPolicyId: string; + logDir: string; +} +``` + +关键要求: + +- Gateway 只监听 `127.0.0.1` +- health endpoint 校验 `healthToken` +- WebSocket 握手必须携带 `wsSessionToken` +- `OPENAI_BASE_URL` 指向 NIANXX LLM Proxy +- 客户端不注入任何第三方模型 API key +- Main 退出时发送优雅关闭信号,超时后再强制结束进程 + +### 5.2.5 IPC 与 preload 契约 + +Renderer 通过 `contextBridge` 使用受限 API: + +```typescript +declare global { + interface Window { + yinian: { + auth: { + getSessionState(): Promise; + loginWithSms(input: LoginWithSmsInput): Promise; + loginWithPassword(input: LoginWithPasswordInput): Promise; + logout(): Promise; + }; + app: { + getConfig(): Promise; + switchHotel(hotelId: HotelId): Promise; + getKernelEndpoint(): Promise<{ wsUrl: string; token: string }>; + openDiagnostics(): Promise; + }; + skills: { + sync(): Promise; + listLocal(): Promise; + }; + diagnostics: { + getHealth(): Promise; + exportBundle(): Promise<{ path: string }>; + }; + }; + } +} +``` + +禁止事项: + +- 禁止 Renderer 读取 keychain +- 禁止 Renderer 直接调用 NIANXX 服务端 +- 禁止 Renderer 直接 spawn 子进程 +- 禁止 Renderer 直接读写 skill bundle 目录 + +## 5.3 Renderer 信息架构 + +智念不是通用聊天工具,第一屏应是酒店运营工作台。推荐主导航: + +| 页面 | 目标用户 | 主要任务 | +|---|---|---| +| 今日 | 前台、运营、店长 | 看异常、看日报、处理待确认事项 | +| 对话 | 运营、实施 | 临时询问、追问结果、自然语言触发任务 | +| Skills | 运营、实施 | 查看已开通能力、手动运行、查看说明 | +| 自动任务 | 运营、店长 | 管理早报、巡检、定时提醒 | +| 报告 | 店长、老板 | 查看日报、周报、异常归档 | +| 通知 | 运营、实施 | 管理企微、钉钉、邮箱等通道 | +| 设置 | 实施、管理员 | 酒店信息、账号、诊断、版本 | + +### 5.3.1 今日页 + +今日页是默认首页,解决“打开软件后我该看什么”的问题。 + +核心模块: + +- 顶部酒店切换器:显示当前酒店、城市、账号角色 +- 今日摘要:已完成任务、异常数量、待确认数量 +- 待处理队列:价格异常、任务失败、通知未送达、客评待回复 +- 早报卡片:展示当天日报摘要,支持展开完整报告 +- 快捷运行:携程巡检、美团巡检、生成早报、客评回复 +- 最近执行:显示任务状态、耗时、是否有 artifact + +状态设计: + +| 状态 | UI 行为 | +|---|---| +| 无任务 | 显示空态和推荐开通的首个自动任务 | +| 正在同步 | 顶部轻量进度,不阻塞查看旧数据 | +| 有异常 | 待处理队列置顶,使用明确优先级 | +| 离线 | 显示本地缓存结果,禁用需要联网的操作 | + +### 5.3.2 对话页 + +对话页保留 Agent 的灵活性,但交互要比 ClawX 更业务化: + +- 左侧是会话列表,默认按酒店分组 +- 中间是消息流 +- 右侧是上下文面板,展示当前酒店、可用 skills、最近 artifacts +- 输入框支持 `@skill`,但普通用户不需要理解技术名称 +- 工具调用以业务步骤展示,例如“正在读取携程价格”而不是裸 tool name +- artifact 可固定到报告页或导出 + +### 5.3.3 Skills 页 + +Skills 页只展示服务端已开通能力,不做开放 Marketplace。 + +每个 skill 卡片包含: + +- 名称、图标、简介 +- 开通状态、版本、最近运行时间 +- 支持触发方式:手动、定时、通知回复 +- 需要的权限摘要 +- 手动运行按钮 +- 历史执行入口 + +实施人员可以看到更多诊断信息: + +- bundle 版本 +- manifest hash +- 最近失败原因 +- 本地路径 +- 重新同步按钮 + +### 5.3.4 自动任务页 + +自动任务页围绕 SchedulerPort 构建。 + +功能: + +- 创建定时任务 +- 暂停/恢复任务 +- 修改 cron、人类可读时间、时区 +- 查看上次运行与下次运行 +- 查看执行历史 +- 失败重试策略配置 + +酒店用户看到的是“每天 08:30 生成早报”,不是 cron 表达式。cron 只在高级模式展示。 + +### 5.3.5 通知页 + +通知页管理 NotificationPort 返回的通道。 + +v1 支持: + +- 系统桌面通知 +- 企业微信群机器人 +- 钉钉群机器人 +- 邮箱 + +通道来源分两类: + +- `kernel`:OpenClaw 内核继承通道 +- `nianxx`:NIANXX 服务端托管通道 + +普通用户只能启停和测试通道;实施人员可以配置 webhook、群机器人 token 等敏感参数。敏感参数写服务端,不写客户端。 + +## 5.4 视觉与交互原则 + +设计目标:高端、克制、清楚、低学习成本。 + +原则: + +- 主色使用品牌蓝 `#1A56DB`,但界面不做单一蓝色铺满 +- 页面以白底和浅灰分区为主,少用大面积渐变 +- 卡片只用于任务、skill、报告等重复实体 +- 侧栏固定,主工作区稳定,避免页面跳动 +- 状态颜色统一:成功绿、警告橙、失败红、处理中蓝灰 +- 所有长耗时任务必须有进度、可取消、可查看详情 +- 技术术语默认隐藏,例如 provider、API key、RPC、sandbox +- 失败文案给出可执行下一步,例如“重新同步 skill”或“联系实施人员” + +关键组件: + +| 组件 | 用途 | +|---|---| +| `HotelSwitcher` | 当前租户上下文 | +| `TaskStatusBadge` | 任务状态 | +| `SkillCard` | skill 概览与入口 | +| `ArtifactViewer` | 表格、图表、Markdown、文件预览 | +| `ExecutionTimeline` | skill 执行步骤 | +| `NotificationChannelRow` | 通知通道管理 | +| `DiagnosticsPanel` | 健康检查与日志导出 | + +## 5.5 首次启动与失败态 + +首次启动流程: + +1. 打开应用 +2. 显示登录页 +3. 登录成功后选择酒店 +4. 同步配置与 skills +5. 启动 Gateway +6. 进入今日页 + +必须设计的失败态: + +| 失败态 | 处理 | +|---|---| +| 无网络 | 允许登录页重试;已登录用户进入离线只读 | +| token 过期 | 静默刷新,失败后回登录 | +| 未绑定酒店 | 显示“联系管理员开通” | +| skill 同步失败 | 进入主界面,但禁用相关 skill 并显示重试 | +| Gateway 启动失败 | 显示诊断页和导出日志 | +| 端口冲突 | 显示占用进程、切换备用端口、重试 | +| LLM Proxy 不可用 | 对话和需要 LLM 的 skill 禁用,其他本地功能可用 | + +# 第六章 · 服务端 API 契约 + +## 6.1 设计原则 + +NIANXX 服务端是智念桌面端的控制面。客户端不持有模型密钥,不自行决定 skill entitlement,不直接保存第三方敏感凭证。 + +服务端职责: + +- 认证与设备管理 +- 用户、酒店、角色、权限 +- 客户套餐和 skill 开通 +- skill manifest 与 bundle 下载 +- LLM Proxy、计费、限流、脱敏 +- 通知通道托管 +- 执行结果、日志、报表回流 +- 灰度、功能开关、版本策略 + +## 6.2 通用约定 + +基础路径: + +```text +https://api.nianxx.com/yinian/v1 +``` + +请求头: + +```http +Authorization: Bearer +X-YINIAN-App-Version: 0.1.0 +X-YINIAN-Device-Id: +X-YINIAN-Request-Id: +X-YINIAN-Hotel-Id: +``` + +错误响应: + +```json +{ + "error": { + "code": "permission_denied", + "message": "当前账号无权操作该酒店", + "retryable": false, + "request_id": "req_xxx" + } +} +``` + +错误码与 `KernelErrorCode` 尽量对齐,但服务端可扩展: + +| code | 含义 | +|---|---| +| `auth_required` | 需要登录 | +| `token_expired` | token 过期 | +| `permission_denied` | 权限不足 | +| `hotel_not_found` | 酒店不存在或未绑定 | +| `skill_not_entitled` | 未开通 skill | +| `skill_bundle_invalid` | skill 包无效 | +| `rate_limited` | 触发限流 | +| `llm_proxy_unavailable` | 模型代理不可用 | +| `invalid_input` | 请求参数错误 | +| `server_error` | 服务端异常 | + +## 6.3 Auth API + +### 6.3.1 发送短信验证码 + +```http +POST /auth/sms/send +``` + +```json +{ + "phone": "13800000000", + "purpose": "login" +} +``` + +### 6.3.2 手机号登录 + +```http +POST /auth/login/sms +``` + +```json +{ + "phone": "13800000000", + "code": "123456", + "device": { + "device_id": "dev_xxx", + "platform": "darwin", + "app_version": "0.1.0", + "machine_name": "FrontDesk-Mac" + } +} +``` + +响应: + +```json +{ + "access_token": "eyJ...", + "refresh_token": "rt_xxx", + "expires_in": 3600, + "user": { + "id": "user_001", + "name": "王店长", + "phone": "13800000000" + } +} +``` + +### 6.3.3 账号密码登录 + +```http +POST /auth/login/password +``` + +用于 NIANXX 内部账号和部分管理员账号。 + +### 6.3.4 刷新 token + +```http +POST /auth/refresh +``` + +```json +{ + "refresh_token": "rt_xxx", + "device_id": "dev_xxx" +} +``` + +### 6.3.5 当前用户 + +```http +GET /auth/me +``` + +返回用户、可访问酒店、角色、权限: + +```json +{ + "user": { "id": "user_001", "name": "王店长", "phone": "13800000000" }, + "hotels": [ + { + "id": "hotel_001", + "name": "智念杭州西湖店", + "city": "杭州", + "role": "manager", + "permissions": ["skill:run", "task:manage", "report:view"] + } + ] +} +``` + +## 6.4 Config API + +### 6.4.1 配置同步 + +```http +GET /config/sync?hotel_id=hotel_001 +``` + +响应: + +```json +{ + "server_time": 1777190400000, + "hotel": { + "id": "hotel_001", + "name": "智念杭州西湖店", + "brand": "智念", + "city": "杭州", + "ota": [ + { "ota": "ctrip", "externalId": "ctrip_123", "enabled": true }, + { "ota": "meituan", "externalId": "mt_456", "enabled": true } + ] + }, + "feature_flags": { + "review_reply_helper": true, + "auto_update_skills": true + }, + "model_policy": { + "id": "policy_standard_cn", + "allowed_models": ["default"], + "max_tokens_per_day": 1000000 + }, + "ui_policy": { + "default_page": "today", + "show_advanced_settings": false + } +} +``` + +## 6.5 Skill API + +### 6.5.1 Skill manifest + +```http +GET /skills/manifest?hotel_id=hotel_001&client_version=0.1.0 +``` + +响应: + +```json +{ + "manifest_version": "2026-04-26.1", + "public_key_id": "yinian-skill-signing-2026", + "skills": [ + { + "id": "ctrip-price-monitor", + "version": "1.0.0", + "enabled": true, + "entitlement": "standard", + "bundle_url": "https://cdn.nianxx.com/skills/ctrip-price-monitor-1.0.0.tgz", + "bundle_sha256": "abc123...", + "manifest_sha256": "def456...", + "signature": "base64...", + "required_client_version": ">=0.1.0", + "kill_switch": false + } + ] +} +``` + +### 6.5.2 Bundle 下载 + +```http +GET /skills/bundles/{skill_id}/{version} +``` + +实际生产可返回短期 CDN 签名 URL。客户端必须校验 hash 和签名,不信任 CDN 本身。 + +### 6.5.3 Skill 执行结果回流 + +```http +POST /skills/executions +``` + +```json +{ + "execution_id": "exec_001", + "hotel_id": "hotel_001", + "skill_id": "ctrip-price-monitor", + "version": "1.0.0", + "status": "success", + "started_at": 1777190400000, + "finished_at": 1777190460000, + "duration_ms": 60000, + "summary": { + "rooms_checked": 32, + "anomalies": 2 + }, + "artifacts": [ + { "kind": "table", "id": "artifact_001", "title": "价格快照" } + ] +} +``` + +## 6.6 LLM Proxy API + +Gateway 的模型请求统一走 NIANXX LLM Proxy。 + +```http +POST /llm/v1/chat/completions +``` + +约束: + +- 请求必须携带客户端 access token 或由 Gateway 换取的短期 proxy token +- 服务端根据 hotel、user、skill、model_policy 做鉴权 +- 服务端负责选择真实模型供应商 +- 服务端负责限流、计费、日志脱敏 +- 客户端不出现 OpenAI、Anthropic 等供应商 API key + +建议增加 Gateway 专用 token: + +```http +POST /llm/proxy-token +``` + +```json +{ + "hotel_id": "hotel_001", + "device_id": "dev_xxx", + "kernel_session_id": "ks_xxx" +} +``` + +响应: + +```json +{ + "proxy_token": "kpt_xxx", + "expires_in": 1800, + "base_url": "https://api.nianxx.com/yinian/v1/llm/v1" +} +``` + +## 6.7 Notification API + +### 6.7.1 查询通道 + +```http +GET /notifications/channels?hotel_id=hotel_001 +``` + +```json +{ + "channels": [ + { + "id": "ch_wecom_001", + "kind": "wecom", + "label": "前台运营群", + "recipient": "杭州西湖店运营群", + "enabled": true, + "source": "nianxx" + } + ] +} +``` + +### 6.7.2 测试通道 + +```http +POST /notifications/channels/{channel_id}/test +``` + +### 6.7.3 派发通知 + +```http +POST /notifications/dispatch +``` + +```json +{ + "hotel_id": "hotel_001", + "channel_ids": ["ch_wecom_001"], + "content": { + "kind": "markdown", + "markdown": "今日价格巡检发现 2 个异常" + }, + "source": { + "kind": "skill_execution", + "id": "exec_001" + } +} +``` + +## 6.8 Data Sink 与事件上报 + +客户端上报分三类: + +| 类型 | Endpoint | 是否含业务数据 | +|---|---|---| +| 产品行为事件 | `POST /events/ingest` | 否,默认匿名或弱标识 | +| skill 执行结果 | `POST /skills/executions` | 是,需按租户隔离 | +| 诊断日志 | `POST /diagnostics/upload` | 可能含敏感信息,上传前脱敏 | + +行为事件示例: + +```json +{ + "events": [ + { + "name": "skill_run_clicked", + "time": 1777190400000, + "hotel_id": "hotel_001", + "properties": { + "skill_id": "ctrip-price-monitor", + "entry": "today_page" + } + } + ] +} +``` + +## 6.9 权限模型 + +角色初版: + +| 角色 | 权限 | +|---|---| +| `owner` | 查看全部报告、管理通知、管理自动任务、运行 skills | +| `manager` | 查看报告、管理自动任务、运行 skills | +| `staff` | 查看今日页、运行部分 skills、处理待办 | +| `viewer` | 只读查看报告和历史 | +| `nianxx_admin` | 跨酒店实施、诊断、配置 entitlement | + +权限粒度: + +```text +hotel:view +report:view +skill:view +skill:run +skill:manage +task:view +task:manage +notification:view +notification:manage +diagnostics:export +``` + +# 第七章 · Skills v1 详细设计 + +## 7.1 v1 Skill 设计原则 + +v1 不追求覆盖所有酒店场景,只做能快速证明价值的四个 skill: + +1. 价格巡检:直接节省运营时间 +2. 早间日报:每天固定触达店长和老板 +3. 客评回复:体现 AI 生成质量 +4. 通知闭环:把结果送到用户已经在用的企微/钉钉 + +数据获取原则: + +- 优先使用客户授权的官方 API 或服务端集成 +- 无官方 API 时,浏览器自动化作为 v1 可用路径 +- 涉及 OTA 账号密码时,优先服务端托管,不写入客户端 +- skill 输出必须可解释,不能只给一句模型结论 + +## 7.2 `ctrip-price-monitor` + +目标:每日巡检指定酒店在携程的房型、日期、价格、可售状态,识别异常并通知运营人员。 + +输入: + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `hotelId` | `hotel_id` | 是 | 当前酒店 | +| `dateRange` | `date_range` | 否 | 默认未来 7 天 | +| `priceFloor` | `number` | 否 | 成本价或底价 | +| `roomTypes` | `string[]` | 否 | 指定房型 | + +输出: + +- `snapshot`:价格快照 +- `anomalies`:异常列表 +- `summary`:房型数、日期数、异常数 + +异常规则: + +| 异常 | 规则 | +|---|---| +| `below_floor` | 价格低于成本价或配置底价 | +| `large_gap` | 相邻日期价差超过配置阈值 | +| `unavailable` | 应售房型不可售 | +| `missing_room` | 页面未找到已配置房型 | +| `parse_suspect` | 解析置信度过低,需要人工确认 | + +执行步骤: + +1. 读取酒店 OTA 绑定 +2. 打开携程酒店页面或调用服务端数据接口 +3. 抓取未来 N 天房型价格 +4. 标准化房型名称 +5. 执行规则判定 +6. 必要时调用 LLM 对异常描述做自然语言解释 +7. 生成 table artifact 和 markdown 摘要 +8. 有异常则通知指定通道 + +验收标准: + +- 能稳定输出未来 7 天价格快照 +- 同一酒店同一 skill 不并发执行 +- 页面结构变化时返回 `parse_suspect`,不输出误导性结论 +- 失败日志能定位到抓取、解析、规则、通知哪个阶段 + +## 7.3 `meituan-price-monitor` + +目标与携程巡检一致,但适配美团/大众点评酒店页面或服务端数据源。 + +复用策略: + +- 复用 `skills-hotel-core` 中的价格标准化、异常规则、报告模板 +- 独立维护 `meituan` parser +- 与携程输出结构保持一致,方便日报 skill 聚合 + +新增关注点: + +- 美团房型命名与携程不完全一致,需要房型映射表 +- 同一房型可能出现多个套餐,需要选择代表价格或列出套餐差异 +- 可售状态、取消政策、早餐信息可作为 v1.1 扩展字段 + +验收标准: + +- 输出 schema 与 `ctrip-price-monitor` 兼容 +- 可与携程结果在 `daily-report` 中合并 +- 支持房型映射配置 + +## 7.4 `daily-report` + +目标:每天早上生成酒店运营日报,汇总 OTA 价格巡检、异常、昨日任务执行和建议动作。 + +输入: + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `hotelId` | `hotel_id` | 是 | 当前酒店 | +| `date` | `date` | 否 | 默认今天 | +| `includeOta` | `string[]` | 否 | 默认已启用 OTA | +| `sendToChannels` | `string[]` | 否 | 默认服务端配置 | + +执行步骤: + +1. 调用或读取携程巡检结果 +2. 调用或读取美团巡检结果 +3. 汇总异常与趋势 +4. 生成店长可读的 Markdown 日报 +5. 输出 report artifact +6. 按配置发送企微/钉钉/邮箱 + +日报结构: + +```markdown +# 今日运营早报 + +## 重点结论 + +## OTA 价格概览 + +## 异常与建议 + +## 今日建议动作 + +## 明细表 +``` + +验收标准: + +- 生成内容不超过 2 屏手机阅读长度 +- 明细可在桌面端展开查看 +- LLM 生成内容必须基于结构化输入,不允许凭空编造 +- 没有异常时也要生成“已完成巡检”的正向反馈 + +## 7.5 `review-reply-helper` + +目标:根据客人评价内容、酒店品牌语气、问题类型,生成可编辑的回复建议。 + +输入: + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `hotelId` | `hotel_id` | 是 | 当前酒店 | +| `reviewText` | `string` | 是 | 客评原文 | +| `rating` | `number` | 否 | 评分 | +| `platform` | `string` | 否 | 携程/美团/大众点评等 | +| `tone` | `string` | 否 | 默认酒店品牌语气 | + +输出: + +- `reply`:建议回复 +- `issueTags`:问题标签 +- `riskLevel`:是否需要人工升级 +- `alternatives`:可选回复版本 + +规则: + +- 差评默认不自动发送,只生成建议 +- 涉及安全、卫生、赔偿、隐私、投诉升级,标记 `high` +- 回复必须避免承诺未确认的补偿 +- 回复中不得泄露内部操作信息 + +验收标准: + +- 生成回复可直接复制 +- 高风险客评必须提示人工处理 +- 同一评价可生成“正式/温和/简短”三种版本 + +## 7.6 共享包 `skills-hotel-core` + +共享能力: + +- OTA 房型标准化 +- 价格异常规则 +- 日期与时区处理 +- 酒店品牌语气配置 +- 通知模板渲染 +- artifact 生成工具 +- 执行日志辅助函数 + +建议目录: + +``` +packages/skills-hotel-core/ +├── src/ +│ ├── ota/ +│ ├── pricing/ +│ ├── report/ +│ ├── review/ +│ └── templates/ +└── tests/ +``` + +## 7.7 Skill 测试要求 + +每个 v1 skill 至少包含: + +- manifest schema 测试 +- 输入校验测试 +- parser fixture 测试 +- 异常规则单元测试 +- artifact snapshot 测试 +- 失败态测试 +- bundle 构建测试 + +价格巡检类 skill 额外要求: + +- 页面结构字段缺失时不崩溃 +- 空房型、空价格、重复房型可处理 +- 时区固定为酒店所在地 + +# 第八章 · 非功能性要求 + +## 8.1 安全 + +安全是 v1 硬门槛,不是上线后的增强项。 + +### 8.1.1 Token 与密钥 + +- 客户端永不保存第三方模型 API key +- `refresh_token` 存系统 keychain +- `access_token` 只在内存保存 +- Gateway 使用短期 `proxy_token` 调用 LLM Proxy +- token 刷新失败立即降级到登录态 + +### 8.1.2 Gateway 本地访问控制 + +- Gateway 只监听 `127.0.0.1` +- health endpoint 需要 Main 注入的 `healthToken` +- WebSocket 连接需要短期 `wsSessionToken` +- token 随应用启动生成,退出即失效 +- Renderer 只能通过 Main 获取 endpoint 和 token + +### 8.1.3 Skill Bundle 安全 + +- 所有 bundle 必须签名 +- 客户端内置 NIANXX skill 公钥指纹 +- 下载后校验 sha256 和签名 +- manifest 权限与实际 bundle 行为不一致时拒绝加载 +- 支持服务端 kill switch +- 支持回滚到上一版本 + +### 8.1.4 权限与沙箱 + +- skill 权限最小化 +- 默认禁止 shell +- 默认禁止任意文件系统访问 +- 网络权限按域名白名单声明 +- 浏览器能力必须受限于目标域 +- 高风险 capability 需要服务端 entitlement 开启 + +### 8.1.5 数据脱敏 + +日志中默认脱敏: + +- 手机号 +- 邮箱 +- token +- webhook URL +- OTA 账号 +- 客评中的手机号、身份证号等个人信息 + +诊断包上传前必须二次脱敏,并显示将上传的内容摘要。 + +## 8.2 性能 + +启动目标: + +| 指标 | 目标 | +|---|---| +| 冷启动到登录页 | < 3 秒 | +| 已登录启动到今日页可见 | < 6 秒 | +| Gateway ready | < 15 秒 | +| skill manifest 同步 | < 5 秒,网络正常时 | +| 今日页首次渲染 | < 2 秒,使用本地缓存 | + +运行目标: + +- 对话 token 流不卡 UI +- 大 artifact 分页或虚拟滚动 +- 单个 skill 执行不阻塞其他 UI 操作 +- 日志写入异步化 +- CPU 长时间占用超过阈值时上报诊断事件 + +## 8.3 离线能力 + +离线状态下: + +| 功能 | 行为 | +|---|---| +| 今日页 | 显示最近一次缓存 | +| 报告页 | 可查看历史报告 | +| 对话 | 禁用发送,显示离线提示 | +| Skills | 可查看说明和历史,默认禁止运行需要网络的 skill | +| 自动任务 | 本地可显示,但执行前检查网络 | +| 通知 | 本地系统通知可用,外部通道排队或失败 | + +离线恢复后: + +- 自动刷新 token +- 同步 config +- 同步 skill manifest +- 上报积压事件 +- 检查失败任务是否可重试 + +## 8.4 可观测性 + +日志分层: + +| 日志 | 内容 | +|---|---| +| app.log | 应用生命周期、登录、配置同步 | +| kernel.log | Gateway 启停、健康检查 | +| skill.log | skill 执行、阶段、错误 | +| network.log | 服务端请求摘要,不记录敏感 body | +| renderer.log | UI 错误、页面崩溃 | + +关键事件: + +- `app_started` +- `login_success` +- `config_sync_success` +- `skill_sync_failed` +- `kernel_ready` +- `skill_execution_started` +- `skill_execution_completed` +- `notification_dispatch_failed` +- `llm_proxy_rate_limited` + +错误必须带: + +- request id +- hotel id +- user id hash +- device id +- app version +- skill id/version +- kernel version + +## 8.5 更新策略 + +客户端更新: + +- v1 使用手动确认更新 +- 关键安全更新可强提醒 +- 更新前检查 Gateway 是否有任务运行 +- 更新失败保留旧版本可启动 + +Skill 更新: + +- 默认静默更新 +- 正在执行的 skill 不热替换 +- 新版本失败可回滚上一版本 +- 服务端可按酒店灰度发布 + +## 8.6 兼容性 + +v1 支持: + +| 平台 | 要求 | +|---|---| +| macOS | 13 及以上 | +| Windows | Windows 10 及以上 | + +暂不承诺 Linux 正式支持。内部开发可运行,但不作为 v1 客户交付范围。 + +## 8.7 隐私与合规 + +原则: + +- 明确区分本地数据、服务端数据、第三方平台数据 +- 默认最小化回传 +- 报告与执行结果按酒店租户隔离 +- 客评内容属于敏感业务数据,回传和训练用途需单独授权 +- 不使用客户数据训练通用模型,除非合同另行约定 + +# 第九章 · 里程碑、交付计划与风险登记册 + +## 9.1 里程碑 + +以 2026-04-26 为 v0 启动日,建议第一阶段按 10 周推进。 + +| 里程碑 | 日期 | 目标 | 交付物 | +|---|---|---|---| +| M0 项目启动 | 2026-04-26 至 2026-05-03 | 完成 fork、技术验证、PRD 评审 | ClawX fork、架构 ADR、任务拆分 | +| M1 登录与控制面 | 2026-05-04 至 2026-05-17 | 登录、酒店上下文、配置同步跑通 | AuthManager、ConfigSync、基础 API | +| M2 Kernel 适配 | 2026-05-18 至 2026-05-31 | Gateway 生命周期、Port Adapter、端口检测 | KernelLifecycle、ConversationPort MVP | +| M3 Skill 下发 | 2026-06-01 至 2026-06-14 | manifest、bundle、验签、首个 skill | SkillManager、ctrip-price-monitor | +| M4 产品 UI Alpha | 2026-06-15 至 2026-06-28 | 今日页、Skills、自动任务、报告页 | Alpha 客户端 | +| M5 试点 Beta | 2026-06-29 至 2026-07-12 | 2 到 3 家酒店试点,闭环反馈 | Beta 包、试点报告、风险修复 | + +## 9.2 MVP 范围 + +必须包含: + +- 登录与酒店选择 +- 今日页 +- 对话页基础能力 +- Skill 列表与手动运行 +- 携程价格巡检 +- 日报生成 +- 企业微信或钉钉通知一种 +- Gateway 生命周期管理 +- skill bundle 签名校验 +- 基础诊断包导出 + +暂缓: + +- 开放 Marketplace +- 私有部署 +- 手机端 +- 多内核切换 UI +- 复杂工作流编排 +- 全量通知通道管理 + +## 9.3 评审关口 + +| 关口 | 必须通过 | +|---|---| +| 架构评审 | 端口契约、Main/Renderer 边界、安全边界 | +| 安全评审 | token、Gateway、本地文件、skill bundle | +| UI 评审 | 今日页、对话页、失败态、诊断页 | +| Skill 评审 | manifest、权限、测试、回滚 | +| 试点评审 | 真实酒店任务完成率、失败率、用户反馈 | + +## 9.4 风险登记册 + +| 风险 | 概率 | 影响 | 应对 | +|---|---|---|---| +| ClawX 上游结构变化快,fork 同步困难 | 中 | 高 | 保持上游同步分支,减少早期大重构 | +| OpenClaw Gateway API 不稳定 | 中 | 高 | Adapter 隔离,关键 RPC 写契约测试 | +| OTA 页面结构变化导致 parser 失效 | 高 | 高 | fixture 测试、parse_suspect、快速热更新 skill | +| skill 远程下发带来安全风险 | 中 | 极高 | 签名、hash、权限、沙箱、kill switch | +| 酒店用户不理解 Agent 对话 | 中 | 中 | 今日页优先,任务卡片优先,对话作为补充 | +| LLM Proxy 成本失控 | 中 | 高 | model policy、配额、缓存、按 skill 计量 | +| 通知送达不稳定 | 中 | 中 | 状态回执、重试、失败提示、备用通道 | +| Windows 环境差异 | 中 | 中 | 早期纳入 Windows 测试机和自动化打包 | +| 客户数据合规边界不清 | 中 | 高 | 合同、授权、脱敏、租户隔离、审计 | + +# 附录 A · 术语表 + +| 术语 | 说明 | +|---|---| +| 智念桌面端 | 面向酒店运营的 AI Agent 桌面客户端 | +| Shell | 桌面壳,包括 UI、登录态、本地存储、通知 | +| Kernel | Agent runtime,当前为 OpenClaw | +| Adapter | 把领域 Port 翻译为具体内核调用的适配层 | +| Port | Shell 使用的领域接口,包括 Conversation、Skill、Scheduler、Notification | +| Skill | 可下发、可版本化、可执行的行业能力包 | +| Skill Bundle | skill 打包后的发布产物 | +| Manifest | skill 的声明式元数据 | +| Entitlement | 某酒店或客户已开通的能力权益 | +| LLM Proxy | NIANXX 服务端模型代理层 | +| Gateway | 本地 OpenClaw 服务进程 | +| Artifact | skill 或对话产生的结构化结果,如表格、报告、图片、文件 | +| Kill Switch | 服务端远程禁用某 skill 或某版本的能力 | + +# 附录 B · OpenClaw 上游同步策略 + +## B.1 分支模型 + +建议保留三个长期分支: + +| 分支 | 用途 | +|---|---| +| `upstream-main` | 镜像 ClawX 上游 main | +| `yinian-main` | 智念主开发分支 | +| `release/*` | 客户发布分支 | + +同步流程: + +1. 定期 fetch ClawX upstream +2. 更新 `upstream-main` +3. 创建 `sync/upstream-YYYYMMDD` +4. merge 或 cherry-pick 到 `yinian-main` +5. 跑完整 CI +6. 对冲突文件写同步记录 + +## B.2 改造边界 + +尽量少改上游核心文件,把 YINIAN 逻辑放入独立模块: + +- `src/yinian/*` +- `electron/yinian/*` +- `packages/kernel-core` +- `packages/kernel-context` +- `packages/kernel-adapter-openclaw` +- `packages/ui-kit` + +如果必须改上游文件,要求: + +- 添加注释说明 YINIAN 修改原因 +- 在同步记录中登记 +- 对应测试覆盖 + +## B.3 上游能力取舍 + +保留: + +- Gateway 生命周期基础能力 +- 原有 channel 能力 +- Cron / scheduler 能力 +- Skill 加载机制 +- Provider 抽象中可复用部分 + +替换或隐藏: + +- 用户自填 API key +- 开发者 Marketplace +- 过度技术化设置页 +- 不适合酒店用户的 debug 信息 + +# 附录 C · CI 强制规则清单 + +## C.1 依赖方向 + +必须强制: + +- `kernel-core` 禁止 import OpenClaw +- Renderer 禁止 import `electron` +- Renderer 禁止 import Node 内置模块 +- Renderer 禁止在 `bootstrap.tsx` 之外 import `kernel-adapter-openclaw` +- Skills 禁止 import App UI 代码 +- Skill logic 只能依赖 `skill-spec` 和 `skills-hotel-core` + +## C.2 安全检查 + +CI 必须检查: + +- 无硬编码 API key +- 无硬编码生产 token +- skill manifest 包含 permissions +- skill bundle 构建产物可复现 +- bundle hash 与 manifest.lock 一致 +- preload 暴露 API 白名单 + +## C.3 测试 + +最低要求: + +| 范围 | 要求 | +|---|---| +| `kernel-core` | 类型和 schema 测试 | +| Adapter | RPC 映射契约测试 | +| Main | Auth、Config、SkillManager 单元测试 | +| Renderer | 关键页面组件测试 | +| Skills | fixture、parser、规则、bundle 测试 | +| E2E | 登录、同步、运行 skill、查看报告 | + +## C.4 发布检查 + +发布前必须通过: + +- TypeScript typecheck +- ESLint +- 单元测试 +- skill bundle 验签测试 +- macOS 打包 +- Windows 打包 +- smoke test +- 诊断包导出测试 + +--- + +> **v0.3 后续建议**:下一轮评审重点放在第五章 UI 信息架构、第六章 API 权限模型、第八章 skill 安全边界。评审通过后即可拆分工程 tickets,进入 ClawX fork 改造。 diff --git a/docs/SERVER_CONTRACT_V0.md b/docs/SERVER_CONTRACT_V0.md new file mode 100644 index 0000000..71c4475 --- /dev/null +++ b/docs/SERVER_CONTRACT_V0.md @@ -0,0 +1,243 @@ +# YINIAN Server Contract v0 + +> Updated: 2026-04-26 +> Status: Draft contract for M2 integration +> Client switch: set `YINIAN_API_BASE_URL` to enable HTTP mode. Without it, desktop uses mock mode. + +## 1. Transport + +- Base URL: `YINIAN_API_BASE_URL` +- Content type: `application/json` +- Auth header: `Authorization: Bearer ` +- Desktop header: `X-YINIAN-App-Version: 0.1.0` +- Timestamps: Unix milliseconds. +- Field casing: server should prefer `camelCase`; desktop accepts key `snake_case` aliases for M1/M2 migration. + +## 2. Error Shape + +Non-2xx responses should return: + +```json +{ + "error": { + "code": "SESSION_EXPIRED", + "message": "Session expired" + } +} +``` + +The desktop currently surfaces `error.message` and falls back to `YINIAN API request failed: `. + +Recommended error codes: + +- `INVALID_CREDENTIALS` +- `INVALID_SMS_CODE` +- `SESSION_EXPIRED` +- `WORKSPACE_FORBIDDEN` +- `CONFIG_UNAVAILABLE` +- `SKILLS_MANIFEST_UNAVAILABLE` + +## 3. Device Payload + +Login requests include: + +```json +{ + "device": { + "device_id": "dev_local_electron", + "platform": "darwin", + "app_version": "0.1.0", + "machine_name": "YINIAN Desktop" + } +} +``` + +The exact `device_id` is a placeholder in M2 and should be replaced by a stable desktop installation id later. + +## 4. Auth + +### POST `/auth/login/sms` + +Request: + +```json +{ + "phone": "13800000000", + "code": "123456", + "device": {} +} +``` + +Response: + +```json +{ + "accessToken": "access_demo", + "refreshToken": "refresh_demo", + "accessTokenExpiresAt": 1777188600000 +} +``` + +### POST `/auth/login/password` + +Request: + +```json +{ + "account": "ops@example.com", + "password": "secret", + "device": {} +} +``` + +Response shape is the same as SMS login. + +### POST `/auth/refresh` + +Request: + +```json +{ + "refreshToken": "refresh_demo", + "device": {} +} +``` + +Response: + +```json +{ + "accessToken": "access_refreshed", + "refreshToken": "refresh_rotated", + "accessTokenExpiresAt": 1777189500000 +} +``` + +M2 desktop persists only `refreshToken`; `accessToken` remains memory-only. + +### GET `/auth/me` + +Headers: + +```http +Authorization: Bearer access_demo +``` + +Response: + +```json +{ + "user": { + "id": "user_ops_001", + "name": "王管理员", + "phone": "13800000000", + "email": "ops@example.com", + "avatar": "https://cdn.example.com/avatar.png" + }, + "hotels": [ + { + "id": "workspace_hangzhou_ops", + "name": "智念企业组织空间", + "brand": "智念" + } + ], + "currentHotelId": "workspace_hangzhou_ops", + "accessTokenExpiresAt": 1777188600000 +} +``` + +## 5. Config Sync + +### GET `/config/sync?hotel_id=` + +Response: + +```json +{ + "serverTime": 1777188000000, + "hotel": { + "id": "workspace_hangzhou_ops", + "name": "智念企业组织空间", + "brand": "智念" + }, + "entitlements": [ + { + "skillId": "daily-report", + "name": "日报生成助手", + "version": "1.0.0", + "enabled": true, + "category": "reporting", + "triggers": ["scheduled", "manual"], + "lastRunAt": 1777184400000 + } + ], + "notificationChannels": [ + { + "id": "wechat_ops", + "kind": "wecom", + "label": "业务通知群", + "recipient": "room_001", + "enabled": true, + "source": "nianxx" + } + ], + "featureFlags": { + "skillsSync": true, + "advancedSettings": false + }, + "uiPolicy": { + "defaultPage": "today", + "showAdvancedSettings": false + } +} +``` + +## 6. Skills Manifest + +### GET `/skills/manifest?hotel_id=` + +Response: + +```json +{ + "serverTime": 1777188000000, + "hotelId": "workspace_hangzhou_ops", + "manifestVersion": "2026.04.26.1", + "skills": [ + { + "skillId": "data-check", + "name": "数据检查助手", + "version": "1.2.0", + "enabled": true, + "bundleSha256": "sha256-demo", + "bundleUrl": "https://cdn.example.com/skills/data-check-1.2.0.tgz" + }, + { + "skillId": "customer-reply-helper", + "name": "客户回复助手", + "version": "0.9.0", + "enabled": false, + "bundleSha256": "sha256-disabled" + } + ] +} +``` + +M2 desktop consumes manifest metadata only. It does not download `bundleUrl`, verify signatures, or unpack bundles yet. + +## 7. Client Normalization Rules + +The desktop accepts: + +- `camelCase` and `snake_case` aliases for known fields. +- Missing `user.name` as `未命名用户`. +- Missing hotel fields with safe empty/default values. +- Unknown skill categories as `ops-automation`. +- Unknown trigger values are ignored. +- Missing `uiPolicy.defaultPage` defaults to `today`. +- Missing arrays default to `[]`. + +Malformed server responses should not break the shell, but required auth fields still fail fast: + +- Missing `accessToken` after login or refresh throws `服务端未返回 access token`. +- Authenticated config/skills calls without a session throw `请先登录`. diff --git a/docs/START_HERE.md b/docs/START_HERE.md new file mode 100644 index 0000000..f017cce --- /dev/null +++ b/docs/START_HERE.md @@ -0,0 +1,47 @@ +# YINIAN Desktop 项目启动说明 + +## 当前基线 + +- 上游基础:ClawX `0.3.11` +- 产品定位:快速使用 AI Agent 桌面客户端 +- 当前分支:`yinian-main` +- PRD:`docs/PRD.md` +- M1 交付说明:`docs/M1_HANDOFF.md` +- 服务端契约草案:`docs/SERVER_CONTRACT_V0.md` +- Pilot QA 清单:`docs/PILOT_QA.md` + +## 第一阶段目标 + +1. 保留 ClawX 基础功能可运行。 +2. 接入 YINIAN 登录态和工作空间上下文。 +3. 建立 `kernel-core` 端口契约。 +4. 建立 OpenClaw Adapter 的最小实现。 +5. 替换首屏为“今日”工作台。 +6. 跑通一个服务端下发 skill 的闭环。 + +## 工程边界 + +- Renderer 不直接访问 NIANXX 服务端。 +- Renderer 不读取 token。 +- 客户端不保存第三方模型 API key。 +- Skill bundle 必须签名后才能加载。 +- OpenClaw 相关调用必须逐步收口到 `packages/kernel-adapter-openclaw`。 + +## 常用命令 + +```bash +pnpm install +pnpm run typecheck +pnpm run test +pnpm run build:vite +pnpm run test:e2e +pnpm run dev +``` + +## 关键环境变量 + +| 变量 | 用途 | +|---|---| +| `YINIAN_API_BASE_URL` | 配置后启用真实 HTTP control plane;未配置时使用 mock。 | +| `CLAWX_LEGACY_AUTOSTART=1` | 调试时恢复 ClawX 旧的 Gateway 启动行为。 | +| `CLAWX_E2E=1` | Playwright E2E 兼容模式,保留旧 setup/main flow。 | diff --git a/docs/adr/0001-fork-and-layering-strategy.md b/docs/adr/0001-fork-and-layering-strategy.md new file mode 100644 index 0000000..fdddb0c --- /dev/null +++ b/docs/adr/0001-fork-and-layering-strategy.md @@ -0,0 +1,34 @@ +# ADR 0001: ClawX Fork 与 YINIAN 分层改造策略 + +## 状态 + +Accepted + +## 背景 + +智念桌面端基于 ClawX fork,目标用户从开发者切换为酒店前台、运营、店长和 NIANXX 实施人员。ClawX 提供了 Electron 壳、OpenClaw Gateway 生命周期、技能、渠道、Cron 等基础能力,但其产品心智仍是通用开发者工具。 + +## 决策 + +v1 阶段采取渐进式改造: + +1. 保留 ClawX 根应用结构、构建链路和 Gateway 管理能力。 +2. 新增 `packages/*` 作为 YINIAN 平台边界层。 +3. 在 Renderer 中逐步从通用 ClawX 页面迁移到酒店运营工作台。 +4. 在 Main 中新增 Auth、Config Sync、Skill Manager、Kernel Lifecycle 安全增强。 +5. 在 ClawX 上游变更稳定后,再评估是否迁移到 `apps/desktop` 目录结构。 + +## 后果 + +好处: + +- 第一阶段可快速运行和验证。 +- 保留上游同步能力。 +- YINIAN 领域边界可以逐步变硬。 + +代价: + +- 初期目录结构不是最终理想形态。 +- 一段时间内 ClawX 与 YINIAN 命名会共存。 +- 需要 CI 逐步补上依赖方向约束。 + diff --git a/electron-builder.yml b/electron-builder.yml index f80ba20..77664f3 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -1,6 +1,6 @@ -appId: app.clawx.desktop -productName: ClawX -copyright: Copyright © 2026 ClawX +appId: app.zhinian.assistant +productName: 智念助手 +copyright: Copyright © 2026 YINIAN compression: maximum artifactName: ${productName}-${version}-${os}-${arch}.${ext} @@ -86,8 +86,8 @@ mac: entitlementsInherit: entitlements.mac.plist notarize: true extendInfo: - NSMicrophoneUsageDescription: ClawX requires microphone access for voice features - NSCameraUsageDescription: ClawX requires camera access for video features + NSMicrophoneUsageDescription: 智念助手需要麦克风权限用于语音相关能力 + NSCameraUsageDescription: 智念助手需要摄像头权限用于视频相关能力 dmg: # Explicit volume size prevents dmg-builder@1.2.0 auto-calculation from @@ -133,8 +133,8 @@ nsis: differentialPackage: true createDesktopShortcut: true createStartMenuShortcut: true - shortcutName: ClawX - uninstallDisplayName: ClawX + shortcutName: 智念助手 + uninstallDisplayName: 智念助手 license: LICENSE include: scripts/installer.nsh installerIcon: resources/icons/icon.ico @@ -161,17 +161,17 @@ linux: arch: - x64 category: Utility - maintainer: ClawX Team - vendor: ClawX - synopsis: AI Assistant powered by OpenClaw - description: ClawX is a graphical AI assistant application that integrates with OpenClaw Gateway to provide intelligent automation and assistance across multiple messaging platforms. + maintainer: YINIAN + vendor: YINIAN + synopsis: B 端 AI Agent 桌面助手 + description: 智念助手是面向 B 端业务场景的 AI Agent 桌面客户端,基于 OpenClaw Gateway 提供自动化与智能协作能力。 desktop: entry: - Name: ClawX - Comment: AI Assistant powered by OpenClaw + Name: 智念助手 + Comment: B 端 AI Agent 桌面助手 Categories: Utility;Network; - Keywords: ai;assistant;automation;chat; - StartupWMClass: clawx + Keywords: ai;assistant;business;automation;operations; + StartupWMClass: zhinian-assistant appImage: license: LICENSE diff --git a/electron/api/routes/channels.ts b/electron/api/routes/channels.ts index b7bd6aa..b0ab645 100644 --- a/electron/api/routes/channels.ts +++ b/electron/api/routes/channels.ts @@ -21,6 +21,8 @@ import { assignChannelAccountToAgent, clearAllBindingsForChannel, clearChannelBinding, + deleteAgentConfig, + ensureChannelAgentForAccount, listAgentsSnapshot, listAgentsSnapshotFromConfig, } from '../../utils/agent-config'; @@ -48,6 +50,7 @@ import { import { getOpenClawConfigDir } from '../../utils/paths'; import { cancelWeChatLoginSession, + listWeChatAccountAliases, saveWeChatAccountState, startWeChatLoginSession, waitForWeChatLoginSession, @@ -309,28 +312,13 @@ function isSameConfigValues( async function ensureScopedChannelBinding(channelType: string, accountId?: string): Promise { const storedChannelType = resolveStoredChannelType(channelType); - // Multi-agent safety: only bind when the caller explicitly scopes the account. - // Global channel saves (no accountId) must not override routing to "main". - if (!accountId) return; - const agents = await listAgentsSnapshot(); - if (!agents.agents || agents.agents.length === 0) return; + const scopedAccountId = accountId?.trim() || 'default'; - // Keep backward compatibility for the legacy default account. - if (accountId === 'default') { - if (agents.agents.some((entry) => entry.id === 'main')) { - await assignChannelAccountToAgent('main', storedChannelType, 'default'); - } - return; - } - - // Legacy compatibility: if accountId matches an existing agentId, keep auto-binding. - if (agents.agents.some((entry) => entry.id === accountId)) { + if (scopedAccountId !== 'default') { await migrateLegacyChannelWideBinding(storedChannelType); - await assignChannelAccountToAgent(accountId, storedChannelType, accountId); - return; } - await migrateLegacyChannelWideBinding(storedChannelType); + await ensureChannelAgentForAccount(storedChannelType, scopedAccountId); } async function migrateLegacyChannelWideBinding(channelType: string): Promise { @@ -512,6 +500,78 @@ const channelTargetCache = new Map { + const storedChannelType = resolveStoredChannelType(channelType); + const owner = await readChannelBindingOwner(storedChannelType, accountId); + if (!isManagedChannelAgent(storedChannelType, accountId, owner)) return; + await deleteAgentConfig(owner!); + logger.info('[channels.config] deleted managed channel agent', { + channelType: storedChannelType, + accountId, + agentId: owner, + }); +} + +async function deleteManagedChannelAgentsForChannel(channelType: string): Promise { + const storedChannelType = resolveStoredChannelType(channelType); + const config = await readOpenClawConfig(); + const configuredAccounts = listConfiguredChannelAccountsFromConfig(config); + const accountIds = configuredAccounts[storedChannelType]?.accountIds ?? []; + const candidates = new Set(accountIds); + if (Array.isArray((config as { bindings?: unknown }).bindings)) { + for (const binding of (config as { bindings: unknown[] }).bindings) { + if (!binding || typeof binding !== 'object') continue; + const candidate = binding as { match?: { channel?: unknown; accountId?: unknown } }; + if (!candidate.match || typeof candidate.match !== 'object') continue; + if (candidate.match.channel !== storedChannelType) continue; + if (typeof candidate.match.accountId === 'string' && candidate.match.accountId.trim()) { + candidates.add(candidate.match.accountId.trim()); + } + } + } + + for (const accountId of candidates) { + await deleteManagedChannelAgentIfOwned(storedChannelType, accountId); + } +} + +function isEnabledConfigAccount(account: unknown): boolean { + if (!account || typeof account !== 'object') return false; + const candidate = account as { enabled?: unknown }; + return candidate.enabled !== false; +} + export async function buildChannelAccountsView( ctx: HostApiContext, options?: { probe?: boolean; skipRuntime?: boolean }, @@ -521,11 +581,26 @@ export async function buildChannelAccountsView( // Read config once and share across all sub-calls (was 5 readFile calls before). const openClawConfig = await readOpenClawConfig(); - const [configuredChannels, configuredAccounts, agentsSnapshot] = await Promise.all([ + const [configuredChannels, configuredAccounts, initialAgentsSnapshot] = await Promise.all([ listConfiguredChannelsFromConfig(openClawConfig), Promise.resolve(listConfiguredChannelAccountsFromConfig(openClawConfig)), listAgentsSnapshotFromConfig(openClawConfig), ]); + let agentsSnapshot = initialAgentsSnapshot; + + for (const [rawChannelType, accountSummary] of Object.entries(configuredAccounts)) { + const storedChannelType = resolveStoredChannelType(rawChannelType); + if (!AUTO_AGENT_BOUND_CHANNELS.has(storedChannelType)) continue; + for (const accountId of accountSummary.accountIds ?? []) { + const scopedAccountId = accountId?.trim() || 'default'; + if (agentsSnapshot.channelAccountOwners[`${storedChannelType}:${scopedAccountId}`]) continue; + agentsSnapshot = await ensureChannelAgentForAccount(storedChannelType, scopedAccountId); + logger.info('[channels.accounts] auto-created missing channel agent binding', { + channelType: storedChannelType, + accountId: scopedAccountId, + }); + } + } let gatewayStatus: GatewayChannelStatusPayload | null = null; if (!skipRuntime) { @@ -540,13 +615,13 @@ export async function buildChannelAccountsView( { probe }, probe ? 5000 : 8000, ); - lastChannelsStatusOkAt = Date.now(); + lastChannelsStatusOkAt = nextChannelStatusTimestamp(lastChannelsStatusFailureAt); logger.info( `[channels.accounts] channels.status probe=${probe ? '1' : '0'} elapsedMs=${Date.now() - rpcStartedAt} snapshot=${buildGatewayStatusSnapshot(gatewayStatus)}` ); } catch { const probe = options?.probe === true; - lastChannelsStatusFailureAt = Date.now(); + lastChannelsStatusFailureAt = nextChannelStatusTimestamp(lastChannelsStatusOkAt); logger.warn( `[channels.accounts] channels.status probe=${probe ? '1' : '0'} failed after ${Date.now() - startedAt}ms` ); @@ -605,6 +680,9 @@ export async function buildChannelAccountsView( if (!accountId) { return acc; } + if (rawChannelType === 'agentbus' && accountId === 'default') { + return acc; + } if (!shouldIncludeRuntimeAccountId(accountId, configuredAccountIdSet, account)) { return acc; } @@ -615,7 +693,15 @@ export async function buildChannelAccountsView( const accounts: ChannelAccountView[] = accountIds.map((accountId) => { const runtime = runtimeAccounts.find((item) => item.accountId === accountId); - const runtimeSnapshot: ChannelRuntimeAccountSnapshot = runtime ?? {}; + const configuredAccount = channelSection?.accounts?.[accountId]; + const isAgentBusConfigConnected = + rawChannelType === 'agentbus' + && channelAccountsFromConfig.includes(accountId) + && isEnabledConfigAccount(configuredAccount) + && !(typeof runtime?.lastError === 'string' && runtime.lastError.trim()); + const runtimeSnapshot: ChannelRuntimeAccountSnapshot = isAgentBusConfigConnected + ? { ...runtime, connected: true, running: true } + : (runtime ?? {}); const status = computeChannelRuntimeStatus(runtimeSnapshot, { gatewayHealthState: effectiveGatewayHealthState, }); @@ -623,8 +709,8 @@ export async function buildChannelAccountsView( accountId, name: runtime?.name || accountId, configured: channelAccountsFromConfig.includes(accountId) || runtime?.configured === true, - connected: runtime?.connected === true, - running: runtime?.running === true, + connected: runtimeSnapshot.connected === true, + running: runtimeSnapshot.running === true, linked: runtime?.linked === true, lastError: typeof runtime?.lastError === 'string' ? runtime.lastError : undefined, status, @@ -1286,6 +1372,22 @@ export async function handleChannelRoutes( return true; } + if (url.pathname === '/api/channels/account-aliases' && req.method === 'GET') { + try { + const channelType = toUiChannelType(url.searchParams.get('channelType')?.trim() || ''); + if (channelType !== UI_WECHAT_CHANNEL_TYPE) { + sendJson(res, 200, { success: true, channelType, aliases: [] }); + return true; + } + + const aliases = await listWeChatAccountAliases(); + sendJson(res, 200, { success: true, channelType, aliases }); + } catch (error) { + sendJson(res, 500, { success: false, error: String(error) }); + } + return true; + } + if (url.pathname === '/api/channels/targets' && req.method === 'GET') { try { const channelType = url.searchParams.get('channelType')?.trim() || ''; @@ -1534,10 +1636,12 @@ export async function handleChannelRoutes( const accountId = url.searchParams.get('accountId') || undefined; const storedChannelType = resolveStoredChannelType(channelType); if (accountId) { + await deleteManagedChannelAgentIfOwned(storedChannelType, accountId); await deleteChannelAccountConfig(channelType, accountId); await clearChannelBinding(storedChannelType, accountId); scheduleGatewayChannelSaveRefresh(ctx, storedChannelType, `channel:deleteAccount:${storedChannelType}`); } else { + await deleteManagedChannelAgentsForChannel(storedChannelType); await deleteChannelConfig(channelType); await clearAllBindingsForChannel(storedChannelType); scheduleGatewayChannelRestart(ctx, `channel:deleteConfig:${storedChannelType}`); diff --git a/electron/api/routes/knowledge.ts b/electron/api/routes/knowledge.ts new file mode 100644 index 0000000..f2a0193 --- /dev/null +++ b/electron/api/routes/knowledge.ts @@ -0,0 +1,271 @@ +import type { IncomingMessage, ServerResponse } from 'node:http'; +import crypto from 'node:crypto'; +import { basename, extname, join } from 'node:path'; +import { mkdir, readFile, stat, writeFile, copyFile } from 'node:fs/promises'; +import type { HostApiContext } from '../context'; +import { parseJsonBody, sendJson } from '../route-utils'; +import { getDataDir } from '../../utils/paths'; + +const KNOWLEDGE_ROOT = join(getDataDir(), 'yinian', 'knowledge'); +const MAX_KNOWLEDGE_FILE_BYTES = 20 * 1024 * 1024; +const MAX_CONTEXT_CHARS_PER_FILE = 32_000; +const MAX_CONTEXT_TOTAL_CHARS = 96_000; + +const TEXT_MIME_BY_EXT: Record = { + '.txt': 'text/plain', + '.md': 'text/markdown', + '.markdown': 'text/markdown', + '.csv': 'text/csv', + '.tsv': 'text/tab-separated-values', + '.json': 'application/json', + '.jsonl': 'application/x-ndjson', + '.xml': 'application/xml', + '.html': 'text/html', + '.htm': 'text/html', + '.yaml': 'application/yaml', + '.yml': 'application/yaml', + '.log': 'text/plain', + '.ini': 'text/plain', + '.conf': 'text/plain', + '.css': 'text/css', + '.js': 'text/javascript', + '.jsx': 'text/javascript', + '.ts': 'text/typescript', + '.tsx': 'text/typescript', + '.py': 'text/x-python', + '.sql': 'application/sql', +}; + +const WORD_MIME_BY_EXT: Record = { + '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', +}; + +export interface KnowledgeDocument { + id: string; + workspaceId: string; + name: string; + mimeType: string; + size: number; + storedPath: string; + textPath?: string; + originalPath?: string; + importedAt: number; + status: 'stored'; +} + +function sanitizeWorkspaceId(workspaceId?: string): string { + const value = workspaceId?.trim() || 'default'; + return value.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 96) || 'default'; +} + +function getWorkspaceDir(workspaceId?: string): string { + return join(KNOWLEDGE_ROOT, sanitizeWorkspaceId(workspaceId)); +} + +function getRegistryPath(workspaceId?: string): string { + return join(getWorkspaceDir(workspaceId), 'registry.json'); +} + +async function readRegistry(workspaceId?: string): Promise { + const raw = await readFile(getRegistryPath(workspaceId), 'utf8').catch(() => ''); + if (!raw.trim()) return []; + try { + const parsed = JSON.parse(raw) as unknown; + return Array.isArray(parsed) ? parsed as KnowledgeDocument[] : []; + } catch { + return []; + } +} + +async function writeRegistry(workspaceId: string | undefined, docs: KnowledgeDocument[]): Promise { + await mkdir(getWorkspaceDir(workspaceId), { recursive: true }); + await writeFile(getRegistryPath(workspaceId), JSON.stringify(docs, null, 2), 'utf8'); +} + +function getTextMimeType(filePath: string): string | null { + const ext = extname(filePath).toLowerCase(); + return TEXT_MIME_BY_EXT[ext] ?? WORD_MIME_BY_EXT[ext] ?? null; +} + +function isDocx(filePath: string): boolean { + return extname(filePath).toLowerCase() === '.docx'; +} + +async function extractDocxText(filePath: string): Promise { + const mammoth = await import('mammoth'); + const result = await mammoth.extractRawText({ path: filePath }); + return result.value.trim(); +} + +export async function importKnowledgeFiles(params: { + workspaceId?: string; + filePaths: string[]; +}): Promise<{ documents: KnowledgeDocument[]; rejected: Array<{ filePath: string; reason: string }> }> { + const workspaceId = sanitizeWorkspaceId(params.workspaceId); + const workspaceDir = getWorkspaceDir(workspaceId); + const filesDir = join(workspaceDir, 'files'); + const textDir = join(workspaceDir, 'texts'); + await mkdir(filesDir, { recursive: true }); + await mkdir(textDir, { recursive: true }); + + const currentDocs = await readRegistry(workspaceId); + const importedDocs: KnowledgeDocument[] = []; + const rejected: Array<{ filePath: string; reason: string }> = []; + + for (const filePath of params.filePaths) { + const mimeType = getTextMimeType(filePath); + if (!mimeType) { + rejected.push({ filePath, reason: '仅支持文本类知识文件' }); + continue; + } + + const fileStat = await stat(filePath).catch(() => null); + if (!fileStat || !fileStat.isFile()) { + rejected.push({ filePath, reason: '文件不存在或不可读取' }); + continue; + } + if (fileStat.size > MAX_KNOWLEDGE_FILE_BYTES) { + rejected.push({ filePath, reason: '文件超过 20MB 限制' }); + continue; + } + + const id = crypto.randomUUID(); + const ext = extname(filePath).toLowerCase(); + const storedPath = join(filesDir, `${id}${ext}`); + await copyFile(filePath, storedPath); + let textPath: string | undefined; + + if (isDocx(filePath)) { + try { + const extractedText = await extractDocxText(storedPath); + if (!extractedText) { + rejected.push({ filePath, reason: 'Word 文档未提取到可用文本' }); + continue; + } + textPath = join(textDir, `${id}.txt`); + await writeFile(textPath, extractedText, 'utf8'); + } catch { + rejected.push({ filePath, reason: 'Word 文档解析失败,请确认文件为 .docx 格式' }); + continue; + } + } + + const doc: KnowledgeDocument = { + id, + workspaceId, + name: basename(filePath), + mimeType, + size: fileStat.size, + storedPath, + ...(textPath ? { textPath } : {}), + originalPath: filePath, + importedAt: Date.now(), + status: 'stored', + }; + importedDocs.push(doc); + } + + if (importedDocs.length > 0) { + await writeRegistry(workspaceId, [...importedDocs, ...currentDocs]); + } + + return { documents: importedDocs, rejected }; +} + +export async function buildKnowledgeContext(params: { + workspaceId?: string; + documentIds: string[]; +}): Promise<{ context: string; documents: KnowledgeDocument[]; missing: string[] }> { + const workspaceId = sanitizeWorkspaceId(params.workspaceId); + const selectedIds = new Set(params.documentIds.filter((id) => typeof id === 'string' && id.trim())); + if (selectedIds.size === 0) { + return { context: '', documents: [], missing: [] }; + } + + const registry = await readRegistry(workspaceId); + const docs = registry.filter((doc) => selectedIds.has(doc.id)); + const missing = [...selectedIds].filter((id) => !docs.some((doc) => doc.id === id)); + const sections: string[] = []; + const usedDocs: KnowledgeDocument[] = []; + let totalChars = 0; + + for (const doc of docs) { + const readablePath = doc.textPath || doc.storedPath; + const raw = await readFile(readablePath, 'utf8').catch(() => ''); + const text = raw.trim(); + if (!text) { + missing.push(doc.id); + continue; + } + + const remaining = MAX_CONTEXT_TOTAL_CHARS - totalChars; + if (remaining <= 0) break; + const content = text.slice(0, Math.min(MAX_CONTEXT_CHARS_PER_FILE, remaining)); + totalChars += content.length; + usedDocs.push(doc); + sections.push([ + `## ${doc.name}`, + `类型:${doc.mimeType}`, + content, + ].join('\n')); + } + + if (sections.length === 0) { + return { context: '', documents: [], missing }; + } + + return { + context: [ + '[知识库上下文]', + '用户已选择在本轮对话中使用当前组织空间知识库。以下内容来自智念助手保存的本地备份文件;回答前请优先参考这些内容。', + ...sections, + ].join('\n\n'), + documents: usedDocs, + missing, + }; +} + +export async function handleKnowledgeRoutes( + req: IncomingMessage, + res: ServerResponse, + url: URL, + _ctx: HostApiContext, +): Promise { + if (url.pathname === '/api/knowledge/files' && req.method === 'GET') { + const workspaceId = sanitizeWorkspaceId(url.searchParams.get('workspaceId') ?? undefined); + const documents = await readRegistry(workspaceId); + sendJson(res, 200, { documents }); + return true; + } + + if (url.pathname === '/api/knowledge/import-paths' && req.method === 'POST') { + try { + const body = await parseJsonBody<{ workspaceId?: string; filePaths?: string[] }>(req); + const filePaths = Array.isArray(body.filePaths) ? body.filePaths : []; + if (filePaths.length === 0) { + sendJson(res, 400, { success: false, error: 'No files selected' }); + return true; + } + + const result = await importKnowledgeFiles({ workspaceId: body.workspaceId, filePaths }); + sendJson(res, 200, { success: true, ...result }); + } catch (error) { + sendJson(res, 500, { success: false, error: String(error) }); + } + return true; + } + + if (url.pathname === '/api/knowledge/context' && req.method === 'POST') { + try { + const body = await parseJsonBody<{ workspaceId?: string; documentIds?: string[] }>(req); + const documentIds = Array.isArray(body.documentIds) ? body.documentIds : []; + const result = await buildKnowledgeContext({ workspaceId: body.workspaceId, documentIds }); + sendJson(res, 200, { success: true, ...result }); + } catch (error) { + sendJson(res, 500, { success: false, error: String(error) }); + } + return true; + } + + return false; +} diff --git a/electron/api/server.ts b/electron/api/server.ts index d2085cd..f1901ef 100644 --- a/electron/api/server.ts +++ b/electron/api/server.ts @@ -14,6 +14,7 @@ import { handleLogRoutes } from './routes/logs'; import { handleUsageRoutes } from './routes/usage'; import { handleSkillRoutes } from './routes/skills'; import { handleFileRoutes } from './routes/files'; +import { handleKnowledgeRoutes } from './routes/knowledge'; import { handleSessionRoutes } from './routes/sessions'; import { handleCronRoutes } from './routes/cron'; import { handleDiagnosticsRoutes } from './routes/diagnostics'; @@ -35,6 +36,7 @@ const coreRouteHandlers: RouteHandler[] = [ handleChannelRoutes, handleSkillRoutes, handleFileRoutes, + handleKnowledgeRoutes, handleSessionRoutes, handleCronRoutes, handleDiagnosticsRoutes, diff --git a/electron/gateway/config-sync.ts b/electron/gateway/config-sync.ts index f7b0d2c..1caf7ff 100644 --- a/electron/gateway/config-sync.ts +++ b/electron/gateway/config-sync.ts @@ -20,13 +20,13 @@ import { getApiKey, getDefaultProvider, getProvider } from '../utils/secure-stor import { getProviderEnvVar, getKeyableProviderTypes } from '../utils/provider-registry'; import { getOpenClawDir, getOpenClawEntryPath, isOpenClawPresent } from '../utils/paths'; import { getUvMirrorEnv } from '../utils/uv-env'; -import { cleanupDanglingWeChatPluginState, listConfiguredChannelsFromConfig, readOpenClawConfig } from '../utils/channel-config'; +import { cleanupDanglingWeChatPluginState, listConfiguredChannelsFromConfig, readOpenClawConfig, writeOpenClawConfig, type ChannelConfigData } from '../utils/channel-config'; import { sanitizeOpenClawConfig, batchSyncConfigFields } from '../utils/openclaw-auth'; import { buildProxyEnv, resolveProxySettings } from '../utils/proxy'; import { syncProxyConfigToOpenClaw } from '../utils/openclaw-proxy'; import { logger } from '../utils/logger'; import { prependPathEntry } from '../utils/env-path'; -import { copyPluginFromNodeModules, fixupPluginManifest, cpSyncSafe } from '../utils/plugin-install'; +import { copyPluginFromNodeModules, ensureCloudSyncPluginInstalled, fixupPluginManifest, cpSyncSafe } from '../utils/plugin-install'; import { stripSystemdSupervisorEnv } from './config-sync-env'; @@ -60,6 +60,69 @@ const CHANNEL_PLUGIN_MAP: Record = * plugin and must be removed. */ const BUILTIN_CHANNEL_EXTENSIONS = ['discord', 'telegram', 'qqbot']; +const CLOUD_SYNC_PLUGIN_ID = 'cloud-sync'; +const DEFAULT_CLOUD_SYNC_SERVER_URL = 'https://onefeel.brother7.cn'; + +function isPlainRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function normalizeServerUrl(value: string | undefined): string | undefined { + const trimmed = value?.trim(); + if (!trimmed) return undefined; + return trimmed.replace(/\/+$/, ''); +} + +export function deriveCloudSyncServerUrl(): string { + const explicit = normalizeServerUrl( + process.env.YINIAN_CLOUD_SYNC_SERVER_URL ?? process.env.CLOUDCLAW_SERVER_URL, + ); + if (explicit) return explicit; + + const apiBaseUrl = normalizeServerUrl(process.env.YINIAN_API_BASE_URL); + if (apiBaseUrl) { + return apiBaseUrl.replace(/\/ingress$/i, ''); + } + + return DEFAULT_CLOUD_SYNC_SERVER_URL; +} + +export async function ensureCloudSyncPluginConfigured(): Promise { + if (process.env.YINIAN_CLOUD_SYNC_ENABLED === '0') { + return; + } + + const installResult = ensureCloudSyncPluginInstalled(); + if (installResult.warning) { + logger.warn(`[plugin] Cloud Sync: ${installResult.warning}`); + } + if (!installResult.installed) { + return; + } + + const serverUrl = deriveCloudSyncServerUrl(); + const config = await readOpenClawConfig(); + config.plugins ??= {}; + config.plugins.enabled = true; + + const allow = Array.isArray(config.plugins.allow) ? config.plugins.allow : []; + if (!allow.includes(CLOUD_SYNC_PLUGIN_ID)) { + config.plugins.allow = [...allow, CLOUD_SYNC_PLUGIN_ID]; + } + + config.plugins.entries ??= {}; + const existingEntry = config.plugins.entries[CLOUD_SYNC_PLUGIN_ID]; + const entry: ChannelConfigData = isPlainRecord(existingEntry) ? { ...existingEntry } : {}; + const existingEntryConfig = isPlainRecord(entry.config) ? entry.config : {}; + entry.enabled = true; + entry.config = { + ...existingEntryConfig, + serverUrl, + }; + config.plugins.entries[CLOUD_SYNC_PLUGIN_ID] = entry; + + await writeOpenClawConfig(config); +} function cleanupStaleBuiltInExtensions(): void { for (const ext of BUILTIN_CHANNEL_EXTENSIONS) { @@ -89,12 +152,15 @@ function buildBundledPluginSources(pluginDirName: string): string[] { return app.isPackaged ? [ join(process.resourcesPath, 'openclaw-plugins', pluginDirName), + join(process.resourcesPath, 'resources', 'openclaw-plugins', pluginDirName), join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', pluginDirName), join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', pluginDirName), ] : [ join(app.getAppPath(), 'build', 'openclaw-plugins', pluginDirName), + join(app.getAppPath(), 'resources', 'openclaw-plugins', pluginDirName), join(process.cwd(), 'build', 'openclaw-plugins', pluginDirName), + join(process.cwd(), 'resources', 'openclaw-plugins', pluginDirName), ]; } @@ -295,6 +361,12 @@ export async function syncGatewayConfigBeforeLaunch( logger.warn('Failed to clean dangling WeChat plugin state before launch:', err); } + try { + await ensureCloudSyncPluginConfigured(); + } catch (err) { + logger.warn('Failed to configure Cloud Sync plugin before launch:', err); + } + // Remove stale copies of built-in extensions (Discord, Telegram) that // override OpenClaw's working built-in plugins and break channel loading. try { diff --git a/electron/gateway/ws-client.ts b/electron/gateway/ws-client.ts index e2c9e5e..c5d9e0b 100644 --- a/electron/gateway/ws-client.ts +++ b/electron/gateway/ws-client.ts @@ -127,6 +127,9 @@ export function buildGatewayConnectFrame(options: { signedAtMs, token: options.token ?? null, nonce: options.challengeNonce, + version: 'v3', + platform: options.platform, + deviceFamily: null, }); const signature = signDevicePayload(options.deviceIdentity.privateKeyPem, payload); return { diff --git a/electron/main/index.ts b/electron/main/index.ts index 326c8c6..225cfbe 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -48,7 +48,8 @@ import { browserOAuthManager } from '../utils/browser-oauth'; import { whatsAppLoginManager } from '../utils/whatsapp-login'; import { syncAllProviderAuthToRuntime } from '../services/providers/provider-runtime-sync'; -const WINDOWS_APP_USER_MODEL_ID = 'app.clawx.desktop'; +const WINDOWS_APP_USER_MODEL_ID = 'app.zhinian.assistant'; +const PRODUCT_NAME = '智念助手'; const isE2EMode = process.env.CLAWX_E2E === '1'; const requestedUserDataDir = process.env.CLAWX_USER_DATA_DIR?.trim(); @@ -56,6 +57,8 @@ if (isE2EMode && requestedUserDataDir) { app.setPath('userData', requestedUserDataDir); } +app.setName(PRODUCT_NAME); + // Disable GPU hardware acceleration globally for maximum stability across // all GPU configurations (no GPU, integrated, discrete). // @@ -77,7 +80,7 @@ app.disableHardwareAcceleration(); // on X11 it supplements the StartupWMClass matching. // Must be called before app.whenReady() / before any window is created. if (process.platform === 'linux') { - app.setDesktopName('clawx.desktop'); + app.setDesktopName('zhinian-assistant.desktop'); } // Prevent multiple instances of the app from running simultaneously. @@ -139,21 +142,29 @@ function getIconsDir(): string { return join(__dirname, '../../resources/icons'); } +function getAppIconPath(): string { + const iconsDir = getIconsDir(); + return process.platform === 'win32' + ? join(iconsDir, 'icon.ico') + : join(iconsDir, 'icon.png'); +} + /** * Get the app icon for the current platform */ function getAppIcon(): Electron.NativeImage | undefined { - if (process.platform === 'darwin') return undefined; // macOS uses the app bundle icon - - const iconsDir = getIconsDir(); - const iconPath = - process.platform === 'win32' - ? join(iconsDir, 'icon.ico') - : join(iconsDir, 'icon.png'); - const icon = nativeImage.createFromPath(iconPath); + const icon = nativeImage.createFromPath(getAppIconPath()); return icon.isEmpty() ? undefined : icon; } +function applyRuntimeAppIcon(): void { + if (process.platform !== 'darwin' || !app.dock) return; + const icon = nativeImage.createFromPath(join(getIconsDir(), 'icon.png')); + if (!icon.isEmpty()) { + app.dock.setIcon(icon); + } +} + /** * Create the main application window */ @@ -162,6 +173,12 @@ function createWindow(): BrowserWindow { const isWindows = process.platform === 'win32'; const useCustomTitleBar = isWindows; const shouldSkipSetupForE2E = process.env.CLAWX_E2E_SKIP_SETUP === '1'; + const e2eRendererQuery = isE2EMode + ? { + e2e: '1', + ...(shouldSkipSetupForE2E ? { e2eSkipSetup: '1' } : {}), + } + : undefined; const win = new BrowserWindow({ width: 1280, @@ -180,6 +197,7 @@ function createWindow(): BrowserWindow { trafficLightPosition: isMac ? { x: 16, y: 16 } : undefined, frame: isMac || !useCustomTitleBar, show: false, + title: PRODUCT_NAME, }); // Handle external links — only allow safe protocols to prevent arbitrary @@ -201,18 +219,18 @@ function createWindow(): BrowserWindow { // Load the app if (process.env.VITE_DEV_SERVER_URL) { const rendererUrl = new URL(process.env.VITE_DEV_SERVER_URL); - if (shouldSkipSetupForE2E) { - rendererUrl.searchParams.set('e2eSkipSetup', '1'); + if (e2eRendererQuery) { + Object.entries(e2eRendererQuery).forEach(([key, value]) => { + rendererUrl.searchParams.set(key, value); + }); } win.loadURL(rendererUrl.toString()); - if (!isE2EMode) { + if (!isE2EMode && process.env.YINIAN_OPEN_DEVTOOLS === '1') { win.webContents.openDevTools(); } } else { win.loadFile(join(__dirname, '../../dist/index.html'), { - query: shouldSkipSetupForE2E - ? { e2eSkipSetup: '1' } - : undefined, + query: e2eRendererQuery, }); } @@ -281,7 +299,7 @@ function createMainWindow(): BrowserWindow { async function initialize(): Promise { // Initialize logger first logger.init(); - logger.info('=== ClawX Application Starting ==='); + logger.info('=== 智念助手 Application Starting ==='); logger.debug( `Runtime: platform=${process.platform}/${process.arch}, electron=${process.versions.electron}, node=${process.versions.node}, packaged=${app.isPackaged}, pid=${process.pid}, ppid=${process.ppid}` ); @@ -469,9 +487,12 @@ async function initialize(): Promise { hostEventBus.emit('channel:whatsapp-error', error); }); - // Start Gateway automatically (this seeds missing bootstrap files with full templates) + // YINIAN: do not start Gateway before a user has logged in and a workspace + // context has been loaded. Legacy ClawX auto-start can be restored for + // debugging with CLAWX_LEGACY_AUTOSTART=1. const gatewayAutoStart = await getSetting('gatewayAutoStart'); - if (!isE2EMode && gatewayAutoStart) { + const legacyAutoStart = process.env.CLAWX_LEGACY_AUTOSTART === '1'; + if (!isE2EMode && gatewayAutoStart && legacyAutoStart) { try { await syncAllProviderAuthToRuntime(); logger.debug('Auto-starting Gateway...'); @@ -483,6 +504,8 @@ async function initialize(): Promise { } } else if (isE2EMode) { logger.info('Gateway auto-start skipped in E2E mode'); + } else if (!legacyAutoStart) { + logger.info('Gateway auto-start deferred until YINIAN login'); } else { logger.info('Gateway auto-start disabled in settings'); } @@ -543,7 +566,7 @@ if (gotTheLock) { // When a second instance is launched, focus the existing window instead. app.on('second-instance', () => { - logger.info('Second ClawX instance detected; redirecting to the existing window'); + logger.info('Second 智念助手 instance detected; redirecting to the existing window'); const focusRequest = requestSecondInstanceFocus( mainWindowFocusState, @@ -560,6 +583,7 @@ if (gotTheLock) { // Application lifecycle app.whenReady().then(() => { + applyRuntimeAppIcon(); void initialize().catch((error) => { logger.error('Application initialization failed:', error); }); diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index 00937d4..52c40f2 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -63,6 +63,7 @@ import { import { validateApiKeyWithProvider } from '../services/providers/provider-validation'; import { appUpdater } from './updater'; import { registerHostApiProxyHandlers } from './ipc/host-api-proxy'; +import { registerYinianHandlers } from './ipc/yinian'; import { isLaunchAtStartupKey, isProxyKey, @@ -85,6 +86,9 @@ export function registerIpcHandlers( // Host API proxy handlers registerHostApiProxyHandlers(); + // YINIAN hotel control-plane handlers + registerYinianHandlers(); + // Gateway handlers registerGatewayHandlers(gatewayManager, mainWindow); diff --git a/electron/main/ipc/yinian.ts b/electron/main/ipc/yinian.ts new file mode 100644 index 0000000..1562ed9 --- /dev/null +++ b/electron/main/ipc/yinian.ts @@ -0,0 +1,60 @@ +import { ipcMain } from 'electron'; +import { getYinianControlPlane } from '../../yinian/control-plane'; +import { getYinianStorage } from '../../yinian/storage'; +import type { YinianLoginWithPasswordInput, YinianLoginWithSmsInput, YinianSavedCredentials } from '../../../shared/yinian'; + +export function registerYinianHandlers(): void { + ipcMain.handle('yinian:server:status', async () => getYinianControlPlane().getServerStatus()); + + ipcMain.handle('yinian:auth:createImageCaptcha', async (_, randomStr?: string) => { + return getYinianControlPlane().createImageCaptcha(randomStr); + }); + + ipcMain.handle('yinian:auth:restoreSession', async () => getYinianControlPlane().restoreSession()); + + ipcMain.handle('yinian:auth:getSessionState', async () => getYinianControlPlane().getSessionState()); + + ipcMain.handle('yinian:auth:loginWithSms', async (_, input: YinianLoginWithSmsInput) => { + return getYinianControlPlane().loginWithSms(input); + }); + + ipcMain.handle('yinian:auth:loginWithPassword', async (_, input: YinianLoginWithPasswordInput) => { + return getYinianControlPlane().loginWithPassword(input); + }); + + ipcMain.handle('yinian:auth:logout', async () => getYinianControlPlane().logout()); + + ipcMain.handle('yinian:auth:getSavedCredentials', async () => { + return getYinianStorage().getSavedCredentials(); + }); + + ipcMain.handle('yinian:auth:saveCredentials', async (_, input: Pick) => { + const account = typeof input.account === 'string' ? input.account.trim() : ''; + if (!account) { + await getYinianStorage().clearSavedCredentials(); + return undefined; + } + const credentials: YinianSavedCredentials = { + account, + password: input.rememberPassword ? input.password ?? '' : undefined, + rememberPassword: Boolean(input.rememberPassword), + updatedAt: Date.now(), + }; + await getYinianStorage().setSavedCredentials(credentials); + return credentials; + }); + + ipcMain.handle('yinian:auth:clearSavedCredentials', async () => { + await getYinianStorage().clearSavedCredentials(); + }); + + ipcMain.handle('yinian:config:get', async () => getYinianControlPlane().getConfigSnapshot()); + + ipcMain.handle('yinian:hotel:switch', async (_, hotelId: string) => getYinianControlPlane().switchHotel(hotelId)); + + ipcMain.handle('yinian:skills:sync', async () => getYinianControlPlane().syncSkills()); + + ipcMain.handle('yinian:skills:listLocal', async () => getYinianControlPlane().listLocalSkills()); + + ipcMain.handle('yinian:skills:getRegistry', async (_, hotelId?: string) => getYinianControlPlane().getSkillRegistry(hotelId)); +} diff --git a/electron/main/launch-at-startup.ts b/electron/main/launch-at-startup.ts index bccc685..a9654f9 100644 --- a/electron/main/launch-at-startup.ts +++ b/electron/main/launch-at-startup.ts @@ -4,7 +4,7 @@ import { dirname, join } from 'node:path'; import { logger } from '../utils/logger'; import { getSetting } from '../utils/store'; -const LINUX_AUTOSTART_FILE = join('.config', 'autostart', 'clawx.desktop'); +const LINUX_AUTOSTART_FILE = join('.config', 'autostart', 'zhinian-assistant.desktop'); function quoteDesktopArg(value: string): string { if (!value) return '""'; @@ -30,8 +30,8 @@ function getLinuxDesktopEntry(): string { '[Desktop Entry]', 'Type=Application', 'Version=1.0', - 'Name=ClawX', - 'Comment=ClawX - AI Assistant', + 'Name=智念助手', + 'Comment=B 端 AI Agent 桌面助手', `Exec=${getLinuxExecCommand()}`, 'Terminal=false', 'Categories=Utility;', diff --git a/electron/main/tray.ts b/electron/main/tray.ts index 0dc9c95..bd8b83d 100644 --- a/electron/main/tray.ts +++ b/electron/main/tray.ts @@ -57,7 +57,7 @@ export function createTray(mainWindow: BrowserWindow): Tray { tray = new Tray(icon); // Set tooltip - tray.setToolTip('ClawX - AI Assistant'); + tray.setToolTip('智念助手 - B 端 AI Agent'); const showWindow = () => { if (mainWindow.isDestroyed()) return; @@ -68,7 +68,7 @@ export function createTray(mainWindow: BrowserWindow): Tray { // Create context menu const contextMenu = Menu.buildFromTemplate([ { - label: 'Show ClawX', + label: '显示智念助手', click: showWindow, }, { @@ -91,7 +91,7 @@ export function createTray(mainWindow: BrowserWindow): Tray { label: 'Quick Actions', submenu: [ { - label: 'Open Chat', + label: '打开对话', click: () => { if (mainWindow.isDestroyed()) return; mainWindow.show(); @@ -99,7 +99,7 @@ export function createTray(mainWindow: BrowserWindow): Tray { }, }, { - label: 'Open Settings', + label: '打开设置', click: () => { if (mainWindow.isDestroyed()) return; mainWindow.show(); @@ -112,7 +112,7 @@ export function createTray(mainWindow: BrowserWindow): Tray { type: 'separator', }, { - label: 'Check for Updates...', + label: '检查更新...', click: () => { if (mainWindow.isDestroyed()) return; mainWindow.webContents.send('update:check'); @@ -122,7 +122,7 @@ export function createTray(mainWindow: BrowserWindow): Tray { type: 'separator', }, { - label: 'Quit ClawX', + label: '退出智念助手', click: () => { app.quit(); }, @@ -157,7 +157,7 @@ export function createTray(mainWindow: BrowserWindow): Tray { */ export function updateTrayStatus(status: string): void { if (tray) { - tray.setToolTip(`ClawX - ${status}`); + tray.setToolTip(`智念助手 - ${status}`); } } diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 60dcab8..d8fe765 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -3,6 +3,10 @@ * Exposes safe APIs to the renderer process via contextBridge */ import { contextBridge, ipcRenderer } from 'electron'; +import type { + YinianLoginWithPasswordInput, + YinianLoginWithSmsInput, +} from '../../shared/yinian'; /** * IPC renderer methods exposed to the renderer process @@ -45,6 +49,22 @@ const electronAPI = { 'app:quit', 'app:relaunch', 'app:request', + // YINIAN + 'yinian:auth:restoreSession', + 'yinian:auth:createImageCaptcha', + 'yinian:auth:getSessionState', + 'yinian:auth:loginWithSms', + 'yinian:auth:loginWithPassword', + 'yinian:auth:logout', + 'yinian:auth:getSavedCredentials', + 'yinian:auth:saveCredentials', + 'yinian:auth:clearSavedCredentials', + 'yinian:config:get', + 'yinian:hotel:switch', + 'yinian:skills:sync', + 'yinian:skills:listLocal', + 'yinian:skills:getRegistry', + 'yinian:server:status', // Window controls 'window:minimize', 'window:maximize', @@ -266,8 +286,35 @@ const electronAPI = { isDev: process.env.NODE_ENV === 'development' || !!process.env.VITE_DEV_SERVER_URL, }; +const yinianAPI = { + auth: { + createImageCaptcha: (randomStr?: string) => ipcRenderer.invoke('yinian:auth:createImageCaptcha', randomStr), + restoreSession: () => ipcRenderer.invoke('yinian:auth:restoreSession'), + getSessionState: () => ipcRenderer.invoke('yinian:auth:getSessionState'), + loginWithSms: (input: YinianLoginWithSmsInput) => ipcRenderer.invoke('yinian:auth:loginWithSms', input), + loginWithPassword: (input: YinianLoginWithPasswordInput) => ipcRenderer.invoke('yinian:auth:loginWithPassword', input), + logout: () => ipcRenderer.invoke('yinian:auth:logout'), + getSavedCredentials: () => ipcRenderer.invoke('yinian:auth:getSavedCredentials'), + saveCredentials: (input: Pick) => ipcRenderer.invoke('yinian:auth:saveCredentials', input), + clearSavedCredentials: () => ipcRenderer.invoke('yinian:auth:clearSavedCredentials'), + }, + app: { + getServerStatus: () => ipcRenderer.invoke('yinian:server:status'), + getConfig: () => ipcRenderer.invoke('yinian:config:get'), + switchHotel: (hotelId: string) => ipcRenderer.invoke('yinian:hotel:switch', hotelId), + switchWorkspace: (workspaceId: string) => ipcRenderer.invoke('yinian:hotel:switch', workspaceId), + }, + skills: { + sync: () => ipcRenderer.invoke('yinian:skills:sync'), + listLocal: () => ipcRenderer.invoke('yinian:skills:listLocal'), + getRegistry: (hotelId?: string) => ipcRenderer.invoke('yinian:skills:getRegistry', hotelId), + }, +}; + // Expose the API to the renderer process contextBridge.exposeInMainWorld('electron', electronAPI); +contextBridge.exposeInMainWorld('yinian', yinianAPI); // Type declarations for the renderer process export type ElectronAPI = typeof electronAPI; +export type YinianAPI = typeof yinianAPI; diff --git a/electron/utils/agent-config.ts b/electron/utils/agent-config.ts index 525a5d3..49c1718 100644 --- a/electron/utils/agent-config.ts +++ b/electron/utils/agent-config.ts @@ -12,6 +12,25 @@ const MAIN_AGENT_ID = 'main'; const MAIN_AGENT_NAME = 'Main Agent'; const DEFAULT_ACCOUNT_ID = 'default'; const DEFAULT_WORKSPACE_PATH = '~/.openclaw/workspace'; +const CHANNEL_AGENT_LABELS: Record = { + wechat: '微信', + wecom: '企业微信', + dingtalk: '钉钉', + feishu: '飞书', + lark: '飞书', + telegram: 'Telegram', + whatsapp: 'WhatsApp', + discord: 'Discord', + signal: 'Signal', + imessage: 'iMessage', + matrix: 'Matrix', + line: 'LINE', + msteams: 'Teams', + googlechat: 'Google Chat', + mattermost: 'Mattermost', + qqbot: 'QQ', + agentbus: 'AgentBus', +}; const AGENT_BOOTSTRAP_FILES = [ 'AGENTS.md', 'SOUL.md', @@ -144,6 +163,30 @@ function slugifyAgentId(name: string): string { return normalized; } +function normalizeChannelAgentIdSegment(value: string): string { + const normalized = value + .trim() + .toLowerCase() + .replace(/[^a-z0-9_-]+/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, '') + .slice(0, 48); + return normalized || 'default'; +} + +function buildChannelAgentIdentity(channelType: string, accountId: string): { id: string; name: string } { + const uiChannelType = toUiChannelType(channelType); + const channelSegment = normalizeChannelAgentIdSegment(uiChannelType); + const accountSegment = normalizeChannelAgentIdSegment(accountId || DEFAULT_ACCOUNT_ID); + const label = CHANNEL_AGENT_LABELS[uiChannelType] ?? uiChannelType; + const isDefaultAccount = accountSegment === DEFAULT_ACCOUNT_ID; + + return { + id: isDefaultAccount ? `channel-${channelSegment}` : `channel-${channelSegment}-${accountSegment}`, + name: isDefaultAccount ? `${label}助手` : `${label}助手 ${accountId}`, + }; +} + async function fileExists(path: string): Promise { try { await access(path, constants.F_OK); @@ -775,6 +818,54 @@ export async function assignChannelAccountToAgent( }); } +export async function ensureChannelAgentForAccount( + channelType: string, + accountId = DEFAULT_ACCOUNT_ID, +): Promise { + return withConfigLock(async () => { + const normalizedAccountId = accountId.trim() || DEFAULT_ACCOUNT_ID; + const config = await readOpenClawConfig() as AgentConfigDocument; + const { agentsConfig, entries, syntheticMain } = normalizeAgentsConfig(config); + const identity = buildChannelAgentIdentity(channelType, normalizedAccountId); + const nextEntries = syntheticMain + ? [createImplicitMainEntry(config), ...entries.filter((_, index) => index > 0)] + : [...entries]; + + let agentEntry = nextEntries.find((entry) => entry.id === identity.id); + if (!agentEntry) { + agentEntry = { + id: identity.id, + name: identity.name, + workspace: `~/.openclaw/workspace-${identity.id}`, + agentDir: getDefaultAgentDirPath(identity.id), + }; + nextEntries.push(agentEntry); + await provisionAgentFilesystem(config, agentEntry, { inheritWorkspace: true }); + logger.info('Created channel agent config entry', { + agentId: identity.id, + channelType, + accountId: normalizedAccountId, + }); + } else if (!agentEntry.name) { + agentEntry.name = identity.name; + } + + config.agents = { + ...agentsConfig, + list: nextEntries, + }; + config.bindings = upsertBindingsForChannel(config.bindings, channelType, identity.id, normalizedAccountId); + + await writeOpenClawConfig(config); + logger.info('Ensured channel agent binding', { + agentId: identity.id, + channelType, + accountId: normalizedAccountId, + }); + return buildSnapshotFromConfig(config); + }); +} + export async function clearChannelBinding(channelType: string, accountId?: string): Promise { return withConfigLock(async () => { const config = await readOpenClawConfig() as AgentConfigDocument; diff --git a/electron/utils/channel-config.ts b/electron/utils/channel-config.ts index 0870783..85f42aa 100644 --- a/electron/utils/channel-config.ts +++ b/electron/utils/channel-config.ts @@ -1112,7 +1112,26 @@ export function listConfiguredChannelAccountsFromConfig(config: OpenClawConfig): const accounts = getChannelAccountsMap(section); const accountIds = accounts - ? Object.keys(accounts).filter((accountId) => accountId.trim().length > 0) + ? Object.keys(accounts).filter((accountId) => { + if (!accountId.trim()) return false; + if (channelType !== 'agentbus' || accountId !== DEFAULT_ACCOUNT_ID) return true; + const configuredDefault = typeof section.defaultAccount === 'string' ? section.defaultAccount.trim() : ''; + const accountConfig = accounts[accountId]; + // AgentBus stores legacy top-level connection fields and may mirror a + // partial `accounts.default` object during generic edits. Treat that + // mirror as display/routing noise unless it is explicitly the default + // account and contains the fields a real AgentBus account needs. + if (configuredDefault && configuredDefault !== DEFAULT_ACCOUNT_ID) return false; + return Boolean( + accountConfig + && typeof accountConfig.id === 'string' + && accountConfig.id.trim() + && typeof accountConfig.agentId === 'string' + && accountConfig.agentId.trim() + && typeof accountConfig.wsUrl === 'string' + && accountConfig.wsUrl.trim(), + ); + }) : []; let defaultAccountId = typeof section.defaultAccount === 'string' && section.defaultAccount.trim() diff --git a/electron/utils/device-identity.ts b/electron/utils/device-identity.ts index de1a2c0..30156f8 100644 --- a/electron/utils/device-identity.ts +++ b/electron/utils/device-identity.ts @@ -28,7 +28,9 @@ export interface DeviceAuthPayloadParams { signedAtMs: number; token?: string | null; nonce?: string | null; - version?: 'v1' | 'v2'; + version?: 'v1' | 'v2' | 'v3'; + platform?: string | null; + deviceFamily?: string | null; } const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex'); @@ -143,5 +145,10 @@ export function buildDeviceAuthPayload(params: DeviceAuthPayloadParams): string token, ]; if (version === 'v2') base.push(params.nonce ?? ''); + if (version === 'v3') { + base.push(params.nonce ?? ''); + base.push(params.platform?.trim() ?? ''); + base.push(params.deviceFamily?.trim() ?? ''); + } return base.join('|'); } diff --git a/electron/utils/paths.ts b/electron/utils/paths.ts index 428fc8c..f8bacee 100644 --- a/electron/utils/paths.ts +++ b/electron/utils/paths.ts @@ -3,13 +3,25 @@ * Cross-platform path resolution helpers */ import { createRequire } from 'node:module'; -import { join } from 'path'; +import { dirname, isAbsolute, join, normalize } from 'path'; import { homedir } from 'os'; -import { existsSync, mkdirSync, readFileSync, realpathSync } from 'fs'; +import { cpSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync } from 'fs'; const require = createRequire(import.meta.url); type ElectronAppLike = Pick; +type OpenClawRuntimeSource = 'dev' | 'external' | 'managed' | 'bundled' | 'missing'; + +type OpenClawRuntimeResolution = { + dir: string; + source: OpenClawRuntimeSource; + version?: string; + bundledDir?: string; + managedDir?: string; + installedFromBundled?: boolean; +}; + +let cachedOpenClawRuntime: OpenClawRuntimeResolution | null = null; export { quoteForCmd, @@ -91,6 +103,218 @@ export function ensureDir(dir: string): void { } } +function fsPath(filePath: string): string { + if (process.platform !== 'win32') return filePath; + if (!filePath) return filePath; + if (filePath.startsWith('\\\\?\\')) return filePath; + const windowsPath = filePath.replace(/\//g, '\\'); + if (!isAbsolute(windowsPath)) return windowsPath; + if (windowsPath.startsWith('\\\\')) { + return `\\\\?\\UNC\\${windowsPath.slice(2)}`; + } + return `\\\\?\\${windowsPath}`; +} + +function normalizeCandidatePath(dir: string): string { + return normalize(expandPath(dir.trim())); +} + +function readOpenClawVersion(dir: string): string | undefined { + try { + const pkgPath = join(dir, 'package.json'); + if (existsSync(fsPath(pkgPath))) { + const pkg = JSON.parse(readFileSync(fsPath(pkgPath), 'utf-8')) as { version?: string }; + return pkg.version; + } + } catch { + // Ignore version read errors. + } + return undefined; +} + +function isValidOpenClawPackageDir(dir: string): boolean { + return Boolean(dir) + && existsSync(fsPath(dir)) + && existsSync(fsPath(join(dir, 'package.json'))) + && existsSync(fsPath(join(dir, 'openclaw.mjs'))); +} + +function samePath(left: string, right: string): boolean { + try { + return realpathSync(fsPath(left)) === realpathSync(fsPath(right)); + } catch { + return normalize(left) === normalize(right); + } +} + +function logOpenClawRuntime(message: string, extra?: unknown): void { + try { + const { logger } = require('./logger') as typeof import('./logger'); + if (extra === undefined) { + logger.info(message); + } else { + logger.info(message, extra); + } + } catch { + // Logger may be unavailable in unit tests or non-Electron contexts. + } +} + +function getBundledOpenClawDir(): string { + if (getElectronApp().isPackaged) { + return join(process.resourcesPath, 'openclaw'); + } + return join(__dirname, '../../node_modules/openclaw'); +} + +function getManagedOpenClawDir(): string { + return join(getOpenClawConfigDir(), 'runtime', 'openclaw'); +} + +function getEnvOpenClawCandidates(): string[] { + return [ + process.env.YINIAN_OPENCLAW_DIR, + process.env.CLAWX_OPENCLAW_DIR, + process.env.OPENCLAW_DIR, + ].filter((value): value is string => Boolean(value?.trim())); +} + +function getStandardOpenClawCandidates(): string[] { + const home = homedir(); + const candidates = [ + join(home, '.openclaw', 'openclaw'), + join(home, '.openclaw', 'node_modules', 'openclaw'), + join(home, '.npm-global', 'lib', 'node_modules', 'openclaw'), + join(home, '.local', 'lib', 'node_modules', 'openclaw'), + ]; + + if (process.platform === 'darwin') { + candidates.push( + '/usr/local/lib/node_modules/openclaw', + '/opt/homebrew/lib/node_modules/openclaw', + ); + } else if (process.platform === 'linux') { + candidates.push( + '/usr/local/lib/node_modules/openclaw', + '/usr/lib/node_modules/openclaw', + ); + } else if (process.platform === 'win32') { + const appData = process.env.APPDATA; + const programFiles = process.env.ProgramFiles; + const programFilesX86 = process.env['ProgramFiles(x86)']; + if (appData) candidates.push(join(appData, 'npm', 'node_modules', 'openclaw')); + if (programFiles) candidates.push(join(programFiles, 'nodejs', 'node_modules', 'openclaw')); + if (programFilesX86) candidates.push(join(programFilesX86, 'nodejs', 'node_modules', 'openclaw')); + } + + return candidates; +} + +function findExternalOpenClawDir(excludedDirs: string[]): string | null { + const seen = new Set(); + const candidates = [...getEnvOpenClawCandidates(), ...getStandardOpenClawCandidates()]; + + for (const rawCandidate of candidates) { + const candidate = normalizeCandidatePath(rawCandidate); + if (seen.has(candidate)) continue; + seen.add(candidate); + if (excludedDirs.some((excluded) => samePath(candidate, excluded))) continue; + if (isValidOpenClawPackageDir(candidate)) return candidate; + } + + return null; +} + +function installBundledOpenClawToManagedRuntime(bundledDir: string, managedDir: string): boolean { + if (!isValidOpenClawPackageDir(bundledDir)) return false; + + const bundledVersion = readOpenClawVersion(bundledDir); + const managedVersion = isValidOpenClawPackageDir(managedDir) + ? readOpenClawVersion(managedDir) + : undefined; + + if (managedVersion && bundledVersion && managedVersion === bundledVersion) { + return false; + } + + const tempDir = `${managedDir}.tmp-${Date.now()}`; + rmSync(fsPath(tempDir), { recursive: true, force: true }); + mkdirSync(fsPath(dirname(tempDir)), { recursive: true }); + cpSync(fsPath(bundledDir), fsPath(tempDir), { recursive: true, dereference: true }); + rmSync(fsPath(managedDir), { recursive: true, force: true }); + cpSync(fsPath(tempDir), fsPath(managedDir), { recursive: true, dereference: true }); + rmSync(fsPath(tempDir), { recursive: true, force: true }); + return true; +} + +function resolveOpenClawRuntime(): OpenClawRuntimeResolution { + if (cachedOpenClawRuntime) return cachedOpenClawRuntime; + + const app = getElectronApp(); + const bundledDir = getBundledOpenClawDir(); + const managedDir = getManagedOpenClawDir(); + + if (!app.isPackaged) { + cachedOpenClawRuntime = { + dir: bundledDir, + source: isValidOpenClawPackageDir(bundledDir) ? 'dev' : 'missing', + version: readOpenClawVersion(bundledDir), + bundledDir, + managedDir, + }; + return cachedOpenClawRuntime; + } + + const externalDir = findExternalOpenClawDir([bundledDir, managedDir]); + if (externalDir) { + cachedOpenClawRuntime = { + dir: externalDir, + source: 'external', + version: readOpenClawVersion(externalDir), + bundledDir, + managedDir, + }; + logOpenClawRuntime('[openclaw-runtime] Using existing OpenClaw installation', cachedOpenClawRuntime); + return cachedOpenClawRuntime; + } + + let installedFromBundled = false; + if (isValidOpenClawPackageDir(bundledDir)) { + try { + installedFromBundled = installBundledOpenClawToManagedRuntime(bundledDir, managedDir); + } catch (error) { + logOpenClawRuntime('[openclaw-runtime] Failed to install bundled OpenClaw runtime, falling back to bundled resources', error); + } + } + + if (isValidOpenClawPackageDir(managedDir)) { + cachedOpenClawRuntime = { + dir: managedDir, + source: 'managed', + version: readOpenClawVersion(managedDir), + bundledDir, + managedDir, + installedFromBundled, + }; + logOpenClawRuntime( + installedFromBundled + ? '[openclaw-runtime] Installed bundled OpenClaw runtime' + : '[openclaw-runtime] Using managed OpenClaw runtime', + cachedOpenClawRuntime, + ); + return cachedOpenClawRuntime; + } + + cachedOpenClawRuntime = { + dir: bundledDir, + source: isValidOpenClawPackageDir(bundledDir) ? 'bundled' : 'missing', + version: readOpenClawVersion(bundledDir), + bundledDir, + managedDir, + }; + return cachedOpenClawRuntime; +} + /** * Get resources directory (for bundled assets) */ @@ -109,16 +333,17 @@ export function getPreloadPath(): string { } /** - * Get OpenClaw package directory - * - Production (packaged): from resources/openclaw (copied by electron-builder extraResources) - * - Development: from node_modules/openclaw + * Get OpenClaw package directory. + * + * Runtime resolution policy: + * 1. Development uses workspace node_modules/openclaw. + * 2. Packaged builds first adapt an existing user/system OpenClaw package. + * 3. If none exists, copy the bundled OpenClaw package into + * ~/.openclaw/runtime/openclaw and run from there. + * 4. If the managed install fails, fall back to packaged resources/openclaw. */ export function getOpenClawDir(): string { - if (getElectronApp().isPackaged) { - return join(process.resourcesPath, 'openclaw'); - } - // Development: use node_modules/openclaw - return join(__dirname, '../../node_modules/openclaw'); + return resolveOpenClawRuntime().dir; } /** @@ -188,29 +413,26 @@ export interface OpenClawStatus { entryPath: string; dir: string; version?: string; + source?: OpenClawRuntimeSource; + bundledDir?: string; + managedDir?: string; + installedFromBundled?: boolean; } export function getOpenClawStatus(): OpenClawStatus { - const dir = getOpenClawDir(); - let version: string | undefined; - - // Try to read version from package.json - try { - const pkgPath = join(dir, 'package.json'); - if (existsSync(pkgPath)) { - const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); - version = pkg.version; - } - } catch { - // Ignore version read errors - } + const runtime = resolveOpenClawRuntime(); + const dir = runtime.dir; const status: OpenClawStatus = { packageExists: isOpenClawPresent(), isBuilt: isOpenClawBuilt(), entryPath: getOpenClawEntryPath(), dir, - version, + version: runtime.version, + source: runtime.source, + bundledDir: runtime.bundledDir, + managedDir: runtime.managedDir, + installedFromBundled: runtime.installedFromBundled, }; try { diff --git a/electron/utils/plugin-install.ts b/electron/utils/plugin-install.ts index 6c294b0..0e84842 100644 --- a/electron/utils/plugin-install.ts +++ b/electron/utils/plugin-install.ts @@ -483,12 +483,15 @@ export function buildCandidateSources(pluginDirName: string): string[] { return app.isPackaged ? [ join(process.resourcesPath, 'openclaw-plugins', pluginDirName), + join(process.resourcesPath, 'resources', 'openclaw-plugins', pluginDirName), join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', pluginDirName), join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', pluginDirName), ] : [ join(app.getAppPath(), 'build', 'openclaw-plugins', pluginDirName), + join(app.getAppPath(), 'resources', 'openclaw-plugins', pluginDirName), join(process.cwd(), 'build', 'openclaw-plugins', pluginDirName), + join(process.cwd(), 'resources', 'openclaw-plugins', pluginDirName), join(__dirname, '../../build/openclaw-plugins', pluginDirName), ]; } @@ -517,6 +520,10 @@ export function ensureWeChatPluginInstalled(): { installed: boolean; warning?: s return ensurePluginInstalled('openclaw-weixin', buildCandidateSources('openclaw-weixin'), 'WeChat'); } +export function ensureCloudSyncPluginInstalled(): { installed: boolean; warning?: string } { + return ensurePluginInstalled('cloud-sync', buildCandidateSources('cloud-sync'), 'Cloud Sync'); +} + // ── Bulk startup installer ─────────────────────────────────────────────────── /** @@ -528,6 +535,7 @@ const ALL_BUNDLED_PLUGINS = [ { fn: ensureFeishuPluginInstalled, label: 'Feishu' }, { fn: ensureWeChatPluginInstalled, label: 'WeChat' }, + { fn: ensureCloudSyncPluginInstalled, label: 'Cloud Sync' }, ] as const; /** diff --git a/electron/utils/wechat-login.ts b/electron/utils/wechat-login.ts index 743b37c..0e4b00b 100644 --- a/electron/utils/wechat-login.ts +++ b/electron/utils/wechat-login.ts @@ -334,6 +334,27 @@ export async function saveWeChatAccountState(rawAccountId: string, payload: { return accountId; } +export async function listWeChatAccountAliases(): Promise> { + const accountIds = await readAccountIndex(); + const aliases: Array<{ accountId: string; userId: string }> = []; + + for (const accountId of accountIds) { + try { + const filePath = join(WECHAT_ACCOUNTS_DIR, `${normalizeOpenClawAccountId(accountId)}.json`); + const raw = await readFile(filePath, 'utf-8'); + const parsed = JSON.parse(raw) as { userId?: unknown }; + const userId = typeof parsed.userId === 'string' ? parsed.userId.trim() : ''; + if (userId) { + aliases.push({ accountId: normalizeOpenClawAccountId(accountId), userId }); + } + } catch { + // Ignore malformed or removed account state files. + } + } + + return aliases; +} + export async function startWeChatLoginSession(options: { sessionKey?: string; accountId?: string; diff --git a/electron/yinian/control-plane.ts b/electron/yinian/control-plane.ts new file mode 100644 index 0000000..86e87ef --- /dev/null +++ b/electron/yinian/control-plane.ts @@ -0,0 +1,54 @@ +import type { + YinianAuthSession, + YinianConfigSnapshot, + YinianImageCaptcha, + YinianLoginWithPasswordInput, + YinianLoginWithSmsInput, + YinianLocalSkill, + YinianServerStatus, + YinianSessionState, + YinianSkillRegistry, + YinianSkillSyncResult, +} from '../../shared/yinian'; +import { HttpYinianControlPlane } from './http-control-plane'; +import { MockYinianControlPlane } from './mock-control-plane'; +import { getYinianStorage } from './storage'; +import { UnconfiguredYinianControlPlane } from './unconfigured-control-plane'; + +const DEFAULT_YINIAN_API_BASE_URL = 'https://onefeel.brother7.cn/ingress'; + +export interface YinianControlPlane { + getServerStatus(): Promise; + createImageCaptcha(randomStr?: string): Promise; + restoreSession(): Promise; + getSessionState(): Promise; + loginWithSms(input: YinianLoginWithSmsInput): Promise; + loginWithPassword(input: YinianLoginWithPasswordInput): Promise; + logout(): Promise; + switchHotel(hotelId: string): Promise; + getConfigSnapshot(): Promise; + syncSkills(): Promise; + listLocalSkills(): Promise; + getSkillRegistry(hotelId?: string): Promise | undefined>; +} + +let controlPlane: YinianControlPlane | null = null; + +export function getYinianControlPlane(): YinianControlPlane { + if (controlPlane) return controlPlane; + + const apiBaseUrl = process.env.YINIAN_API_BASE_URL?.trim() || DEFAULT_YINIAN_API_BASE_URL; + const explicitMode = process.env.YINIAN_CONTROL_PLANE_MODE?.trim(); + const shouldUseMock = explicitMode === 'mock' || process.env.CLAWX_E2E === '1'; + controlPlane = shouldUseMock + ? new MockYinianControlPlane({ storage: getYinianStorage() }) + : apiBaseUrl + ? new HttpYinianControlPlane({ apiBaseUrl, storage: getYinianStorage() }) + : new UnconfiguredYinianControlPlane(); + + return controlPlane; +} + +export function resetYinianControlPlaneForTests(next?: YinianControlPlane): void { + controlPlane = next ?? null; +} diff --git a/electron/yinian/http-control-plane.ts b/electron/yinian/http-control-plane.ts new file mode 100644 index 0000000..9ff1915 --- /dev/null +++ b/electron/yinian/http-control-plane.ts @@ -0,0 +1,804 @@ +import type { + YinianAuthSession, + YinianConfigSnapshot, + YinianHotel, + YinianImageCaptcha, + YinianNotificationChannel, + YinianSkillEntitlement, + YinianLoginWithPasswordInput, + YinianLoginWithSmsInput, + YinianLocalSkill, + YinianServerStatus, + YinianSessionState, + YinianSkillSyncResult, + YinianUser, +} from '../../shared/yinian'; +import { randomUUID } from 'node:crypto'; +import type { YinianControlPlane } from './control-plane'; +import { getYinianStorage, type YinianStorage } from './storage'; + +interface HttpYinianControlPlaneOptions { + apiBaseUrl: string; + storage?: YinianStorage; +} + +type JsonObject = Record; + +const DEFAULT_ACCESS_TOKEN_TTL_MS = 30 * 60 * 1000; +const SKILL_CATEGORIES: YinianSkillEntitlement['category'][] = [ + 'ota-monitoring', + 'reporting', + 'guest-comm', + 'ops-automation', +]; +const SKILL_TRIGGERS: Array = [ + 'manual', + 'scheduled', + 'webhook', + 'reply', +]; +const DEFAULT_OAUTH_CLIENT_ID = 'customPC'; +const DEFAULT_OAUTH_CLIENT_SECRET = 'customPC'; +const DEFAULT_OAUTH_SCOPE = 'server'; +const DEFAULT_CONFIG_SYNC_PATH = '/config/sync'; +const DEFAULT_SKILLS_MANIFEST_PATH = '/skills/manifest'; + +export class HttpYinianControlPlane implements YinianControlPlane { + private readonly apiBaseUrl: string; + private readonly storage: YinianStorage; + private accessToken: string | null = null; + private refreshToken: string | null = null; + private currentHotelId: string | null = null; + private localSkills: YinianLocalSkill[] = []; + + constructor(options: HttpYinianControlPlaneOptions) { + this.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, ''); + this.storage = options.storage ?? getYinianStorage(); + } + + async getServerStatus(): Promise { + const checkedAt = Date.now(); + try { + const health = await this.request('/health', { auth: false }); + return { + mode: 'http', + apiBaseUrl: this.apiBaseUrl, + reachable: true, + checkedAt, + serverTime: readNumber(health, 'serverTime', 'server_time'), + version: readString(health, 'version', 'appVersion', 'app_version'), + message: readString(health, 'message', 'status') ?? '服务端连接正常', + }; + } catch (error) { + return { + mode: 'http', + apiBaseUrl: this.apiBaseUrl, + reachable: false, + checkedAt, + message: error instanceof Error ? error.message : String(error), + }; + } + } + + async createImageCaptcha(randomStr = createRandomString()): Promise { + const path = `/auth/code/image?randomStr=${encodeURIComponent(randomStr)}`; + const requestUrl = `${this.apiBaseUrl}${path}`; + const response = await fetch(requestUrl, { + method: 'GET', + headers: { + 'X-YINIAN-App-Version': '0.1.0', + }, + }).catch((error: unknown) => { + throw new Error(describeNetworkError(error, requestUrl)); + }); + + if (!response.ok) { + const data = await response.json().catch(() => ({})) as JsonObject; + const message = readErrorMessage(data) ?? `YINIAN API request failed: ${response.status}`; + throw new Error(message); + } + + const contentType = response.headers.get('content-type') ?? ''; + if (contentType.includes('application/json')) { + const data = await response.json().catch(() => ({})) as JsonObject; + return normalizeImageCaptcha(data, randomStr); + } + + const imageBytes = Buffer.from(await response.arrayBuffer()); + return { + randomStr, + imageBase64: imageBytes.toString('base64'), + mimeType: contentType.split(';')[0] || 'image/png', + raw: { + contentType, + size: imageBytes.byteLength, + }, + }; + } + + async restoreSession(): Promise { + const persisted = await this.storage.getSession(); + if (!persisted || persisted.mode !== 'http') { + return { authenticated: false }; + } + + this.refreshToken = persisted.refreshToken ?? null; + this.currentHotelId = persisted.currentHotelId; + if (this.refreshToken) { + try { + return await this.refreshSession(); + } catch { + await this.storage.clearSession(); + this.refreshToken = null; + this.currentHotelId = null; + return { authenticated: false }; + } + } + + // Access tokens intentionally remain memory-only. Until refresh-token + // exchange is implemented, restore returns the persisted tenant snapshot so + // the shell can recover UI context without exposing provider/model keys. + return { + authenticated: true, + user: persisted.user, + hotels: persisted.hotels, + currentHotelId: persisted.currentHotelId, + accessTokenExpiresAt: persisted.accessTokenExpiresAt, + }; + } + + async getSessionState(): Promise { + if (!this.accessToken) return this.restoreSession(); + + const persisted = await this.storage.getSession(); + if (persisted?.mode === 'http') { + return { + authenticated: true, + user: persisted.user, + hotels: persisted.hotels, + currentHotelId: persisted.currentHotelId, + accessTokenExpiresAt: persisted.accessTokenExpiresAt, + }; + } + + return { authenticated: false }; + } + + async loginWithSms(input: YinianLoginWithSmsInput): Promise { + void input; + throw new Error('当前组织空间端仅支持账号密码登录'); + } + + async loginWithPassword(input: YinianLoginWithPasswordInput): Promise { + const response = await this.requestOAuthToken({ + grant_type: 'password', + username: input.account, + account: input.account, + password: input.password, + code: input.captchaCode, + captchaCode: input.captchaCode, + captcha_code: input.captchaCode, + randomStr: input.randomStr, + random_str: input.randomStr, + }); + + return this.applyLoginResponse(response); + } + + async logout(): Promise { + const hotelId = this.currentHotelId; + this.accessToken = null; + this.refreshToken = null; + this.currentHotelId = null; + if (hotelId) { + await this.storage.clearConfig(hotelId); + await this.storage.clearSkillRegistry(hotelId); + } + await this.storage.clearSession(); + this.localSkills = []; + return { authenticated: false }; + } + + async switchHotel(hotelId: string): Promise { + const session = await this.getSessionState(); + if (!session.authenticated) throw new Error('请先登录'); + const hotel = session.hotels.find((item) => item.id === hotelId); + if (!hotel) throw new Error('工作空间不存在或未开通'); + this.currentHotelId = hotelId; + const nextSession = { ...session, currentHotelId: hotelId }; + await this.persistSession(nextSession); + const registry = await this.storage.getSkillRegistry(hotelId); + this.localSkills = registry && 'skills' in registry ? registry.skills : []; + return nextSession; + } + + async getConfigSnapshot(): Promise { + const session = await this.getSessionState(); + if (!session.authenticated) throw new Error('请先登录'); + + const hotelId = session.currentHotelId; + const configPath = buildScopedEndpoint( + process.env.YINIAN_CONFIG_SYNC_PATH?.trim() || DEFAULT_CONFIG_SYNC_PATH, + hotelId, + ); + let response: JsonObject; + try { + response = await this.request(configPath); + } catch (error) { + if (isMissingServerResourceError(error)) { + const snapshot = createDefaultConfigSnapshot(session); + await this.storage.setConfig(snapshot); + return snapshot; + } + throw error; + } + const hotel = normalizeHotel(response.hotel) ?? session.hotels.find((item) => item.id === hotelId) ?? session.hotels[0]; + + const snapshot = { + serverTime: readNumber(response, 'serverTime', 'server_time') ?? Date.now(), + user: session.user, + hotel, + hotels: session.hotels, + entitlements: normalizeEntitlements(readManifestItems(response, 'entitlements')), + notificationChannels: normalizeNotificationChannels(readArray(response, 'notificationChannels', 'notification_channels')), + featureFlags: normalizeBooleanRecord(readObject(response, 'featureFlags', 'feature_flags')), + uiPolicy: normalizeUiPolicy(readObject(response, 'uiPolicy', 'ui_policy')), + }; + await this.storage.setConfig(snapshot); + return snapshot; + } + + async syncSkills(): Promise { + const session = await this.getSessionState(); + if (!session.authenticated) throw new Error('请先登录'); + + const manifestPath = buildScopedEndpoint( + process.env.YINIAN_SKILLS_MANIFEST_PATH?.trim() || DEFAULT_SKILLS_MANIFEST_PATH, + session.currentHotelId, + ); + const now = Date.now(); + let response: JsonObject; + try { + response = await this.request(manifestPath); + } catch (error) { + if (isMissingServerResourceError(error)) { + this.localSkills = []; + await this.storage.setSkillRegistry({ + hotelId: session.currentHotelId, + updatedAt: now, + skills: [], + }); + return { + hotelId: session.currentHotelId, + syncedAt: now, + skills: [], + }; + } + throw error; + } + const skills = readManifestItems(response).filter(isObject).map((item) => normalizeLocalSkill(item, now)); + + const previousRegistry = await this.storage.getSkillRegistry(session.currentHotelId); + const previousSkills = previousRegistry && 'skills' in previousRegistry ? previousRegistry.skills : []; + this.localSkills = skills.map((skill) => { + const previous = previousSkills.find((item) => item.skillId === skill.skillId); + return { + ...skill, + installedAt: previous?.installedAt ?? skill.installedAt, + status: !skill.enabled ? 'disabled' : previous?.version === skill.version ? 'skipped' : previous ? 'updated' : skill.status, + }; + }); + await this.storage.setSkillRegistry({ + hotelId: session.currentHotelId, + updatedAt: now, + skills: this.localSkills, + }); + return { + hotelId: session.currentHotelId, + syncedAt: now, + skills: this.localSkills, + }; + } + + async listLocalSkills(): Promise { + const session = await this.getSessionState(); + if (session.authenticated) { + const registry = await this.storage.getSkillRegistry(session.currentHotelId); + if (registry && 'skills' in registry) { + this.localSkills = registry.skills; + } + } + return this.localSkills; + } + + async getSkillRegistry(hotelId?: string) { + return this.storage.getSkillRegistry(hotelId); + } + + private async applyLoginResponse(response: JsonObject): Promise { + this.accessToken = readString(response, 'accessToken', 'access_token'); + this.refreshToken = readString(response, 'refreshToken', 'refresh_token'); + if (!this.accessToken) throw new Error('服务端未返回 access token'); + + const session = this.normalizeSessionFromPayload(response) ?? await this.getSessionState(); + if (!session.authenticated) throw new Error('登录成功但未获取到工作空间信息'); + this.currentHotelId = session.currentHotelId; + await this.persistSession(session); + return session; + } + + private async refreshSession(): Promise { + if (!this.refreshToken) return { authenticated: false }; + const response = await this.requestOAuthToken({ + grant_type: 'refresh_token', + refreshToken: this.refreshToken, + refresh_token: this.refreshToken, + }); + return this.applyLoginResponse(response); + } + + private async persistSession(session: YinianAuthSession): Promise { + await this.storage.setSession({ + mode: 'http', + user: session.user, + hotels: session.hotels, + currentHotelId: session.currentHotelId, + accessTokenExpiresAt: session.accessTokenExpiresAt, + refreshToken: this.refreshToken ?? undefined, + updatedAt: Date.now(), + }); + } + + private async requestOAuthToken(fields: Record): Promise { + const clientId = process.env.YINIAN_AUTH_CLIENT_ID?.trim() || DEFAULT_OAUTH_CLIENT_ID; + const headers = createOAuthHeaders(clientId); + const clientBodyFields = headers.Authorization + ? {} + : { clientId, client_id: clientId }; + return this.request('/auth/oauth2/token', { + method: 'POST', + auth: false, + form: { + scope: process.env.YINIAN_AUTH_SCOPE?.trim() || DEFAULT_OAUTH_SCOPE, + ...clientBodyFields, + ...fields, + }, + headers, + }); + } + + private normalizeSessionFromPayload(payload: JsonObject): YinianAuthSession | null { + const sessionPayload = readObject(payload, 'session') ?? payload; + const userValue = readObject(sessionPayload, 'user', 'user_info', 'userInfo'); + if (!userValue) return null; + + const user = normalizeUser(userValue); + const hotelsValue = readArray(sessionPayload, 'hotels', 'workspaces', 'businesses', 'tenants'); + const hotels = normalizeHotels(hotelsValue); + if (hotels.length === 0) { + hotels.push(createDefaultWorkspace(sessionPayload, userValue, user)); + } + const currentHotelId = readString(sessionPayload, 'currentHotelId', 'current_hotel_id', 'currentWorkspaceId', 'current_workspace_id', 'workspaceId', 'workspace_id') + ?? this.currentHotelId + ?? hotels[0]?.id; + if (!currentHotelId) return null; + + return { + authenticated: true, + user, + hotels, + currentHotelId, + accessTokenExpiresAt: readAccessTokenExpiresAt(sessionPayload), + }; + } + + private async request( + path: string, + options: { method?: string; body?: JsonObject; form?: Record; auth?: boolean; headers?: Record } = {}, + ): Promise { + const headers: Record = { + 'X-YINIAN-App-Version': '0.1.0', + ...(options.headers ?? {}), + }; + const body = options.form + ? createFormBody(options.form) + : options.body + ? JSON.stringify(options.body) + : undefined; + if (options.body && !headers['Content-Type']) { + headers['Content-Type'] = 'application/json'; + } + if (options.form && !headers['Content-Type']) { + headers['Content-Type'] = 'application/x-www-form-urlencoded'; + } + if (options.auth !== false && this.accessToken) { + headers.Authorization = `Bearer ${this.accessToken}`; + } + + const requestUrl = `${this.apiBaseUrl}${path}`; + const response = await fetch(requestUrl, { + method: options.method ?? 'GET', + headers, + body, + }).catch((error: unknown) => { + throw new Error(describeNetworkError(error, requestUrl)); + }); + + const data = await response.json().catch(() => ({})) as JsonObject; + if (!response.ok) { + const message = readErrorMessage(data) ?? `YINIAN API request failed: ${response.status}`; + throw new Error(message); + } + + const payload = unwrapApiPayload(data); + return payload as T; + } +} + +function createDevicePayload(): JsonObject { + return { + device_id: 'dev_local_electron', + platform: process.platform, + app_version: '0.1.0', + machine_name: '智念助手', + }; +} + +function describeNetworkError(error: unknown, requestUrl: string): string { + const hostname = safeHostname(requestUrl); + const code = readNestedErrorString(error, 'code'); + const syscall = readNestedErrorString(error, 'syscall'); + const detail = code ? `(${code}${syscall ? `/${syscall}` : ''})` : ''; + + if (code === 'ENOTFOUND') { + return `无法连接服务端:域名 ${hostname} 解析失败${detail}。请检查服务端地址、网络或 VPN。`; + } + if (code === 'ECONNREFUSED') { + return `无法连接服务端:${hostname} 拒绝连接${detail}。请确认服务端已启动并允许当前网络访问。`; + } + if (code === 'ETIMEDOUT' || code === 'UND_ERR_CONNECT_TIMEOUT') { + return `无法连接服务端:连接 ${hostname} 超时${detail}。请检查网络、代理或 VPN。`; + } + if (code === 'CERT_HAS_EXPIRED' || code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' || code === 'DEPTH_ZERO_SELF_SIGNED_CERT') { + return `无法连接服务端:${hostname} 的 HTTPS 证书校验失败${detail}。请检查证书配置。`; + } + + return `无法连接服务端:请求 ${hostname} 失败${detail}。请检查网络、代理、VPN 或服务端地址。`; +} + +function safeHostname(requestUrl: string): string { + try { + return new URL(requestUrl).hostname; + } catch { + return requestUrl; + } +} + +function readNestedErrorString(error: unknown, key: string): string | undefined { + const visited = new Set(); + let current: unknown = error; + while (isObject(current) && !visited.has(current)) { + visited.add(current); + const value = current[key]; + if (typeof value === 'string' && value.trim()) return value; + current = current.cause; + } + return undefined; +} + +function createOAuthHeaders(clientId: string): Record { + const explicitBasic = process.env.YINIAN_AUTH_BASIC?.trim(); + if (explicitBasic) { + return { Authorization: explicitBasic.startsWith('Basic ') ? explicitBasic : `Basic ${explicitBasic}` }; + } + + const clientSecret = process.env.YINIAN_AUTH_CLIENT_SECRET?.trim() || DEFAULT_OAUTH_CLIENT_SECRET; + if (!clientSecret) return {}; + return { + Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`, + }; +} + +function createFormBody(fields: Record): string { + const params = new URLSearchParams(); + for (const [key, value] of Object.entries(fields)) { + if (value == null || value === '') continue; + if (Array.isArray(value)) { + value.forEach((item) => params.append(key, item)); + } else { + params.append(key, value); + } + } + return params.toString(); +} + +function createRandomString(): string { + return randomUUID(); +} + +function buildScopedEndpoint(endpoint: string, workspaceId: string): string { + const [rawPath, rawQuery = ''] = endpoint.split('?'); + const path = rawPath + .replaceAll('{workspaceId}', encodeURIComponent(workspaceId)) + .replaceAll('{workspace_id}', encodeURIComponent(workspaceId)) + .replaceAll('{hotelId}', encodeURIComponent(workspaceId)) + .replaceAll('{hotel_id}', encodeURIComponent(workspaceId)) + .replaceAll('{tenantId}', encodeURIComponent(workspaceId)) + .replaceAll('{tenant_id}', encodeURIComponent(workspaceId)); + const params = new URLSearchParams(rawQuery); + const hasExplicitScope = endpoint.includes('{workspaceId}') + || endpoint.includes('{workspace_id}') + || endpoint.includes('{hotelId}') + || endpoint.includes('{hotel_id}') + || endpoint.includes('{tenantId}') + || endpoint.includes('{tenant_id}') + || params.has('workspaceId') + || params.has('workspace_id') + || params.has('hotelId') + || params.has('hotel_id') + || params.has('tenantId') + || params.has('tenant_id'); + + if (!hasExplicitScope) { + params.set('hotel_id', workspaceId); + } + + const query = params.toString(); + return query ? `${path}?${query}` : path; +} + +function normalizeImageCaptcha(value: JsonObject, fallbackRandomStr: string): YinianImageCaptcha { + const payload = unwrapApiPayload(value); + const image = readString(payload, 'image', 'img', 'captcha', 'captchaImage', 'captcha_image'); + const imageBase64 = readString(payload, 'imageBase64', 'image_base64', 'base64'); + return { + randomStr: readString(payload, 'randomStr', 'random_str', 'key') ?? fallbackRandomStr, + image, + imageBase64, + mimeType: readString(payload, 'mimeType', 'mime_type') ?? (imageBase64 ? 'image/png' : undefined), + raw: payload, + }; +} + +function normalizeUser(value: unknown): YinianUser { + const object = isObject(value) ? value : {}; + return { + id: readString(object, 'id', 'userId', 'user_id') ?? 'user_unknown', + name: readString(object, 'name', 'username', 'nickname') ?? '未命名用户', + phone: readString(object, 'phone'), + email: readString(object, 'email'), + avatar: readString(object, 'avatar'), + }; +} + +function createDefaultWorkspace(sessionPayload: JsonObject, userPayload: JsonObject, user: YinianUser): YinianHotel { + const tenantId = readString(userPayload, 'tenantId', 'tenant_id') + ?? readString(sessionPayload, 'tenantId', 'tenant_id') + ?? 'default'; + return { + id: `service_${tenantId}`, + name: readString(sessionPayload, 'tenantName', 'tenant_name', 'workspaceName', 'workspace_name') + ?? `${user.name}的组织空间`, + }; +} + +function createDefaultConfigSnapshot(session: YinianAuthSession): YinianConfigSnapshot { + return { + serverTime: Date.now(), + user: session.user, + hotel: session.hotels.find((item) => item.id === session.currentHotelId) ?? session.hotels[0], + hotels: session.hotels, + entitlements: [], + notificationChannels: [], + featureFlags: {}, + uiPolicy: { + defaultPage: 'today', + showAdvancedSettings: false, + }, + }; +} + +function isMissingServerResourceError(error: unknown): boolean { + return error instanceof Error && /No static resource|404|not found/i.test(error.message); +} + +function normalizeHotels(value: unknown): YinianHotel[] { + if (!Array.isArray(value)) return []; + return value.map(normalizeHotel).filter((hotel): hotel is YinianHotel => Boolean(hotel)); +} + +function normalizeHotel(value: unknown): YinianHotel | null { + if (!isObject(value)) return null; + const hotel: YinianHotel = { + id: readString(value, 'id') ?? readString(value, 'hotelId', 'hotel_id', 'workspaceId', 'workspace_id', 'tenantId', 'tenant_id') ?? 'hotel_unknown', + name: readString(value, 'name') ?? '未命名工作空间', + brand: readString(value, 'brand'), + }; + + return hotel; +} + +function normalizeLocalSkill(value: JsonObject, now: number): YinianLocalSkill { + const skillId = readString( + value, + 'skillId', + 'skill_id', + 'appId', + 'app_id', + 'applicationId', + 'application_id', + 'code', + 'skillCode', + 'skill_code', + 'id', + ) ?? 'unknown-skill'; + const version = readString(value, 'version', 'appVersion', 'app_version', 'currentVersion', 'current_version') ?? '0.0.0'; + const status = readString(value, 'status', 'state'); + const enabled = value.enabled !== false + && value.kill_switch !== true + && status !== 'disabled' + && status !== 'disable' + && status !== 'off'; + return { + skillId, + name: readString(value, 'name', 'appName', 'app_name', 'applicationName', 'application_name', 'title') ?? skillId, + version, + enabled, + installedAt: now, + lastSyncedAt: now, + status: enabled ? 'installed' : 'disabled', + source: 'nianxx', + bundleSha256: readString(value, 'bundleSha256', 'bundle_sha256', 'sha256', 'checksum', 'digest', 'bundleHash', 'bundle_hash'), + }; +} + +function normalizeEntitlements(value: unknown[] | undefined): YinianSkillEntitlement[] { + if (!value) return []; + return value.filter(isObject).map((item) => { + const skillId = readString(item, 'skillId', 'skill_id', 'appId', 'app_id', 'applicationId', 'application_id', 'code', 'id') ?? 'unknown-skill'; + const status = readString(item, 'status', 'state'); + return { + skillId, + name: readString(item, 'name', 'appName', 'app_name', 'applicationName', 'application_name', 'title') ?? skillId, + version: readString(item, 'version', 'appVersion', 'app_version', 'currentVersion', 'current_version') ?? '0.0.0', + enabled: item.enabled !== false && status !== 'disabled' && status !== 'disable' && status !== 'off', + category: normalizeSkillCategory(readString(item, 'category')), + triggers: normalizeSkillTriggers(readArray(item, 'triggers')), + lastRunAt: readNumber(item, 'lastRunAt', 'last_run_at'), + }; + }); +} + +function normalizeNotificationChannels(value: unknown[] | undefined): YinianNotificationChannel[] { + if (!value) return []; + return value.filter(isObject).map((item) => ({ + id: readString(item, 'id') ?? 'channel_unknown', + kind: readString(item, 'kind') ?? 'unknown', + label: readString(item, 'label') ?? '未命名通知通道', + recipient: readString(item, 'recipient') ?? '', + enabled: item.enabled !== false, + source: readString(item, 'source') === 'kernel' ? 'kernel' : 'nianxx', + })); +} + +function normalizeUiPolicy(value: JsonObject | undefined): YinianConfigSnapshot['uiPolicy'] { + const defaultPage = readString(value ?? {}, 'defaultPage', 'default_page'); + return { + defaultPage: defaultPage === 'chat' ? 'chat' : 'today', + showAdvancedSettings: readBoolean(value ?? {}, 'showAdvancedSettings', 'show_advanced_settings') ?? false, + }; +} + +function normalizeSkillCategory(value: string | undefined): YinianSkillEntitlement['category'] { + return SKILL_CATEGORIES.includes(value as YinianSkillEntitlement['category']) + ? value as YinianSkillEntitlement['category'] + : 'ops-automation'; +} + +function normalizeSkillTriggers(value: unknown[] | undefined): YinianSkillEntitlement['triggers'] { + if (!value) return []; + return value.filter((item): item is YinianSkillEntitlement['triggers'][number] => ( + typeof item === 'string' + && SKILL_TRIGGERS.includes(item as YinianSkillEntitlement['triggers'][number]) + )); +} + +function readAccessTokenExpiresAt(object: JsonObject): number { + return readNumber(object, 'accessTokenExpiresAt', 'access_token_expires_at', 'expiresAt', 'expires_at') + ?? readExpiresIn(object) + ?? (Date.now() + DEFAULT_ACCESS_TOKEN_TTL_MS); +} + +function readExpiresIn(object: JsonObject): number | undefined { + const expiresIn = readNumber(object, 'expiresIn', 'expires_in'); + if (!expiresIn) return undefined; + return Date.now() + expiresIn * 1000; +} + +function isObject(value: unknown): value is JsonObject { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function readString(object: JsonObject, ...keys: string[]): string | undefined { + for (const key of keys) { + const value = object[key]; + if (typeof value === 'string') return value; + } + return undefined; +} + +function readNumber(object: JsonObject, ...keys: string[]): number | undefined { + for (const key of keys) { + const value = object[key]; + if (typeof value === 'number') return value; + if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) return Number(value); + } + return undefined; +} + +function readBoolean(object: JsonObject, ...keys: string[]): boolean | undefined { + for (const key of keys) { + const value = object[key]; + if (typeof value === 'boolean') return value; + } + return undefined; +} + +function readObject(object: JsonObject, ...keys: string[]): JsonObject | undefined { + for (const key of keys) { + const value = object[key]; + if (isObject(value)) return value; + } + return undefined; +} + +function unwrapApiPayload(data: JsonObject): JsonObject { + const dataObject = readObject(data, 'data'); + if (dataObject) { + return { + ...data, + ...dataObject, + }; + } + return data; +} + +function readManifestItems(object: JsonObject, preferredKey?: string): unknown[] { + if (preferredKey) { + const preferred = readArray(object, preferredKey); + if (preferred) return preferred; + } + + const direct = readArray(object, 'skills', 'apps', 'applications', 'records', 'list', 'items', 'rows'); + if (direct) return direct; + + const data = object.data; + if (Array.isArray(data)) return data; + if (isObject(data)) { + return readManifestItems(data, preferredKey); + } + + return []; +} + +function readErrorMessage(data: JsonObject): string | undefined { + const errorObject = readObject(data, 'error'); + return readString(errorObject ?? {}, 'message', 'msg', 'detail') + ?? readString(data, 'message', 'msg', 'error_description') + ?? (typeof data.error === 'string' ? data.error : undefined); +} + +function readArray(object: JsonObject, ...keys: string[]): unknown[] | undefined { + for (const key of keys) { + const value = object[key]; + if (Array.isArray(value)) return value; + } + return undefined; +} + +function normalizeBooleanRecord(value: JsonObject | undefined): Record { + if (!value) return {}; + return Object.fromEntries( + Object.entries(value).filter((entry): entry is [string, boolean] => typeof entry[1] === 'boolean'), + ); +} diff --git a/electron/yinian/mock-control-plane.ts b/electron/yinian/mock-control-plane.ts new file mode 100644 index 0000000..38bf98f --- /dev/null +++ b/electron/yinian/mock-control-plane.ts @@ -0,0 +1,302 @@ +import type { + YinianAuthSession, + YinianConfigSnapshot, + YinianHotel, + YinianImageCaptcha, + YinianLoginWithPasswordInput, + YinianLoginWithSmsInput, + YinianLocalSkill, + YinianServerStatus, + YinianSessionState, + YinianSkillSyncResult, + YinianUser, +} from '../../shared/yinian'; +import type { YinianControlPlane } from './control-plane'; +import { getYinianStorage, type YinianStorage } from './storage'; + +const MOCK_USER: YinianUser = { + id: 'user_demo_manager', + name: '王管理员', + phone: '13800000000', + email: 'manager@yinian.local', +}; + +const MOCK_HOTELS: YinianHotel[] = [ + { + id: 'workspace_hangzhou_ops', + name: '智念企业组织空间', + brand: '智念', + }, + { + id: 'workspace_shanghai_growth', + name: '智念增长组织空间', + brand: '智念', + }, +]; + +function createSession(user: YinianUser = MOCK_USER, currentHotelId = MOCK_HOTELS[0].id): YinianAuthSession { + return { + authenticated: true, + user, + hotels: MOCK_HOTELS, + currentHotelId, + accessTokenExpiresAt: Date.now() + 60 * 60 * 1000, + }; +} + +interface MockYinianControlPlaneOptions { + storage?: YinianStorage; +} + +export class MockYinianControlPlane implements YinianControlPlane { + private readonly storage: YinianStorage; + private currentSession: YinianSessionState = { authenticated: false }; + private localSkills: YinianLocalSkill[] = []; + + constructor(options: MockYinianControlPlaneOptions = {}) { + this.storage = options.storage ?? getYinianStorage(); + } + + async getServerStatus(): Promise { + return { + mode: 'mock', + reachable: true, + checkedAt: Date.now(), + message: '当前使用本地演示数据', + }; + } + + async createImageCaptcha(randomStr = crypto.randomUUID()): Promise { + return { + randomStr, + image: '', + mimeType: 'image/svg+xml', + raw: { mock: true }, + }; + } + + async restoreSession(): Promise { + const persisted = await this.storage.getSession(); + if (!persisted || persisted.mode !== 'mock') { + this.currentSession = { authenticated: false }; + return this.currentSession; + } + + this.currentSession = { + authenticated: true, + user: persisted.user, + hotels: persisted.hotels, + currentHotelId: persisted.currentHotelId, + accessTokenExpiresAt: persisted.accessTokenExpiresAt, + }; + const registry = await this.storage.getSkillRegistry(persisted.currentHotelId); + this.localSkills = registry && 'skills' in registry ? registry.skills : []; + return this.currentSession; + } + + async getSessionState(): Promise { + if (!this.currentSession.authenticated) { + return this.restoreSession(); + } + return this.currentSession; + } + + async loginWithSms(input: YinianLoginWithSmsInput): Promise { + const phone = input.phone.trim(); + if (!phone || !input.code.trim()) { + throw new Error('手机号和验证码不能为空'); + } + + this.currentSession = createSession({ ...MOCK_USER, phone }); + await this.persistCurrentSession(); + return this.currentSession; + } + + async loginWithPassword(input: YinianLoginWithPasswordInput): Promise { + const account = input.account.trim(); + if (!account || !input.password.trim()) { + throw new Error('账号和密码不能为空'); + } + + this.currentSession = createSession({ + ...MOCK_USER, + name: account === 'admin' ? 'NIANXX 实施' : MOCK_USER.name, + email: account.includes('@') ? account : MOCK_USER.email, + }); + await this.persistCurrentSession(); + return this.currentSession; + } + + async logout(): Promise { + if (this.currentSession.authenticated) { + await this.storage.clearConfig(this.currentSession.currentHotelId); + await this.storage.clearSkillRegistry(this.currentSession.currentHotelId); + } + await this.storage.clearSession(); + this.currentSession = { authenticated: false }; + this.localSkills = []; + return this.currentSession; + } + + async switchHotel(hotelId: string): Promise { + if (!this.currentSession.authenticated) { + throw new Error('请先登录'); + } + + const hotel = MOCK_HOTELS.find((item) => item.id === hotelId); + if (!hotel) { + throw new Error('工作空间不存在或未开通'); + } + + this.currentSession = { + ...this.currentSession, + currentHotelId: hotel.id, + }; + await this.persistCurrentSession(); + const registry = await this.storage.getSkillRegistry(hotel.id); + this.localSkills = registry && 'skills' in registry ? registry.skills : []; + return this.currentSession; + } + + async getConfigSnapshot(): Promise { + if (!this.currentSession.authenticated) { + throw new Error('请先登录'); + } + + const currentHotelId = this.currentSession.currentHotelId; + const hotel = this.currentSession.hotels.find((item) => item.id === currentHotelId) + ?? this.currentSession.hotels[0]; + + const snapshot = { + serverTime: Date.now(), + user: this.currentSession.user, + hotel, + hotels: this.currentSession.hotels, + entitlements: [ + { + skillId: 'data-check', + name: '数据检查助手', + version: '1.0.0', + enabled: true, + category: 'ota-monitoring', + triggers: ['manual', 'scheduled'], + lastRunAt: Date.now() - 48 * 60 * 1000, + }, + { + skillId: 'workflow-check', + name: '流程检查助手', + version: '1.0.0', + enabled: true, + category: 'ops-automation', + triggers: ['manual', 'scheduled'], + lastRunAt: Date.now() - 55 * 60 * 1000, + }, + { + skillId: 'daily-report', + name: '日报生成助手', + version: '0.1.0', + enabled: true, + category: 'reporting', + triggers: ['manual', 'scheduled'], + lastRunAt: Date.now() - 2 * 60 * 60 * 1000, + }, + { + skillId: 'customer-reply-helper', + name: '客户回复助手', + version: '0.1.0', + enabled: true, + category: 'guest-comm', + triggers: ['manual', 'reply'], + }, + ], + notificationChannels: [ + { + id: 'ch_wecom_ops', + kind: 'wecom', + label: '业务通知群', + recipient: '当前组织空间通知群', + enabled: true, + source: 'nianxx', + }, + { + id: 'ch_desktop', + kind: 'desktop', + label: '本机桌面通知', + recipient: '当前设备', + enabled: true, + source: 'kernel', + }, + ], + featureFlags: { + yinianTodayPage: true, + autoUpdateSkills: true, + reviewReplyHelper: true, + }, + uiPolicy: { + defaultPage: 'today', + showAdvancedSettings: false, + }, + }; + await this.storage.setConfig(snapshot); + return snapshot; + } + + async syncSkills(): Promise { + const config = await this.getConfigSnapshot(); + const now = Date.now(); + + const previousRegistry = await this.storage.getSkillRegistry(config.hotel.id); + const previousSkills = previousRegistry && 'skills' in previousRegistry ? previousRegistry.skills : this.localSkills; + this.localSkills = config.entitlements.map((skill) => { + const previous = previousSkills.find((item) => item.skillId === skill.skillId); + return { + skillId: skill.skillId, + name: skill.name, + version: skill.version, + enabled: skill.enabled, + installedAt: previous?.installedAt ?? now, + lastSyncedAt: now, + status: previous?.version === skill.version ? 'skipped' : previous ? 'updated' : 'installed', + source: 'mock', + bundleSha256: `mock-${skill.skillId}-${skill.version}`, + }; + }); + await this.storage.setSkillRegistry({ + hotelId: config.hotel.id, + updatedAt: now, + skills: this.localSkills, + }); + + return { + hotelId: config.hotel.id, + syncedAt: now, + skills: this.localSkills, + }; + } + + async listLocalSkills(): Promise { + if (this.currentSession.authenticated) { + const registry = await this.storage.getSkillRegistry(this.currentSession.currentHotelId); + if (registry && 'skills' in registry) { + this.localSkills = registry.skills; + } + } + return this.localSkills; + } + + async getSkillRegistry(hotelId?: string) { + return this.storage.getSkillRegistry(hotelId); + } + + private async persistCurrentSession(): Promise { + if (!this.currentSession.authenticated) return; + await this.storage.setSession({ + mode: 'mock', + user: this.currentSession.user, + hotels: this.currentSession.hotels, + currentHotelId: this.currentSession.currentHotelId, + accessTokenExpiresAt: this.currentSession.accessTokenExpiresAt, + updatedAt: Date.now(), + }); + } +} diff --git a/electron/yinian/storage.ts b/electron/yinian/storage.ts new file mode 100644 index 0000000..3568b36 --- /dev/null +++ b/electron/yinian/storage.ts @@ -0,0 +1,258 @@ +import type { + YinianConfigSnapshot, + YinianPersistedSession, + YinianSavedCredentials, + YinianSkillRegistry, + YinianSkillRegistryByHotel, +} from '../../shared/yinian'; + +interface YinianStoreShape { + session?: YinianPersistedSession; + savedCredentials?: YinianSavedCredentials & { + passwordEncrypted?: string; + passwordEncoding?: 'electron-safe-storage' | 'plain'; + }; + configs?: Record; + skillRegistryByHotel?: YinianSkillRegistryByHotel; +} + +export interface YinianStorage { + getSession(): Promise; + setSession(session: YinianPersistedSession): Promise; + clearSession(): Promise; + getSavedCredentials(): Promise; + setSavedCredentials(credentials: YinianSavedCredentials): Promise; + clearSavedCredentials(): Promise; + getConfig(hotelId: string): Promise; + setConfig(config: YinianConfigSnapshot): Promise; + clearConfig(hotelId: string): Promise; + getSkillRegistry(hotelId?: string): Promise; + setSkillRegistry(registry: YinianSkillRegistry): Promise; + clearSkillRegistry(hotelId?: string): Promise; + clearAll(): Promise; +} + +// Lazy-load electron-store only in the Electron main process. Tests and pure +// modules can inject createMemoryYinianStorage instead. +let storeInstance: { + get(key: K): YinianStoreShape[K]; + set(key: K, value: YinianStoreShape[K]): void; + delete(key: keyof YinianStoreShape): void; + clear(): void; +} | null = null; + +async function getStore() { + if (!storeInstance) { + const Store = (await import('electron-store')).default; + storeInstance = new Store({ + name: 'yinian', + defaults: { + configs: {}, + skillRegistryByHotel: {}, + }, + }); + } + return storeInstance; +} + +async function encryptPassword(password: string | undefined): Promise<{ + password?: string; + passwordEncrypted?: string; + passwordEncoding?: 'electron-safe-storage' | 'plain'; +}> { + if (!password) return {}; + try { + const { safeStorage } = await import('electron'); + if (safeStorage?.isEncryptionAvailable()) { + return { + passwordEncrypted: safeStorage.encryptString(password).toString('base64'), + passwordEncoding: 'electron-safe-storage', + }; + } + } catch { + // Fall through to the plain fallback used in dev/test environments. + } + return { password, passwordEncoding: 'plain' }; +} + +async function decryptCredentials( + credentials: YinianStoreShape['savedCredentials'], +): Promise { + if (!credentials) return undefined; + if (credentials.passwordEncrypted && credentials.passwordEncoding === 'electron-safe-storage') { + try { + const { safeStorage } = await import('electron'); + if (safeStorage?.isEncryptionAvailable()) { + return { + account: credentials.account, + password: safeStorage.decryptString(Buffer.from(credentials.passwordEncrypted, 'base64')), + rememberPassword: credentials.rememberPassword, + updatedAt: credentials.updatedAt, + }; + } + } catch { + return { + account: credentials.account, + rememberPassword: false, + updatedAt: credentials.updatedAt, + }; + } + } + return { + account: credentials.account, + password: credentials.password, + rememberPassword: credentials.rememberPassword, + updatedAt: credentials.updatedAt, + }; +} + +async function prepareCredentialsForStorage(credentials: YinianSavedCredentials): Promise { + const passwordState = await encryptPassword(credentials.rememberPassword ? credentials.password : undefined); + return { + account: credentials.account, + rememberPassword: credentials.rememberPassword, + updatedAt: credentials.updatedAt, + ...passwordState, + }; +} + +export function createElectronYinianStorage(): YinianStorage { + return { + async getSession() { + return (await getStore()).get('session'); + }, + async setSession(session) { + (await getStore()).set('session', session); + }, + async clearSession() { + (await getStore()).delete('session'); + }, + async getSavedCredentials() { + return decryptCredentials((await getStore()).get('savedCredentials')); + }, + async setSavedCredentials(credentials) { + (await getStore()).set('savedCredentials', await prepareCredentialsForStorage(credentials)); + }, + async clearSavedCredentials() { + (await getStore()).delete('savedCredentials'); + }, + async getConfig(hotelId) { + return (await getStore()).get('configs')?.[hotelId]; + }, + async setConfig(config) { + const store = await getStore(); + store.set('configs', { + ...(store.get('configs') ?? {}), + [config.hotel.id]: config, + }); + }, + async clearConfig(hotelId) { + const store = await getStore(); + const configs = { ...(store.get('configs') ?? {}) }; + delete configs[hotelId]; + store.set('configs', configs); + }, + async getSkillRegistry(hotelId) { + const registries = (await getStore()).get('skillRegistryByHotel') ?? {}; + return hotelId ? registries[hotelId] : registries; + }, + async setSkillRegistry(registry) { + const store = await getStore(); + store.set('skillRegistryByHotel', { + ...(store.get('skillRegistryByHotel') ?? {}), + [registry.hotelId]: registry, + }); + }, + async clearSkillRegistry(hotelId) { + const store = await getStore(); + if (!hotelId) { + store.set('skillRegistryByHotel', {}); + return; + } + const registries = { ...(store.get('skillRegistryByHotel') ?? {}) }; + delete registries[hotelId]; + store.set('skillRegistryByHotel', registries); + }, + async clearAll() { + (await getStore()).clear(); + }, + }; +} + +export function createMemoryYinianStorage(initial?: Partial): YinianStorage { + const state: YinianStoreShape = { + configs: {}, + skillRegistryByHotel: {}, + ...initial, + }; + + return { + async getSession() { + return state.session; + }, + async setSession(session) { + state.session = session; + }, + async clearSession() { + delete state.session; + }, + async getSavedCredentials() { + return decryptCredentials(state.savedCredentials); + }, + async setSavedCredentials(credentials) { + state.savedCredentials = await prepareCredentialsForStorage(credentials); + }, + async clearSavedCredentials() { + delete state.savedCredentials; + }, + async getConfig(hotelId) { + return state.configs?.[hotelId]; + }, + async setConfig(config) { + state.configs = { + ...(state.configs ?? {}), + [config.hotel.id]: config, + }; + }, + async clearConfig(hotelId) { + if (!state.configs) return; + delete state.configs[hotelId]; + }, + async getSkillRegistry(hotelId) { + const registries = state.skillRegistryByHotel ?? {}; + return hotelId ? registries[hotelId] : registries; + }, + async setSkillRegistry(registry) { + state.skillRegistryByHotel = { + ...(state.skillRegistryByHotel ?? {}), + [registry.hotelId]: registry, + }; + }, + async clearSkillRegistry(hotelId) { + if (!hotelId) { + state.skillRegistryByHotel = {}; + return; + } + if (!state.skillRegistryByHotel) return; + delete state.skillRegistryByHotel[hotelId]; + }, + async clearAll() { + delete state.session; + delete state.savedCredentials; + state.configs = {}; + state.skillRegistryByHotel = {}; + }, + }; +} + +let yinianStorage: YinianStorage | null = null; + +export function getYinianStorage(): YinianStorage { + yinianStorage ??= createElectronYinianStorage(); + return yinianStorage; +} + +export function resetYinianStorageForTests(next?: YinianStorage): void { + yinianStorage = next ?? null; + storeInstance = null; +} diff --git a/electron/yinian/unconfigured-control-plane.ts b/electron/yinian/unconfigured-control-plane.ts new file mode 100644 index 0000000..ea7fd98 --- /dev/null +++ b/electron/yinian/unconfigured-control-plane.ts @@ -0,0 +1,70 @@ +import type { + YinianAuthSession, + YinianConfigSnapshot, + YinianImageCaptcha, + YinianLocalSkill, + YinianLoginWithPasswordInput, + YinianLoginWithSmsInput, + YinianServerStatus, + YinianSessionState, + YinianSkillRegistry, + YinianSkillSyncResult, +} from '../../shared/yinian'; +import type { YinianControlPlane } from './control-plane'; + +const MESSAGE = '服务端地址未配置,请设置 YINIAN_API_BASE_URL 后重新启动智念助手。'; + +export class UnconfiguredYinianControlPlane implements YinianControlPlane { + async getServerStatus(): Promise { + return { + mode: 'http', + reachable: false, + checkedAt: Date.now(), + message: MESSAGE, + }; + } + + async createImageCaptcha(randomStr = `${Date.now()}`): Promise { + return { randomStr, raw: { error: MESSAGE } }; + } + + async restoreSession(): Promise { + return { authenticated: false }; + } + + async getSessionState(): Promise { + return { authenticated: false }; + } + + async loginWithSms(_input: YinianLoginWithSmsInput): Promise { + throw new Error(MESSAGE); + } + + async loginWithPassword(_input: YinianLoginWithPasswordInput): Promise { + throw new Error(MESSAGE); + } + + async logout(): Promise { + return { authenticated: false }; + } + + async switchHotel(_hotelId: string): Promise { + throw new Error(MESSAGE); + } + + async getConfigSnapshot(): Promise { + throw new Error(MESSAGE); + } + + async syncSkills(): Promise { + throw new Error(MESSAGE); + } + + async listLocalSkills(): Promise { + return []; + } + + async getSkillRegistry(_hotelId?: string): Promise | undefined> { + return undefined; + } +} diff --git a/index.html b/index.html index ee66b22..18bb260 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,9 @@ - + - ClawX + 智念助手
diff --git a/package.json b/package.json index 59d201a..a930ce6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "clawx", - "version": "0.3.11", + "name": "yinian-desktop", + "version": "0.1.0", "pnpm": { "onlyBuiltDependencies": [ "@discordjs/opus", @@ -26,9 +26,9 @@ ] } }, - "description": "ClawX - Graphical AI Assistant based on OpenClaw", + "description": "智念助手 - B 端 AI Agent 桌面客户端,基于 OpenClaw 内核构建", "main": "dist-electron/main/index.js", - "author": "ClawX Team", + "author": "YINIAN", "license": "MIT", "private": true, "scripts": { @@ -78,12 +78,13 @@ "electron-updater": "^6.8.3", "katex": "^0.16.45", "lru-cache": "^11.2.6", + "mammoth": "1.12.0", "ms": "^2.1.3", "node-machine-id": "^1.1.12", "posthog-node": "^5.28.0", - "tar": "^6.2.1", "rehype-katex": "^7.0.1", "remark-math": "^6.0.0", + "tar": "^6.2.1", "ws": "^8.19.0" }, "devDependencies": { @@ -103,7 +104,7 @@ "@radix-ui/react-toast": "^1.2.15", "@radix-ui/react-tooltip": "^1.2.8", "@soimy/dingtalk": "^3.5.3", - "@tencent-weixin/openclaw-weixin": "^2.1.8", + "@tencent-weixin/openclaw-weixin": "^2.1.10", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@types/node": "^25.3.0", @@ -113,7 +114,7 @@ "@typescript-eslint/eslint-plugin": "^8.56.0", "@typescript-eslint/parser": "^8.56.0", "@vitejs/plugin-react": "^5.1.4", - "@wecom/wecom-openclaw-plugin": "^2026.4.8", + "@wecom/wecom-openclaw-plugin": "^2026.4.27", "@whiskeysockets/baileys": "7.0.0-rc.9", "autoprefixer": "^10.4.24", "class-variance-authority": "^0.7.1", @@ -128,7 +129,7 @@ "i18next": "^25.8.11", "jsdom": "^28.1.0", "lucide-react": "^0.563.0", - "openclaw": "2026.4.15", + "openclaw": "2026.4.26", "png2icons": "^2.0.1", "postcss": "^8.5.6", "react": "^19.2.4", diff --git a/packages/kernel-adapter-openclaw/package.json b/packages/kernel-adapter-openclaw/package.json new file mode 100644 index 0000000..d8527a6 --- /dev/null +++ b/packages/kernel-adapter-openclaw/package.json @@ -0,0 +1,12 @@ +{ + "name": "@yinian/kernel-adapter-openclaw", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "@yinian/kernel-core": "workspace:*" + } +} + diff --git a/packages/kernel-adapter-openclaw/src/index.ts b/packages/kernel-adapter-openclaw/src/index.ts new file mode 100644 index 0000000..a5d95ba --- /dev/null +++ b/packages/kernel-adapter-openclaw/src/index.ts @@ -0,0 +1,81 @@ +import type { + Adapter, + AdapterFactory, + Conversation, + ConversationId, + ConversationPort, + ConversationStreamEvent, + CreateConversationInput, + Message, + SendMessageInput, +} from '@yinian/kernel-core'; + +export interface OpenClawAdapterConfig { + wsUrl: string; + token: string; + reconnect?: { + maxAttempts: number; + backoffMs: number; + }; +} + +class NotImplementedConversationPort implements ConversationPort { + async list(): Promise { + return []; + } + + async get(_id: ConversationId): Promise { + throw new Error('OpenClaw conversation.get adapter is not implemented yet.'); + } + + async create(_input: CreateConversationInput): Promise { + throw new Error('OpenClaw conversation.create adapter is not implemented yet.'); + } + + async delete(_id: ConversationId): Promise { + throw new Error('OpenClaw conversation.delete adapter is not implemented yet.'); + } + + async *send(_input: SendMessageInput): AsyncIterable { + yield { + type: 'error', + error: { + code: 'kernel_unavailable', + message: 'OpenClaw conversation.send adapter is not implemented yet.', + retryable: false, + }, + }; + } + + async abort(_conversationId: ConversationId): Promise { + throw new Error('OpenClaw conversation.abort adapter is not implemented yet.'); + } + + async history(_conversationId: ConversationId): Promise { + return []; + } +} + +export const createOpenClawAdapter: AdapterFactory = (config): Adapter => { + const conversation = new NotImplementedConversationPort(); + + return { + info: { name: 'openclaw', kernelVersion: 'unknown' }, + async connect() { + if (!config.wsUrl || !config.token) { + throw new Error('OpenClaw adapter requires wsUrl and token.'); + } + }, + async disconnect() { + // No persistent socket yet. The real implementation lands with the Gateway contract. + }, + async health() { + return { + ready: false, + details: { wsUrl: config.wsUrl, reason: 'adapter_stub' }, + }; + }, + conversation, + }; +}; + diff --git a/packages/kernel-context/package.json b/packages/kernel-context/package.json new file mode 100644 index 0000000..df157c2 --- /dev/null +++ b/packages/kernel-context/package.json @@ -0,0 +1,12 @@ +{ + "name": "@yinian/kernel-context", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "@yinian/kernel-core": "workspace:*" + } +} + diff --git a/packages/kernel-context/src/index.ts b/packages/kernel-context/src/index.ts new file mode 100644 index 0000000..6677d1c --- /dev/null +++ b/packages/kernel-context/src/index.ts @@ -0,0 +1,22 @@ +import type { Adapter } from '@yinian/kernel-core'; + +export interface KernelContext { + adapter: Adapter; + conversation: Adapter['conversation']; + connect(): Promise; + disconnect(): Promise; + health(): ReturnType; +} + +export function createKernelContext(input: { adapter: Adapter }): KernelContext { + const { adapter } = input; + + return { + adapter, + conversation: adapter.conversation, + connect: () => adapter.connect(), + disconnect: () => adapter.disconnect(), + health: () => adapter.health(), + }; +} + diff --git a/packages/kernel-core/package.json b/packages/kernel-core/package.json new file mode 100644 index 0000000..d2229af --- /dev/null +++ b/packages/kernel-core/package.json @@ -0,0 +1,9 @@ +{ + "name": "@yinian/kernel-core", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts" +} + diff --git a/packages/kernel-core/src/index.ts b/packages/kernel-core/src/index.ts new file mode 100644 index 0000000..a316aa3 --- /dev/null +++ b/packages/kernel-core/src/index.ts @@ -0,0 +1,160 @@ +export type Brand = T & { readonly __brand: Name }; + +export type UserId = Brand; +export type HotelId = Brand; +export type ConversationId = Brand; +export type MessageId = Brand; +export type SkillId = Brand; +export type TaskId = Brand; +export type ExecutionId = Brand; + +export interface User { + id: UserId; + name: string; + avatar?: string; + email?: string; + phone?: string; + hotels: HotelMembership[]; +} + +export interface HotelMembership { + hotelId: HotelId; + role: 'owner' | 'manager' | 'staff' | 'viewer'; +} + +export interface Hotel { + id: HotelId; + name: string; + brand?: string; + city: string; + ota: HotelOTABinding[]; +} + +export interface HotelOTABinding { + ota: 'ctrip' | 'meituan' | 'booking' | 'agoda' | 'qunar' | string; + externalId: string; + enabled: boolean; +} + +export type MessageRole = 'user' | 'assistant' | 'system' | 'tool'; + +export interface Message { + id: MessageId; + conversationId: ConversationId; + role: MessageRole; + blocks: ContentBlock[]; + createdAt: number; +} + +export type ContentBlock = + | TextBlock + | ToolCallBlock + | ToolResultBlock + | ArtifactBlock + | ReasoningBlock; + +export interface TextBlock { + type: 'text'; + text: string; +} + +export interface ReasoningBlock { + type: 'reasoning'; + text: string; +} + +export interface ToolCallBlock { + type: 'tool_call'; + toolCallId: string; + name: string; + input: Record; +} + +export interface ToolResultBlock { + type: 'tool_result'; + toolCallId: string; + status: 'success' | 'failed'; + output: unknown; + error?: KernelError; +} + +export interface ArtifactBlock { + type: 'artifact'; + artifact: Artifact; +} + +export type Artifact = + | { kind: 'markdown'; id: string; title?: string; content: string } + | { kind: 'table'; id: string; title?: string; columns: string[]; rows: unknown[][] } + | { kind: 'chart'; id: string; title?: string; spec: unknown } + | { kind: 'image'; id: string; title?: string; url: string } + | { kind: 'file'; id: string; title?: string; url: string; mime: string }; + +export interface KernelError { + code: KernelErrorCode; + message: string; + retryable: boolean; + cause?: unknown; +} + +export type KernelErrorCode = + | 'kernel_unavailable' + | 'kernel_timeout' + | 'kernel_aborted' + | 'auth_required' + | 'permission_denied' + | 'skill_not_found' + | 'skill_execution_failed' + | 'invalid_input' + | 'rate_limited' + | 'unknown'; + +export interface Conversation { + id: ConversationId; + title: string; + hotelId: HotelId; + createdAt: number; + updatedAt: number; + messageCount: number; +} + +export interface CreateConversationInput { + hotelId: HotelId; + title?: string; +} + +export interface SendMessageInput { + conversationId: ConversationId; + content: TextBlock | { type: 'invoke_skill'; skillId: SkillId; input: Record }; +} + +export type ConversationStreamEvent = + | { type: 'message_start'; message: Pick } + | { type: 'text_delta'; messageId: MessageId; delta: string } + | { type: 'reasoning_delta'; messageId: MessageId; delta: string } + | { type: 'tool_call'; messageId: MessageId; block: ToolCallBlock } + | { type: 'tool_result'; messageId: MessageId; block: ToolResultBlock } + | { type: 'artifact'; messageId: MessageId; artifact: Artifact } + | { type: 'message_complete'; messageId: MessageId; message: Message } + | { type: 'error'; error: KernelError }; + +export interface ConversationPort { + list(query?: { hotelId?: HotelId; limit?: number }): Promise; + get(id: ConversationId): Promise; + create(input: CreateConversationInput): Promise; + delete(id: ConversationId): Promise; + send(input: SendMessageInput): AsyncIterable; + abort(conversationId: ConversationId): Promise; + history(conversationId: ConversationId, opts?: { before?: MessageId; limit?: number }): Promise; +} + +export interface Adapter { + readonly info: { name: string; kernelVersion: string }; + connect(): Promise; + disconnect(): Promise; + health(): Promise<{ ready: boolean; details?: Record }>; + readonly conversation: ConversationPort; +} + +export type AdapterFactory = (config: Config) => Adapter; + diff --git a/packages/skill-spec/package.json b/packages/skill-spec/package.json new file mode 100644 index 0000000..f19f810 --- /dev/null +++ b/packages/skill-spec/package.json @@ -0,0 +1,12 @@ +{ + "name": "@yinian/skill-spec", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "@yinian/kernel-core": "workspace:*" + } +} + diff --git a/packages/skill-spec/src/index.ts b/packages/skill-spec/src/index.ts new file mode 100644 index 0000000..182428e --- /dev/null +++ b/packages/skill-spec/src/index.ts @@ -0,0 +1,36 @@ +import type { Artifact, Hotel, User } from '@yinian/kernel-core'; + +export interface SkillManifest { + spec_version: '0.1'; + id: string; + name: string; + version: string; + author: string; + category: string; + description: string; + required_capabilities: string[]; + permissions: SkillPermissions; +} + +export interface SkillPermissions { + network?: string[]; + filesystem?: 'none' | 'read' | 'read_write'; + shell?: boolean; +} + +export interface SkillRunContext { + hotel: Hotel; + user: User; + emit: { + progress(event: { phase: string; ratio?: number; message?: string }): void; + log(level: 'info' | 'warn' | 'error', message: string): void; + artifact(artifact: Artifact): void; + }; + abortSignal: AbortSignal; +} + +export interface SkillRunResult { + output: Record; + notifications?: Array<{ template: string; vars: Record }>; +} + diff --git a/packages/skills-hotel-core/package.json b/packages/skills-hotel-core/package.json new file mode 100644 index 0000000..61dff88 --- /dev/null +++ b/packages/skills-hotel-core/package.json @@ -0,0 +1,9 @@ +{ + "name": "@yinian/skills-hotel-core", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts" +} + diff --git a/packages/skills-hotel-core/src/index.ts b/packages/skills-hotel-core/src/index.ts new file mode 100644 index 0000000..43b4cbf --- /dev/null +++ b/packages/skills-hotel-core/src/index.ts @@ -0,0 +1,28 @@ +export interface PriceAnomalyRuleInput { + roomType: string; + date: string; + price: number; + priceFloor?: number; +} + +export type PriceAnomalyKind = 'below_floor' | 'large_gap' | 'unavailable' | 'missing_room' | 'parse_suspect'; + +export interface PriceAnomaly { + kind: PriceAnomalyKind; + roomType: string; + date: string; + message: string; +} + +export function detectPriceFloorAnomaly(input: PriceAnomalyRuleInput): PriceAnomaly | null { + if (typeof input.priceFloor !== 'number') return null; + if (input.price >= input.priceFloor) return null; + + return { + kind: 'below_floor', + roomType: input.roomType, + date: input.date, + message: `价格 ${input.price} 低于底价 ${input.priceFloor}`, + }; +} + diff --git a/packages/ui-kit/package.json b/packages/ui-kit/package.json new file mode 100644 index 0000000..b5b8c2a --- /dev/null +++ b/packages/ui-kit/package.json @@ -0,0 +1,9 @@ +{ + "name": "@yinian/ui-kit", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts" +} + diff --git a/packages/ui-kit/src/index.ts b/packages/ui-kit/src/index.ts new file mode 100644 index 0000000..a9e10e3 --- /dev/null +++ b/packages/ui-kit/src/index.ts @@ -0,0 +1,19 @@ +export const yinianTokens = { + color: { + brand: '#1A56DB', + success: '#16A34A', + warning: '#F59E0B', + danger: '#DC2626', + surface: '#FFFFFF', + mutedSurface: '#F8FAFC', + border: '#E2E8F0', + text: '#0F172A', + mutedText: '#64748B', + }, + radius: { + sm: '4px', + md: '6px', + lg: '8px', + }, +} as const; + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 05e2c77..a800f1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,9 @@ importers: lru-cache: specifier: ^11.2.6 version: 11.2.7 + mammoth: + specifier: 1.12.0 + version: 1.12.0 ms: specifier: ^2.1.3 version: 2.1.3 @@ -39,15 +42,15 @@ importers: posthog-node: specifier: ^5.28.0 version: 5.28.5 - tar: - specifier: ^6.2.1 - version: 6.2.1 rehype-katex: specifier: ^7.0.1 version: 7.0.1 remark-math: specifier: ^6.0.0 version: 6.0.0 + tar: + specifier: ^6.2.1 + version: 6.2.1 ws: specifier: ^8.19.0 version: 8.20.0 @@ -57,7 +60,7 @@ importers: version: 10.0.1(eslint@10.1.0(jiti@1.21.7)) '@larksuite/openclaw-lark': specifier: 2026.4.8 - version: 2026.4.8(openclaw@2026.4.15(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1)(@napi-rs/canvas@0.1.97)(@types/express@5.0.6)(apache-arrow@18.1.0)(encoding@0.1.13)(hono@4.12.12)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)) + version: 2026.4.8(openclaw@2026.4.26) '@playwright/test': specifier: ^1.56.1 version: 1.59.0 @@ -99,10 +102,10 @@ importers: version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@soimy/dingtalk': specifier: ^3.5.3 - version: 3.5.3(openclaw@2026.4.15(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1)(@napi-rs/canvas@0.1.97)(@types/express@5.0.6)(apache-arrow@18.1.0)(encoding@0.1.13)(hono@4.12.12)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)) + version: 3.5.3(openclaw@2026.4.26) '@tencent-weixin/openclaw-weixin': - specifier: ^2.1.8 - version: 2.1.8 + specifier: ^2.1.10 + version: 2.1.10 '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 @@ -131,8 +134,8 @@ importers: specifier: ^5.1.4 version: 5.2.0(vite@7.3.1(@types/node@25.5.0)(jiti@1.21.7)(yaml@2.8.3)) '@wecom/wecom-openclaw-plugin': - specifier: ^2026.4.8 - version: 2026.4.8 + specifier: ^2026.4.27 + version: 2026.4.27 '@whiskeysockets/baileys': specifier: 7.0.0-rc.9 version: 7.0.0-rc.9(jimp@1.6.1)(sharp@0.34.5) @@ -176,8 +179,8 @@ importers: specifier: ^0.563.0 version: 0.563.0(react@19.2.4) openclaw: - specifier: 2026.4.15 - version: 2026.4.15(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1)(@napi-rs/canvas@0.1.97)(@types/express@5.0.6)(apache-arrow@18.1.0)(encoding@0.1.13)(hono@4.12.12)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + specifier: 2026.4.26 + version: 2026.4.26 png2icons: specifier: ^2.0.1 version: 2.0.1 @@ -242,6 +245,30 @@ importers: specifier: ^8.8.5 version: 8.8.5 + packages/kernel-adapter-openclaw: + dependencies: + '@yinian/kernel-core': + specifier: workspace:* + version: link:../kernel-core + + packages/kernel-context: + dependencies: + '@yinian/kernel-core': + specifier: workspace:* + version: link:../kernel-core + + packages/kernel-core: {} + + packages/skill-spec: + dependencies: + '@yinian/kernel-core': + specifier: workspace:* + version: link:../kernel-core + + packages/skills-hotel-core: {} + + packages/ui-kit: {} + packages: 7zip-bin@5.2.0: @@ -253,8 +280,8 @@ packages: '@adobe/css-tools@4.4.4': resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} - '@agentclientprotocol/sdk@0.18.2': - resolution: {integrity: sha512-l/o9NKvUc00GPa6RFJ4AccQq2O/PAf83xQ75mThHuL3H571iN4+PEdwnTBez67sS8Nv2aSA373xCZ5CbTXEwzA==} + '@agentclientprotocol/sdk@0.20.0': + resolution: {integrity: sha512-BxEHyE4MvwyOsdyVPub1vEtyrq8E0JSdjC+ckXWimY1VabFCTXdPyXv2y2Omz1j+iod7Z8oBJDXFCJptM0GBqQ==} peerDependencies: zod: ^3.25.0 || ^4.0.0 @@ -262,8 +289,8 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@anthropic-ai/sdk@0.73.0': - resolution: {integrity: sha512-URURVzhxXGJDGUGFunIOtBlSl7KWvZiAAKY/ttTkZAkXT9bTPqdk2eK0b8qqSxXpikh3QKPnPYpiyX98zf5ebw==} + '@anthropic-ai/sdk@0.90.0': + resolution: {integrity: sha512-MzZtPabJF1b0FTDl6Z6H5ljphPwACLGP13lu8MTiB8jXaW/YXlpOp+Po2cVou3MPM5+f5toyLnul9whKCy7fBg==} hasBin: true peerDependencies: zod: ^3.25.0 || ^4.0.0 @@ -271,18 +298,6 @@ packages: zod: optional: true - '@anthropic-ai/sdk@0.80.0': - resolution: {integrity: sha512-WeXLn7zNVk3yjeshn+xZHvld6AoFUOR3Sep6pSoHho5YbSi6HwcirqgPA5ccFuW8QTVJAAU7N8uQQC6Wa9TG+g==} - hasBin: true - peerDependencies: - zod: ^3.25.0 || ^4.0.0 - peerDependenciesMeta: - zod: - optional: true - - '@anthropic-ai/vertex-sdk@0.15.0': - resolution: {integrity: sha512-i2LDdu6VB8Lqqip+kbNSXRxQgFsCg6GPBO/X2zRJwLl99dNzf28nb6Rdi0EodONXsyJfY2TKdGR+y5l1/AKFEg==} - '@ark/schema@0.56.0': resolution: {integrity: sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA==} @@ -316,127 +331,119 @@ packages: '@aws-crypto/util@5.2.0': resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - '@aws-sdk/client-bedrock-runtime@3.1028.0': - resolution: {integrity: sha512-FFdtkxWFmKX1Ka/vjDRKpYsm0/HTlab5qpHl8LAXRmJjhSSiLGiCnJYsYFN+zp3NucL02kM1DlpFU8Xnm7d8Ng==} + '@aws-sdk/client-bedrock-runtime@3.1038.0': + resolution: {integrity: sha512-oGiqs9v9WzPOdv7PDdm9iPibHgrbDvCDyNg43wFZn2PiiEUisFM+xUP2CRMsj41SmwZPhohmZkXiUu1+MghbAQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/client-bedrock@3.1028.0': - resolution: {integrity: sha512-YEUikjoImgUjv2UEpnD/WP0JiLdoLRnkajnSQR9LPCa8+BGy3+j879jimPlAuypOux1/CgqMA7Fwt13IpF2+UA==} + '@aws-sdk/core@3.974.6': + resolution: {integrity: sha512-8Vu7zGxu+39ChR/s5J7nXBw3a2kMHAi0OfKT8ohgTVjX0qYed/8mIfdBb638oBmKrWCwwKjYAM5J/4gMJ8nAJA==} engines: {node: '>=20.0.0'} - '@aws-sdk/client-cognito-identity@3.1027.0': - resolution: {integrity: sha512-xGZgYNqR0lHVm3ASm1y/MYutLgJ2Ja2kufrbBQuwGD7KYrfUVBnDgbYeuEhhyzQhOvoKXpJ7KrA8tmv5Rc3agA==} + '@aws-sdk/credential-provider-env@3.972.32': + resolution: {integrity: sha512-7vA4GHg8NSmQxquJHSBcSM3RgB4ZaaRi6u4+zGFKOmOH6aqlgr2Sda46clkZDYzlirgfY96w15Zj0jh6PT48ng==} engines: {node: '>=20.0.0'} - '@aws-sdk/core@3.973.27': - resolution: {integrity: sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A==} + '@aws-sdk/credential-provider-http@3.972.34': + resolution: {integrity: sha512-vBrhWujFCLp1u8ptJRWYlipMutzPptb8pDQ00rKVH9q67T7rGd3VTWIj63aKrlLuY6qSsw1Rt5F/D/7wnNgryA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-cognito-identity@3.972.22': - resolution: {integrity: sha512-ih6ORpme4i2qJqGckOQ9Lt2iiZ+5tm3bnfsT5TwoPyFnuDURXv3OdhYa3Nr/m0iJr38biqKYKdGKb5GR1KB2hw==} + '@aws-sdk/credential-provider-ini@3.972.36': + resolution: {integrity: sha512-FBHyCmV8EB0gUvh1d+CZm87zt2PrdC7OyWexLRoH3I5zWSOUGa+9t58Y5jbxRfwUp3AWpHAFvKY6YzgR845sVA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-env@3.972.25': - resolution: {integrity: sha512-6QfI0wv4jpG5CrdO/AO0JfZ2ux+tKwJPrUwmvxXF50vI5KIypKVGNF6b4vlkYEnKumDTI1NX2zUBi8JoU5QU3A==} + '@aws-sdk/credential-provider-login@3.972.36': + resolution: {integrity: sha512-IFap01lJKxQc0C/OHmZwZQr/cKq0DhrcmKedRrdnnl42D+P0SImnnnWQjv07uIPqpEdtqmkPXb9TiPYTU+prxQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-http@3.972.27': - resolution: {integrity: sha512-3V3Usj9Gs93h865DqN4M2NWJhC5kXU9BvZskfN3+69omuYlE3TZxOEcVQtBGLOloJB7BVfJKXVLqeNhOzHqSlQ==} + '@aws-sdk/credential-provider-node@3.972.37': + resolution: {integrity: sha512-/WFixFAAiw8WpmjZcI0l4t3DerXLmVinOIfuotmRZnu2qmsFPoqqmstASz0z8bi1pGdFXzeLzf6bwucM3mZcUQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-ini@3.972.29': - resolution: {integrity: sha512-SiBuAnXecCbT/OpAf3vqyI/AVE3mTaYr9ShXLybxZiPLBiPCCOIWSGAtYYGQWMRvobBTiqOewaB+wcgMMZI2Aw==} + '@aws-sdk/credential-provider-process@3.972.32': + resolution: {integrity: sha512-uZp4tlGbpczV8QxmtIwOpSkcyGtBRR8/T4BAumRKfAt1nwCig3FSCZvrKl6ARDIDVRYn5p2oRcAsfFR01EgMGA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-login@3.972.29': - resolution: {integrity: sha512-OGOslTbOlxXexKMqhxCEbBQbUIfuhGxU5UXw3Fm56ypXHvrXH4aTt/xb5Y884LOoteP1QST1lVZzHfcTnWhiPQ==} + '@aws-sdk/credential-provider-sso@3.972.36': + resolution: {integrity: sha512-DsLr0UHMyKzRJKe2bjlwU8q1cfoXg8TIJKV/xwvnalAemiZLOZunFzj/whGnFDZIBVLdnbLiwv5SvRf1+CSwkg==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-node@3.972.30': - resolution: {integrity: sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw==} + '@aws-sdk/credential-provider-web-identity@3.972.36': + resolution: {integrity: sha512-uzrURO7frJhHQVVNR5zBJcCYeMYflmXcWBK1+MiBym2Dfjh6nXATrMixrmGZi+97Q7ETZ+y/4lUwAy0Nfnznjw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-process@3.972.25': - resolution: {integrity: sha512-HR7ynNRdNhNsdVCOCegy1HsfsRzozCOPtD3RzzT1JouuaHobWyRfJzCBue/3jP7gECHt+kQyZUvwg/cYLWurNQ==} + '@aws-sdk/eventstream-handler-node@3.972.14': + resolution: {integrity: sha512-m4X56gxG76/CKfxNVbOFuYwnAZcHgS6HOH8lgp15HoGHIAVTcZfZrXvcYzJFOMLEJgVn+JHBu6EiNV+xSNXXFg==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-sso@3.972.29': - resolution: {integrity: sha512-HWv4SEq3jZDYPlwryZVef97+U8CxxRos5mK8sgGO1dQaFZpV5giZLzqGE5hkDmh2csYcBO2uf5XHjPTpZcJlig==} + '@aws-sdk/middleware-eventstream@3.972.10': + resolution: {integrity: sha512-QUqLs7Af1II9X4fCRAu+EGHG3KHyOp4RkuLhRKoA3NuFlh6TL8i+zXBl8w2LUxqm44B/Kom45hgSlwA1SpTsXQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-web-identity@3.972.29': - resolution: {integrity: sha512-PdMBza1WEKEUPFEmMGCfnU2RYCz9MskU2e8JxjyUOsMKku7j9YaDKvbDi2dzC0ihFoM6ods2SbhfAAro+Gwlew==} + '@aws-sdk/middleware-host-header@3.972.10': + resolution: {integrity: sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-providers@3.1027.0': - resolution: {integrity: sha512-QJZqR86dDtlBKTmpvvyzqSSoaLBF5ubm1RvEydVXkLOpbD5Vakq+IbHRRMARxbQ1E0QZM1p/n6z0mnsjGWbKYw==} + '@aws-sdk/middleware-logger@3.972.10': + resolution: {integrity: sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/eventstream-handler-node@3.972.13': - resolution: {integrity: sha512-2Pi1kD0MDkMAxDHqvpi/hKMs9hXUYbj2GLEjCwy+0jzfLChAsF50SUYnOeTI+RztA+Ic4pnLAdB03f1e8nggxQ==} + '@aws-sdk/middleware-recursion-detection@3.972.11': + resolution: {integrity: sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-eventstream@3.972.9': - resolution: {integrity: sha512-ypgOvpWxQTCnQyDHGxnTviqqANE7FIIzII7VczJnTPCJcJlu17hMQXnvE47aKSKsawVJAaaRsyOEbHQuLJF9ng==} + '@aws-sdk/middleware-sdk-s3@3.972.35': + resolution: {integrity: sha512-lLppaNTAz+wNgLdi4FtHzrlwrGF0ODTnBWHBaFg85SKs0eJ+M+tP5ifrA8f/0lNd+Ak3MC1NGC6RavV3ny4HTg==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-host-header@3.972.9': - resolution: {integrity: sha512-je5vRdNw4SkuTnmRbFZLdye4sQ0faLt8kwka5wnnSU30q1mHO4X+idGEJOOE+Tn1ME7Oryn05xxkDvIb3UaLaQ==} + '@aws-sdk/middleware-user-agent@3.972.36': + resolution: {integrity: sha512-O2beToxguBvrZFFZ+fFgPbbae8MvyIBjQ6lImee4APHEXXNAD5ZJ2ayLF1mb7rsKw86TM81y5czg82bZncjSjg==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-logger@3.972.9': - resolution: {integrity: sha512-HsVgDrruhqI28RkaXALm8grJ7Agc1wF6Et0xh6pom8NdO2VdO/SD9U/tPwUjewwK/pVoka+EShBxyCvgsPCtog==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/middleware-recursion-detection@3.972.10': - resolution: {integrity: sha512-RVQQbq5orQ/GHUnXvqEOj2HHPBJm+mM+ySwZKS5UaLBwra5ugRtiH09PLUoOZRl7a1YzaOzXSuGbn9iD5j60WQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/middleware-user-agent@3.972.29': - resolution: {integrity: sha512-f/sIRzuTfEjg6NsbMYvye2VsmnQoNgntntleQyx5uGacUYzszbfIlO3GcI6G6daWUmTm0IDZc11qMHWwF0o0mQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/middleware-websocket@3.972.15': - resolution: {integrity: sha512-hsZ35FORQsN5hwNdMD6zWmHCphbXkDxO6j+xwCUiuMb0O6gzS/PWgttQNl1OAn7h/uqZAMUG4yOS0wY/yhAieg==} + '@aws-sdk/middleware-websocket@3.972.16': + resolution: {integrity: sha512-86+S9oCyRVGzoMRpQhxkArp7kD2K75GPmaNevd9B6EyNhWoNvnCZZ3WbgN4j7ZT+jvtvBCGZvI2XHsWZJ+BRIg==} engines: {node: '>= 14.0.0'} - '@aws-sdk/nested-clients@3.996.19': - resolution: {integrity: sha512-uFkmCDXvmQYLanlYdOFS0+MQWkrj9wPMt/ZCc/0J0fjPim6F5jBVBmEomvGY/j77ILW6GTPwN22Jc174Mhkw6Q==} + '@aws-sdk/nested-clients@3.997.4': + resolution: {integrity: sha512-4Sf+WY1lMJzXlw5MiyCMe/UzdILCwvuaHThbqMXS6dfh9gZy3No360I42RXquOI/ULUOhWy2HCyU0Fp20fQGPQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/region-config-resolver@3.972.11': - resolution: {integrity: sha512-6Q8B1dcx6BBqUTY1Mc/eROKA0FImEEY5VPSd6AGPEUf0ErjExz4snVqa9kNJSoVDV1rKaNf3qrWojgcKW+SdDg==} + '@aws-sdk/region-config-resolver@3.972.13': + resolution: {integrity: sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==} engines: {node: '>=20.0.0'} - '@aws-sdk/token-providers@3.1026.0': - resolution: {integrity: sha512-Ieq/HiRrbEtrYP387Nes0XlR7H1pJiJOZKv+QyQzMYpvTiDs0VKy2ZB3E2Zf+aFovWmeE7lRE4lXyF7dYM6GgA==} + '@aws-sdk/signature-v4-multi-region@3.996.23': + resolution: {integrity: sha512-wBbys3Y53Ikly556vyADurKpYQHXS7Jjaskbz+Ga9PZCz7PB/9f3VdKbDlz7dqIzn+xwz7L/a6TR4iXcOi8IRw==} engines: {node: '>=20.0.0'} - '@aws-sdk/token-providers@3.1028.0': - resolution: {integrity: sha512-2vDFrEhJDlUHyvDxqDyOk97cejMM8GJDyQbFfOCEWclGwhTjlj1mdyj36xsxh7DYyuquhjqfbvhpl6ZzsVol0w==} + '@aws-sdk/token-providers@3.1038.0': + resolution: {integrity: sha512-Qniru+9oGGb/HNK/gGZWbV3jsD0k71ngE7qMQ/x6gYNYLd2EOwHCS6E2E6jfkaqO4i0d+nNKmfRy8bNcshKdGQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/types@3.973.7': - resolution: {integrity: sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg==} + '@aws-sdk/types@3.973.8': + resolution: {integrity: sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==} engines: {node: '>=20.0.0'} - '@aws-sdk/util-endpoints@3.996.6': - resolution: {integrity: sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg==} + '@aws-sdk/util-arn-parser@3.972.3': + resolution: {integrity: sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==} engines: {node: '>=20.0.0'} - '@aws-sdk/util-format-url@3.972.9': - resolution: {integrity: sha512-fNJXHrs0ZT7Wx0KGIqKv7zLxlDXt2vqjx9z6oKUQFmpE5o4xxnSryvVHfHpIifYHWKz94hFccIldJ0YSZjlCBw==} + '@aws-sdk/util-endpoints@3.996.8': + resolution: {integrity: sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-format-url@3.972.10': + resolution: {integrity: sha512-DEKiHNJVtNxdyTeQspzY+15Po/kHm6sF0Cs4HV9Q2+lplB63+DrvdeiSoOSdWEWAoO2RcY1veoXVDz2tWxWCgQ==} engines: {node: '>=20.0.0'} '@aws-sdk/util-locate-window@3.965.5': resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/util-user-agent-browser@3.972.9': - resolution: {integrity: sha512-sn/LMzTbGjYqCCF24390WxPd6hkpoSptiUn5DzVp4cD71yqw+yGEGm1YCxyEoPXyc8qciM8UzLJcZBFslxo5Uw==} + '@aws-sdk/util-user-agent-browser@3.972.10': + resolution: {integrity: sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==} - '@aws-sdk/util-user-agent-node@3.973.15': - resolution: {integrity: sha512-fYn3s9PtKdgQkczGZCFMgkNEe8aq1JCVbnRqjqN9RSVW43xn2RV9xdcZ3z01a48Jpkuh/xCmBKJxdLOo4Ozg7w==} + '@aws-sdk/util-user-agent-node@3.973.22': + resolution: {integrity: sha512-YTYqTmOUrwbm1h99Ee4y/mVYpFRl0oSO/amtP5cc1BZZWdaAVWs9zj3TkyRHWvR9aI/ZS8m3mS6awXtYUlWyaw==} engines: {node: '>=20.0.0'} peerDependencies: aws-crt: '>=1.0.0' @@ -444,14 +451,10 @@ packages: aws-crt: optional: true - '@aws-sdk/xml-builder@3.972.17': - resolution: {integrity: sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg==} + '@aws-sdk/xml-builder@3.972.21': + resolution: {integrity: sha512-qxNiHUtlrsjTeSlrPWiFkWps7uD6YB4eKzg7eLAFH8jbiHTlt0ePNlo2Xu+WlftP38JIcMaIX4jTUjOlE2ySWw==} engines: {node: '>=20.0.0'} - '@aws/bedrock-token-generator@1.1.0': - resolution: {integrity: sha512-i+DkWnfdA4j4sffy9dI4k3OGoOWqN8CTGdtO4IZ3c0kpKYFr6KyqzqLQmoRNrF3ACFcWj6u+J6cbBQ97j9wx5w==} - engines: {node: '>=16.0.0'} - '@aws/lambda-invoke-store@0.2.4': resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} engines: {node: '>=18.0.0'} @@ -550,9 +553,6 @@ packages: resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} hasBin: true - '@buape/carbon@0.15.0': - resolution: {integrity: sha512-3V3XXIqtBzU5vSpCp4avX0RKbYyCIh493XDS/nRJvL7Num/9gB8Ylhd1ywt39gBGaNJScJW1hoWxRyN6Il6thw==} - '@cacheable/memory@2.0.8': resolution: {integrity: sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw==} @@ -575,9 +575,6 @@ packages: '@clack/prompts@1.2.0': resolution: {integrity: sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w==} - '@cloudflare/workers-types@4.20260405.1': - resolution: {integrity: sha512-PokTmySa+D6MY01R1UfYH48korsN462NK/fl3aw47Hg7XuLuSo/RTpjT0vtWaJhJoFY5tHGOBBIbDcIc8wltLg==} - '@csstools/color-helpers@6.0.2': resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} engines: {node: '>=20.19.0'} @@ -618,18 +615,6 @@ packages: resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==} engines: {node: '>= 8.9.0'} - '@discordjs/node-pre-gyp@0.4.5': - resolution: {integrity: sha512-YJOVVZ545x24mHzANfYoy0BJX5PDyeZlpiJjDkUBM/V/Ao7TFX9lcUvCN4nr0tbr5ubeaXxtEBILUrHtTphVeQ==} - hasBin: true - - '@discordjs/opus@0.10.0': - resolution: {integrity: sha512-HHEnSNrSPmFEyndRdQBJN2YE6egyXS9JUnJWyP6jficK0Y+qKMEZXyYTgmzpjrxXP1exM/hKaNP7BRBUEWkU5w==} - engines: {node: '>=12.0.0'} - - '@discordjs/voice@0.19.2': - resolution: {integrity: sha512-3yJ255e4ag3wfZu/DSxeOZK1UtnqNxnspmLaQetGT0pDkThNZoHs+Zg6dgZZ19JEVomXygvfHn9lNpICZuYtEA==} - engines: {node: '>=22.12.0'} - '@electron/asar@3.4.1': resolution: {integrity: sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==} engines: {node: '>=10.12.0'} @@ -670,15 +655,9 @@ packages: engines: {node: '>=14.14'} hasBin: true - '@emnapi/core@1.9.2': - resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} - '@emnapi/runtime@1.9.1': resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@esbuild/aix-ppc64@0.27.4': resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} engines: {node: '>=18'} @@ -835,9 +814,6 @@ packages: cpu: [x64] os: [win32] - '@eshaz/web-worker@1.2.2': - resolution: {integrity: sha512-WxXiHFmD9u/owrzempiDlBB1ZYqiLnm9s6aPc8AlFQalq2tKmqdmMr9GXOupDgzXtqnBipj8Un0gkIm7Sjf8mw==} - '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -910,31 +886,12 @@ packages: '@modelcontextprotocol/sdk': optional: true - '@grammyjs/runner@2.0.3': - resolution: {integrity: sha512-nckmTs1dPWfVQteK9cxqxzE+0m1VRvluLWB8UgFzsjg62w3qthPJt0TYtJBEdG7OedvfQq4vnFAyE6iaMkR42A==} - engines: {node: '>=12.20.0 || >=14.13.1'} - peerDependencies: - grammy: ^1.13.1 - - '@grammyjs/transformer-throttler@1.2.1': - resolution: {integrity: sha512-CpWB0F3rJdUiKsq7826QhQsxbZi4wqfz1ccKX+fr+AOC+o8K7ZvS+wqX0suSu1QCsyUq2MDpNiKhyL2ZOJUS4w==} - engines: {node: ^12.20.0 || >=14.13.1} - peerDependencies: - grammy: ^1.0.0 - - '@grammyjs/types@3.26.0': - resolution: {integrity: sha512-jlnyfxfev/2o68HlvAGRocAXgdPPX5QabG7jZlbqC2r9DZyWBfzTlg+nu3O3Fy4EhgLWu28hZ/8wr7DsNamP9A==} - '@hapi/boom@9.1.4': resolution: {integrity: sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==} '@hapi/hoek@9.3.0': resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} - '@homebridge/ciao@1.3.6': - resolution: {integrity: sha512-2F9N/15Q/GnoBXimr8PFg7fb1QrAQBvuZpaW2kseWOOy14Lzc3yZB1mT9N1Ju/4hlkboU33uHxtOxZkvkPoE/w==} - hasBin: true - '@hono/node-server@1.19.13': resolution: {integrity: sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==} engines: {node: '>=18.14.1'} @@ -1255,60 +1212,6 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} - '@lancedb/lancedb-darwin-arm64@0.27.2': - resolution: {integrity: sha512-+XM68V/Rou8kKWDnUeKvg9ChKS0zGeQC2sKAop+06Ty4LwIjEGkeYBYrK0vMhZkBN5EFaOjTOp8E8hGQxdFwXA==} - engines: {node: '>= 18'} - cpu: [arm64] - os: [darwin] - - '@lancedb/lancedb-linux-arm64-gnu@0.27.2': - resolution: {integrity: sha512-laiTTDeMUTzm7t+t6ME5nNQMDoERjmkeuWAFWekbXiFdmp62Dqu34Lvf2BvpWnKwxLMZ5JcBJFIw32WS8/8Jnw==} - engines: {node: '>= 18'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@lancedb/lancedb-linux-arm64-musl@0.27.2': - resolution: {integrity: sha512-bK5Mc50EvwGZaaiym5CoPu8Y4GNSyEEvTQ0dTC2AUIm83qdQu1rGw6kkYtc/rTH/hbvAvPQot4agHDZfMVxfYw==} - engines: {node: '>= 18'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@lancedb/lancedb-linux-x64-gnu@0.27.2': - resolution: {integrity: sha512-qe+ML0YmPru0o84f33RBHqoNk6zsHBjiXTLKsEBDiiFYKks/XMsrkKy9NQYcTxShBrg/nx/MLzCzd7dihqgNYw==} - engines: {node: '>= 18'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@lancedb/lancedb-linux-x64-musl@0.27.2': - resolution: {integrity: sha512-ZpX6Oxn06qvzAdm+D/gNb3SRp/A9lgRAPvPg6nnMmSQk5XamC/hbGO07uK1wwop7nlqXUH/thk4is2y2ieWdTw==} - engines: {node: '>= 18'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@lancedb/lancedb-win32-arm64-msvc@0.27.2': - resolution: {integrity: sha512-4ffpFvh49MiUtkdFJOmBytXEbgUPXORphTOuExnJAgT1VAKwQcu4ZzdsgNoK6mumKBaU+pYQU/MedNkgTzx/Lw==} - engines: {node: '>= 18'} - cpu: [arm64] - os: [win32] - - '@lancedb/lancedb-win32-x64-msvc@0.27.2': - resolution: {integrity: sha512-XlwiI6CK2Gkqq+FFVAStHojao/XjIJpDPTm7Tb9SpLL64IlwGw3yaT2hnWKTm90W4KlSrpfSldPly+s+y4U7JQ==} - engines: {node: '>= 18'} - cpu: [x64] - os: [win32] - - '@lancedb/lancedb@0.27.2': - resolution: {integrity: sha512-JQpZHV5KzUzDI3flYCjtZcfHlEbL8lM54E0NT+jrRYe29aKYegfavvPsAsuZp0VdcMwFMZcpMkaBhjQMo/fwvg==} - engines: {node: '>= 18'} - cpu: [x64, arm64] - os: [darwin, linux, win32] - peerDependencies: - apache-arrow: '>=15.0.0 <=18.1.0' - '@larksuite/openclaw-lark@2026.4.8': resolution: {integrity: sha512-HHIwDBQPtEZSLHHCnV52ip5WL8mag/qBUucIyH+pbyMHAAXyKtPHTfXFGTm1/HVMC0ZH2km5f11QvNyy85SDGw==} engines: {node: '>=22'} @@ -1363,106 +1266,98 @@ packages: resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} engines: {node: '>= 10.0.0'} - '@mariozechner/clipboard-darwin-arm64@0.3.2': - resolution: {integrity: sha512-uBf6K7Je1ihsgvmWxA8UCGCeI+nbRVRXoarZdLjl6slz94Zs1tNKFZqx7aCI5O1i3e0B6ja82zZ06BWrl0MCVw==} + '@mariozechner/clipboard-darwin-arm64@0.3.3': + resolution: {integrity: sha512-+zhuZGXqVrdkbIRdnwiZNbTJ7V3elq/A+C5d5laJoyhJgWs41eO5NUMkBkj6f23F2L4PRXEhdn5/ktlPx+bG3Q==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@mariozechner/clipboard-darwin-universal@0.3.2': - resolution: {integrity: sha512-mxSheKTW2U9LsBdXy0SdmdCAE5HqNS9QUmpNHLnfJ+SsbFKALjEZc5oRrVMXxGQSirDvYf5bjmRyT0QYYonnlg==} + '@mariozechner/clipboard-darwin-universal@0.3.3': + resolution: {integrity: sha512-x9aRfTyndVqpEQ44LNNCK/EXZd9y8rWkLQgNhmWpby9PXrjPhNxfjUc2Db4mt4nJjU/4zzO8F5v/XyzlUGSdhQ==} engines: {node: '>= 10'} os: [darwin] - '@mariozechner/clipboard-darwin-x64@0.3.2': - resolution: {integrity: sha512-U1BcVEoidvwIp95+HJswSW+xr28EQiHR7rZjH6pn8Sja5yO4Yoe3yCN0Zm8Lo72BbSOK/fTSq0je7CJpaPCspg==} + '@mariozechner/clipboard-darwin-x64@0.3.3': + resolution: {integrity: sha512-6ut/NawB0KiYPCwrirgNp6Br62LntL978q7G6d/Rs2pmPvQb53bP96eUMYl+Y3a7Qk13bGZ4w9rVPFxRE9m9ag==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@mariozechner/clipboard-linux-arm64-gnu@0.3.2': - resolution: {integrity: sha512-BsinwG3yWTIjdgNCxsFlip7LkfwPk+ruw/aFCXHUg/fb5XC/Ksp+YMQ7u0LUtiKzIv/7LMXgZInJQH6gxbAaqQ==} + '@mariozechner/clipboard-linux-arm64-gnu@0.3.3': + resolution: {integrity: sha512-gf3dH4kBddU1AOyHVB53mjLUFfJAKlTmxTMw51jdeg7eE7IjfEBXVvM4bifMtBxbWkT0eA0FUZ1C0KQ6Z5l6pw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@mariozechner/clipboard-linux-arm64-musl@0.3.2': - resolution: {integrity: sha512-0/Gi5Xq2V6goXBop19ePoHvXsmJD9SzFlO3S+d6+T2b+BlPcpOu3Oa0wTjl+cZrLAAEzA86aPNBI+VVAFDFPKw==} + '@mariozechner/clipboard-linux-arm64-musl@0.3.3': + resolution: {integrity: sha512-o1paj2+zmAQ/LaPS85XJCxhNowNQpxYM2cGY6pWvB5Kqmz6hZjl6CzDg5tbf1hZkn/Em6jpOaE2UtMxKdELBDA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@mariozechner/clipboard-linux-riscv64-gnu@0.3.2': - resolution: {integrity: sha512-2AFFiXB24qf0zOZsxI1GJGb9wQGlOJyN6UwoXqmKS3dpQi/l6ix30IzDDA4c4ZcCcx4D+9HLYXhC1w7Sov8pXA==} + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.3': + resolution: {integrity: sha512-dkEhE4ekePJwMbBq9HP1//CFMNmDzA/iV9AXqBfvL5CWmmDIRXqh4A3YZt3tWO/HdMerX+xNCEiR7WiOsIG+UA==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] libc: [glibc] - '@mariozechner/clipboard-linux-x64-gnu@0.3.2': - resolution: {integrity: sha512-v6fVnsn7WMGg73Dab8QMwyFce7tzGfgEixKgzLP8f1GJqkJZi5zO4k4FOHzSgUufgLil63gnxvMpjWkgfeQN7A==} + '@mariozechner/clipboard-linux-x64-gnu@0.3.3': + resolution: {integrity: sha512-lT2yANtTLlEtFBIH3uGoRa/CQas/eBoLNi3qr9axQFoRgF4RGPSJ66yHOSnMECBneTIb1Iqv3UxokTfX27CdoQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@mariozechner/clipboard-linux-x64-musl@0.3.2': - resolution: {integrity: sha512-xVUtnoMQ8v2JVyfJLKKXACA6avdnchdbBkTsZs8BgJQo29qwCp5NIHAUO8gbJ40iaEGToW5RlmVk2M9V0HsHEw==} + '@mariozechner/clipboard-linux-x64-musl@0.3.3': + resolution: {integrity: sha512-saq/MCB0QHK/7ZZLjAZ0QkbY944dyjOsur8gneGCfMitt+GOiE1CU4OUipHC4b6x8UDY9bRLsR4aBaxu22OFPA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@mariozechner/clipboard-win32-arm64-msvc@0.3.2': - resolution: {integrity: sha512-AEgg95TNi8TGgak2wSXZkXKCvAUTjWoU1Pqb0ON7JHrX78p616XUFNTJohtIon3e0w6k0pYPZeCuqRCza/Tqeg==} + '@mariozechner/clipboard-win32-arm64-msvc@0.3.3': + resolution: {integrity: sha512-cGuvSj0/2X2w983yEcKw+i+r1EBej6ZZIN+fXG3eY2G/HaIQpbXpLvMxKyZ9LKtbZx+Z6q/gELEoSBMLML6BaQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@mariozechner/clipboard-win32-x64-msvc@0.3.2': - resolution: {integrity: sha512-tGRuYpZwDOD7HBrCpyRuhGnHHSCknELvqwKKUG4JSfSB7JIU7LKRh6zx6fMUOQd8uISK35TjFg5UcNih+vJhFA==} + '@mariozechner/clipboard-win32-x64-msvc@0.3.3': + resolution: {integrity: sha512-5hvaEq/bgYovTIGx43O/S7loIHYV3ue90WcV1dz0wdMXroVKZKeU/yfwM0PALQA1OcrEHiGXGySFReXr72lGtA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@mariozechner/clipboard@0.3.2': - resolution: {integrity: sha512-IHQpksNjo7EAtGuHFU+tbWDp5LarH3HU/8WiB9O70ZEoBPHOg0/6afwSLK0QyNMMmx4Bpi/zl6+DcBXe95nWYA==} + '@mariozechner/clipboard@0.3.3': + resolution: {integrity: sha512-e7jASirzfm+ROiOGFh843+cFZTy3DfzP+jldCvh8RnEk0C3QihDTn7dd7Yh7KAJydwIJ18FJSZ2swHvCJhk18g==} engines: {node: '>= 10'} '@mariozechner/jiti@2.6.5': resolution: {integrity: sha512-faGUlTcXka5l7rv0lP3K3vGW/ejRuOS24RR2aSFWREUQqzjgdsuWNo/IiPqL3kWRGt6Ahl2+qcDAwtdeWeuGUw==} hasBin: true - '@mariozechner/pi-agent-core@0.66.1': - resolution: {integrity: sha512-Nj54A7SuB/EQi8r3Gs+glFOr9wz/a9uxYFf0pCLf2DE7VmzA9O7WSejrvArna17K6auftLSdNyRRe2bIO0qezg==} + '@mariozechner/pi-agent-core@0.70.2': + resolution: {integrity: sha512-g1hIdKyDwmQOoBGO0R4OhpemKeMENeK0vE5FJtuQKqEcsdCAkVBgZAK6aZUARYZVxMA718JS6WPLFWoddzjD7g==} engines: {node: '>=20.0.0'} - '@mariozechner/pi-ai@0.66.1': - resolution: {integrity: sha512-7IZHvpsFdKEBkTmjNrdVL7JLUJVIpha6bwTr12cZ5XyDrxij06wP6Ncpnf4HT5BXAzD5w2JnoqTOSbMEIZj3dg==} + '@mariozechner/pi-ai@0.70.2': + resolution: {integrity: sha512-+30LRPjXsXF+oI96DvGWMbdPGeqoLJvadh6UPev7wx2DzhC9FEqXkQcoMZ0usbCm7E9pl8ua8a9s/pQ5ikaUbg==} engines: {node: '>=20.0.0'} hasBin: true - '@mariozechner/pi-coding-agent@0.66.1': - resolution: {integrity: sha512-cNmatT+5HvYzQ78cRhRih00wCeUTH/fFx9ecJh5AbN7axgWU+bwiZYy0cjrTsGVgMGF4xMYlPRn/Nze9JEB+/w==} + '@mariozechner/pi-coding-agent@0.70.2': + resolution: {integrity: sha512-asfNqV89HKAmKvJ1wENBY/UQMIf77kLtkzBrvXnMQV4YbH7D/6KT+VeVzPG6zm5PAZP2UtdLY9B9Cge7IxH37w==} engines: {node: '>=20.6.0'} hasBin: true - '@mariozechner/pi-tui@0.66.1': - resolution: {integrity: sha512-hNFN42ebjwtfGooqoUwM+QaPR1XCyqPuueuP3aLOWS1bZ2nZP/jq8MBuGNrmMw1cgiDcotvOlSNj3BatzEOGsw==} + '@mariozechner/pi-tui@0.70.2': + resolution: {integrity: sha512-PtKC0NepnrYcqMx6MXkWTrBzC9tI62KeC6w940oT46lCbfvgmfqXciR15+9BZpxxc1H4jd3CMrKsmOPVeUqZ0A==} engines: {node: '>=20.0.0'} - '@matrix-org/matrix-sdk-crypto-nodejs@0.4.0': - resolution: {integrity: sha512-+qqgpn39XFSbsD0dFjssGO9vHEP7sTyfs8yTpt8vuqWpUpF20QMwpCZi0jpYw7GxjErNTsMshopuo8677DfGEA==} - engines: {node: '>= 22'} - - '@matrix-org/matrix-sdk-crypto-wasm@18.0.0': - resolution: {integrity: sha512-88+n+dvxLI1cjS10UIlKXVYK7TGWbpAnnaDC9fow7ch/hCvdu3dFhJ3tS3/13N9s9+1QFXB4FFuommj+tHJPhQ==} - engines: {node: '>= 18'} - - '@mistralai/mistralai@1.14.1': - resolution: {integrity: sha512-IiLmmZFCCTReQgPAT33r7KQ1nYo5JPdvGkrkZqA8qQ2qB1GHgs5LoP5K2ICyrjnpw2n8oSxMM/VP+liiKcGNlQ==} + '@mistralai/mistralai@2.2.1': + resolution: {integrity: sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==} '@modelcontextprotocol/sdk@1.29.0': resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} @@ -1474,10 +1369,6 @@ packages: '@cfworker/json-schema': optional: true - '@mozilla/readability@0.6.0': - resolution: {integrity: sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==} - engines: {node: '>=14.0.0'} - '@napi-rs/canvas-android-arm64@0.1.80': resolution: {integrity: sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==} engines: {node: '>= 10'} @@ -1622,24 +1513,13 @@ packages: resolution: {integrity: sha512-8cFniXvrIEnVwuNSRCW9wirRZbHvrD3JVujdS2P5n5xiJZNZMOZcfOvJ1pb66c7jXMKHHglJEDVJGbm8XWFcXQ==} engines: {node: '>= 10'} - '@napi-rs/wasm-runtime@1.1.3': - resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - - '@noble/ciphers@2.1.1': - resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==} - engines: {node: '>= 20.19.0'} - - '@noble/curves@2.0.1': - resolution: {integrity: sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==} - engines: {node: '>= 20.19.0'} - '@noble/hashes@2.0.1': resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} engines: {node: '>= 20.19.0'} + '@nodable/entities@2.1.0': + resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1660,16 +1540,6 @@ packages: resolution: {integrity: sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==} engines: {node: ^18.17.0 || >=20.5.0} - '@pierre/diffs@1.1.13': - resolution: {integrity: sha512-lnX9Fy5eC+07b8g+D8krC3txOY6LRN5VNR1qr9bph9XEyLxbwwfGN7SFRu4HGozpkDdA76JARgxgWHN+uAihmg==} - peerDependencies: - react: ^18.3.1 || ^19.0.0 - react-dom: ^18.3.1 || ^19.0.0 - - '@pierre/theme@0.0.28': - resolution: {integrity: sha512-1j/H/fECBuc9dEvntdWI+l435HZapw+RCJTlqCA6BboQ5TjlnE005j/ROWutXIs8aq5OAc82JI2Kwk4A1WWBgw==} - engines: {vscode: ^1.0.0} - '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -2308,120 +2178,58 @@ packages: cpu: [x64] os: [win32] - '@scure/base@2.0.0': - resolution: {integrity: sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==} - - '@scure/bip32@2.0.1': - resolution: {integrity: sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==} - - '@scure/bip39@2.0.1': - resolution: {integrity: sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==} - - '@shikijs/core@3.23.0': - resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==} - - '@shikijs/engine-javascript@3.23.0': - resolution: {integrity: sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==} - - '@shikijs/engine-oniguruma@3.23.0': - resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} - - '@shikijs/langs@3.23.0': - resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==} - - '@shikijs/themes@3.23.0': - resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==} - - '@shikijs/transformers@3.23.0': - resolution: {integrity: sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ==} - - '@shikijs/types@3.23.0': - resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==} - - '@shikijs/vscode-textmate@10.0.2': - resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} - '@silvia-odwyer/photon-node@0.3.4': resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==} '@sinclair/typebox@0.34.48': resolution: {integrity: sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==} - '@sinclair/typebox@0.34.49': - resolution: {integrity: sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==} - '@sindresorhus/is@4.6.0': resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} - '@slack/bolt@4.7.0': - resolution: {integrity: sha512-Xpf+gKegNvkHpft1z4YiuqZdciJ3tUp1bIRQxylW30Ovf+hzjb0M1zTHVtJsRw9jsjPxHTPoyanEXVvG6qVE1g==} - engines: {node: '>=18', npm: '>=8.6.0'} - peerDependencies: - '@types/express': ^5.0.0 - - '@slack/logger@4.0.1': - resolution: {integrity: sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ==} - engines: {node: '>= 18', npm: '>= 8.6.0'} - - '@slack/oauth@3.0.5': - resolution: {integrity: sha512-exqFQySKhNDptWYSWhvRUJ4/+ndu2gayIy7vg/JfmJq3wGtGdHk531P96fAZyBm5c1Le3yaPYqv92rL4COlU3A==} - engines: {node: '>=18', npm: '>=8.6.0'} - - '@slack/socket-mode@2.0.6': - resolution: {integrity: sha512-Aj5RO3MoYVJ+b2tUjHUXuA3tiIaCUMOf1Ss5tPiz29XYVUi6qNac2A8ulcU1pUPERpXVHTmT1XW6HzQIO74daQ==} - engines: {node: '>= 18', npm: '>= 8.6.0'} - - '@slack/types@2.20.1': - resolution: {integrity: sha512-eWX2mdt1ktpn8+40iiMc404uGrih+2fxiky3zBcPjtXKj6HLRdYlmhrPkJi7JTJm8dpXR6BWVWEDBXtaWMKD6A==} - engines: {node: '>= 12.13.0', npm: '>= 6.12.0'} - - '@slack/web-api@7.15.0': - resolution: {integrity: sha512-va7zYIt3QHG1x9M/jqXXRPFMoOVlVSSRHC5YH+DzKYsrz5xUKOA3lR4THsu/Zxha9N1jOndbKFKLtr0WOPW1Vw==} - engines: {node: '>= 18', npm: '>= 8.6.0'} - - '@smithy/config-resolver@4.4.14': - resolution: {integrity: sha512-N55f8mPEccpzKetUagdvmAy8oohf0J5cuj9jLI1TaSceRlq0pJsIZepY3kmAXAhyxqXPV6hDerDQhqQPKWgAoQ==} + '@smithy/config-resolver@4.4.17': + resolution: {integrity: sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ==} engines: {node: '>=18.0.0'} - '@smithy/core@3.23.14': - resolution: {integrity: sha512-vJ0IhpZxZAkFYOegMKSrxw7ujhhT2pass/1UEcZ4kfl5srTAqtPU5I7MdYQoreVas3204ykCiNhY1o7Xlz6Yyg==} + '@smithy/core@3.23.17': + resolution: {integrity: sha512-x7BlLbUFL8NWCGjMF9C+1N5cVCxcPa7g6Tv9B4A2luWx3be3oU8hQ96wIwxe/s7OhIzvoJH73HAUSg5JXVlEtQ==} engines: {node: '>=18.0.0'} - '@smithy/credential-provider-imds@4.2.13': - resolution: {integrity: sha512-wboCPijzf6RJKLOvnjDAiBxGSmSnGXj35o5ZAWKDaHa/cvQ5U3ZJ13D4tMCE8JG4dxVAZFy/P0x/V9CwwdfULQ==} + '@smithy/credential-provider-imds@4.2.14': + resolution: {integrity: sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-codec@4.2.13': - resolution: {integrity: sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ==} + '@smithy/eventstream-codec@4.2.14': + resolution: {integrity: sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-browser@4.2.13': - resolution: {integrity: sha512-wwybfcOX0tLqCcBP378TIU9IqrDuZq/tDV48LlZNydMpCnqnYr+hWBAYbRE+rFFf/p7IkDJySM3bgiMKP2ihPg==} + '@smithy/eventstream-serde-browser@4.2.14': + resolution: {integrity: sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-config-resolver@4.3.13': - resolution: {integrity: sha512-ied1lO559PtAsMJzg2TKRlctLnEi1PfkNeMMpdwXDImk1zV9uvS/Oxoy/vcy9uv1GKZAjDAB5xT6ziE9fzm5wA==} + '@smithy/eventstream-serde-config-resolver@4.3.14': + resolution: {integrity: sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-node@4.2.13': - resolution: {integrity: sha512-hFyK+ORJrxAN3RYoaD6+gsGDQjeix8HOEkosoajvXYZ4VeqonM3G4jd9IIRm/sWGXUKmudkY9KdYjzosUqdM8A==} + '@smithy/eventstream-serde-node@4.2.14': + resolution: {integrity: sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-universal@4.2.13': - resolution: {integrity: sha512-kRrq4EKLGeOxhC2CBEhRNcu1KSzNJzYY7RK3S7CxMPgB5dRrv55WqQOtRwQxQLC04xqORFLUgnDlc6xrNUULaA==} + '@smithy/eventstream-serde-universal@4.2.14': + resolution: {integrity: sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg==} engines: {node: '>=18.0.0'} - '@smithy/fetch-http-handler@5.3.16': - resolution: {integrity: sha512-nYDRUIvNd4mFmuXraRWt6w5UsZTNqtj4hXJA/iiOD4tuseIdLP9Lq38teH/SZTcIFCa2f+27o7hYpIsWktJKEQ==} + '@smithy/fetch-http-handler@5.3.17': + resolution: {integrity: sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==} engines: {node: '>=18.0.0'} - '@smithy/hash-node@4.2.13': - resolution: {integrity: sha512-4/oy9h0jjmY80a2gOIo75iLl8TOPhmtx4E2Hz+PfMjvx/vLtGY4TMU/35WRyH2JHPfT5CVB38u4JRow7gnmzJA==} + '@smithy/hash-node@4.2.14': + resolution: {integrity: sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==} engines: {node: '>=18.0.0'} - '@smithy/invalid-dependency@4.2.13': - resolution: {integrity: sha512-jvC0RB/8BLj2SMIkY0Npl425IdnxZJxInpZJbu563zIRnVjpDMXevU3VMCRSabaLB0kf/eFIOusdGstrLJ8IDg==} + '@smithy/invalid-dependency@4.2.14': + resolution: {integrity: sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==} engines: {node: '>=18.0.0'} '@smithy/is-array-buffer@2.2.0': @@ -2432,72 +2240,72 @@ packages: resolution: {integrity: sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==} engines: {node: '>=18.0.0'} - '@smithy/middleware-content-length@4.2.13': - resolution: {integrity: sha512-IPMLm/LE4AZwu6qiE8Rr8vJsWhs9AtOdySRXrOM7xnvclp77Tyh7hMs/FRrMf26kgIe67vFJXXOSmVxS7oKeig==} + '@smithy/middleware-content-length@4.2.14': + resolution: {integrity: sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==} engines: {node: '>=18.0.0'} - '@smithy/middleware-endpoint@4.4.29': - resolution: {integrity: sha512-R9Q/58U+qBiSARGWbAbFLczECg/RmysRksX6Q8BaQEpt75I7LI6WGDZnjuC9GXSGKljEbA7N118LhGaMbfrTXw==} + '@smithy/middleware-endpoint@4.4.32': + resolution: {integrity: sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q==} engines: {node: '>=18.0.0'} - '@smithy/middleware-retry@4.5.0': - resolution: {integrity: sha512-/NzISn4grj/BRFVua/xnQwF+7fakYZgimpw2dfmlPgcqecBMKxpB9g5mLYRrmBD5OrPoODokw4Vi1hrSR4zRyw==} + '@smithy/middleware-retry@4.5.6': + resolution: {integrity: sha512-5zhmo2AkstmM/RMKYP0NHfmuYWBR+/umlmSuALgajLxf0X0rLE6d17MfzTxpzkILWVhwvCJkCyPH0AfMlbaucQ==} engines: {node: '>=18.0.0'} - '@smithy/middleware-serde@4.2.17': - resolution: {integrity: sha512-0T2mcaM6v9W1xku86Dk0bEW7aEseG6KenFkPK98XNw0ZhOqOiD1MrMsdnQw9QsL3/Oa85T53iSMlm0SZdSuIEQ==} + '@smithy/middleware-serde@4.2.20': + resolution: {integrity: sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ==} engines: {node: '>=18.0.0'} - '@smithy/middleware-stack@4.2.13': - resolution: {integrity: sha512-g72jN/sGDLyTanrCLH9fhg3oysO3f7tQa6eWWsMyn2BiYNCgjF24n4/I9wff/5XidFvjj9ilipAoQrurTUrLvw==} + '@smithy/middleware-stack@4.2.14': + resolution: {integrity: sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==} engines: {node: '>=18.0.0'} - '@smithy/node-config-provider@4.3.13': - resolution: {integrity: sha512-iGxQ04DsKXLckbgnX4ipElrOTk+IHgTyu0q0WssZfYhDm9CQWHmu6cOeI5wmWRxpXbBDhIIfXMWz5tPEtcVqbw==} + '@smithy/node-config-provider@4.3.14': + resolution: {integrity: sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==} engines: {node: '>=18.0.0'} - '@smithy/node-http-handler@4.5.2': - resolution: {integrity: sha512-/oD7u8M0oj2ZTFw7GkuuHWpIxtWdLlnyNkbrWcyVYhd5RJNDuczdkb0wfnQICyNFrVPlr8YHOhamjNy3zidhmA==} + '@smithy/node-http-handler@4.6.1': + resolution: {integrity: sha512-iB+orM4x3xrr57X3YaXazfKnntl0LHlZB1kcXSGzMV1Tt0+YwEjGlbjk/44qEGtBzXAz6yFDzkYTKSV6Pj2HUg==} engines: {node: '>=18.0.0'} - '@smithy/property-provider@4.2.13': - resolution: {integrity: sha512-bGzUCthxRmezuxkbu9wD33wWg9KX3hJpCXpQ93vVkPrHn9ZW6KNNdY5xAUWNuRCwQ+VyboFuWirG1lZhhkcyRQ==} + '@smithy/property-provider@4.2.14': + resolution: {integrity: sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==} engines: {node: '>=18.0.0'} - '@smithy/protocol-http@5.3.13': - resolution: {integrity: sha512-+HsmuJUF4u8POo6s8/a2Yb/AQ5t/YgLovCuHF9oxbocqv+SZ6gd8lC2duBFiCA/vFHoHQhoq7QjqJqZC6xOxxg==} + '@smithy/protocol-http@5.3.14': + resolution: {integrity: sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==} engines: {node: '>=18.0.0'} - '@smithy/querystring-builder@4.2.13': - resolution: {integrity: sha512-tG4aOYFCZdPMjbgfhnIQ322H//ojujldp1SrHPHpBSb3NqgUp3dwiUGRJzie87hS1DYwWGqDuPaowoDF+rYCbQ==} + '@smithy/querystring-builder@4.2.14': + resolution: {integrity: sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==} engines: {node: '>=18.0.0'} - '@smithy/querystring-parser@4.2.13': - resolution: {integrity: sha512-hqW3Q4P+CDzUyQ87GrboGMeD7XYNMOF+CuTwu936UQRB/zeYn3jys8C3w+wMkDfY7CyyyVwZQ5cNFoG0x1pYmA==} + '@smithy/querystring-parser@4.2.14': + resolution: {integrity: sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==} engines: {node: '>=18.0.0'} - '@smithy/service-error-classification@4.2.13': - resolution: {integrity: sha512-a0s8XZMfOC/qpqq7RCPvJlk93rWFrElH6O++8WJKz0FqnA4Y7fkNi/0mnGgSH1C4x6MFsuBA8VKu4zxFrMe5Vw==} + '@smithy/service-error-classification@4.3.1': + resolution: {integrity: sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==} engines: {node: '>=18.0.0'} - '@smithy/shared-ini-file-loader@4.4.8': - resolution: {integrity: sha512-VZCZx2bZasxdqxVgEAhREvDSlkatTPnkdWy1+Kiy8w7kYPBosW0V5IeDwzDUMvWBt56zpK658rx1cOBFOYaPaw==} + '@smithy/shared-ini-file-loader@4.4.9': + resolution: {integrity: sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==} engines: {node: '>=18.0.0'} - '@smithy/signature-v4@5.3.13': - resolution: {integrity: sha512-YpYSyM0vMDwKbHD/JA7bVOF6kToVRpa+FM5ateEVRpsTNu564g1muBlkTubXhSKKYXInhpADF46FPyrZcTLpXg==} + '@smithy/signature-v4@5.3.14': + resolution: {integrity: sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==} engines: {node: '>=18.0.0'} - '@smithy/smithy-client@4.12.9': - resolution: {integrity: sha512-ovaLEcTU5olSeHcRXcxV6viaKtpkHZumn6Ps0yn7dRf2rRSfy794vpjOtrWDO0d1auDSvAqxO+lyhERSXQ03EQ==} + '@smithy/smithy-client@4.12.13': + resolution: {integrity: sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA==} engines: {node: '>=18.0.0'} - '@smithy/types@4.14.0': - resolution: {integrity: sha512-OWgntFLW88kx2qvf/c/67Vno1yuXm/f9M7QFAtVkkO29IJXGBIg0ycEaBTH0kvCtwmvZxRujrgP5a86RvsXJAQ==} + '@smithy/types@4.14.1': + resolution: {integrity: sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==} engines: {node: '>=18.0.0'} - '@smithy/url-parser@4.2.13': - resolution: {integrity: sha512-2G03yoboIRZlZze2+PT4GZEjgwQsJjUgn6iTsvxA02bVceHR6vp4Cuk7TUnPFWKF+ffNUk3kj4COwkENS2K3vw==} + '@smithy/url-parser@4.2.14': + resolution: {integrity: sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==} engines: {node: '>=18.0.0'} '@smithy/util-base64@4.3.2': @@ -2524,32 +2332,32 @@ packages: resolution: {integrity: sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-browser@4.3.45': - resolution: {integrity: sha512-ag9sWc6/nWZAuK3Wm9KlFJUnRkXLrXn33RFjIAmCTFThqLHY+7wCst10BGq56FxslsDrjhSie46c8OULS+BiIw==} + '@smithy/util-defaults-mode-browser@4.3.49': + resolution: {integrity: sha512-a5bNrdiONYB/qE2BuKegvUMd/+ZDwdg4vsNuuSzYE8qs2EYAdK9CynL+Rzn29PbPiUqoz/cbpRbcLzD5lEevHw==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-node@4.2.49': - resolution: {integrity: sha512-jlN6vHwE8gY5AfiFBavtD3QtCX2f7lM3BKkz7nFKSNfFR5nXLXLg6sqXTJEEyDwtxbztIDBQCfjsGVXlIru2lQ==} + '@smithy/util-defaults-mode-node@4.2.54': + resolution: {integrity: sha512-g1cvrJvOnzeJgEdf7AE4luI7gp6L8weE0y9a9wQUSGtjb8QRHDbCJYuE4Sy0SD9N8RrnNPFsPltAz/OSoBR9Zw==} engines: {node: '>=18.0.0'} - '@smithy/util-endpoints@3.3.4': - resolution: {integrity: sha512-BKoR/ubPp9KNKFxPpg1J28N1+bgu8NGAtJblBP7yHy8yQPBWhIAv9+l92SlQLpolGm71CVO+btB60gTgzT0wog==} + '@smithy/util-endpoints@3.4.2': + resolution: {integrity: sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg==} engines: {node: '>=18.0.0'} '@smithy/util-hex-encoding@4.2.2': resolution: {integrity: sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==} engines: {node: '>=18.0.0'} - '@smithy/util-middleware@4.2.13': - resolution: {integrity: sha512-GTooyrlmRTqvUen4eK7/K1p6kryF7bnDfq6XsAbIsf2mo51B/utaH+XThY6dKgNCWzMAaH/+OLmqaBuLhLWRow==} + '@smithy/util-middleware@4.2.14': + resolution: {integrity: sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==} engines: {node: '>=18.0.0'} - '@smithy/util-retry@4.3.0': - resolution: {integrity: sha512-tSOPQNT/4KfbvqeMovWC3g23KSYy8czHd3tlN+tOYVNIDLSfxIsrPJihYi5TpNcoV789KWtgChUVedh2y6dDPg==} + '@smithy/util-retry@4.3.5': + resolution: {integrity: sha512-h1IJsbgMDA+jaTjrco/JsyfWOgHRJBv8myB1y4AEI2fjIzD6ktZ7pFAyTw+gwN9GKIAygvC6db0mq0j8N2rFOg==} engines: {node: '>=18.0.0'} - '@smithy/util-stream@4.5.22': - resolution: {integrity: sha512-3H8iq/0BfQjUs2/4fbHZ9aG9yNzcuZs24LPkcX1Q7Z+qpqaGM8+qbGmE8zo9m2nCRgamyvS98cHdcWvR6YUsew==} + '@smithy/util-stream@4.5.25': + resolution: {integrity: sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA==} engines: {node: '>=18.0.0'} '@smithy/util-uri-escape@4.2.2': @@ -2568,97 +2376,6 @@ packages: resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==} engines: {node: '>=18.0.0'} - '@snazzah/davey-android-arm-eabi@0.1.11': - resolution: {integrity: sha512-T1RYbNYKN6tLOcGIDKJd8OI6FBSEemwL7DOYdTMmhqfhhMr3YVN8WOhfoxGg63OcnpTN2e2c5tdY2bAx25RmQQ==} - engines: {node: '>= 10'} - cpu: [arm] - os: [android] - - '@snazzah/davey-android-arm64@0.1.11': - resolution: {integrity: sha512-ksJn/x2VU8h6w9eku1HT96ugSRZ7lKVkKNKbFleaFN+U99DJaPM+gMu2YvnFU4V54HR06ZBnRihnVG6VLXQpDw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [android] - - '@snazzah/davey-darwin-arm64@0.1.11': - resolution: {integrity: sha512-E1d7PbaaVMO3Lj9EiAPqOVbuV0xg5+PsHzHH097DDXiD1+zUDXvJaTnUWsnm5z50pJniHpi4GtaYmk+ieB/guA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@snazzah/davey-darwin-x64@0.1.11': - resolution: {integrity: sha512-Tl4TI/LTmgJZepgbgVMYDi8RqlAkPtPg1OEBPl7a9Tn3AwR36Vs6lyIT1cs/lGy/ds/+B+mKI4rPObN1cyILTw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@snazzah/davey-freebsd-x64@0.1.11': - resolution: {integrity: sha512-T8Iw9FXkuI1T+YBAFzh9v/TXf9IOTOSqnd/BFpTRTrlW72PR2lhIidzSmg027VxO7r5pX47iFwiOkb9I/NU/EA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [freebsd] - - '@snazzah/davey-linux-arm-gnueabihf@0.1.11': - resolution: {integrity: sha512-1Txj+8pqA8uq/OGtaUaBFWAPnNMQzFgIywj0iA7EI4xZl+mab48/pv+YZ1pNb/suC6ynsW44oB9efiXSdcUAgA==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - - '@snazzah/davey-linux-arm64-gnu@0.1.11': - resolution: {integrity: sha512-ERzF5nM/IYW1BcN3wLXpEwBCGLFf0kGJUVhaV6yfiInz0tkU8UmvrrgpaMaACfMjIhfWdq5CcX+aTkXo/saNcg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@snazzah/davey-linux-arm64-musl@0.1.11': - resolution: {integrity: sha512-e6pX6Hiabtz99q+H/YHNkm9JVlpqN8HGh0qPib8G2+UY4/SSH8WvqWipk3v581dMy2oyCHt7MOoY1aU1P1N/xA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@snazzah/davey-linux-x64-gnu@0.1.11': - resolution: {integrity: sha512-TW5bSoqChOJMbvsDb4wAATYrxmAXuNnse7wFNVSAJUaZKSeRfZbu3UAiPWSNn7GwLwSfU6hg322KZUn8IWCuvg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@snazzah/davey-linux-x64-musl@0.1.11': - resolution: {integrity: sha512-5j6Pmc+Wzv5lSxVP6quA7teYRJXibkZqQyYGfTDnTsUOO5dPpcojpqlXlkhyvsA1OAQTj4uxbOCciN3cVWwzug==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@snazzah/davey-wasm32-wasi@0.1.11': - resolution: {integrity: sha512-rKOwZ/0J8lp+4VEyOdMDBRP9KR+PksZpa9V1Qn0veMzy4FqTVKthkxwGqewheFe0SFg9fdvt798l/PBFrfDeZw==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - - '@snazzah/davey-win32-arm64-msvc@0.1.11': - resolution: {integrity: sha512-5fptJU4tX901m3mj0SHiBljMrPT4ZEsynbBhR7bK1yn9TY1jjyhN8EFi7QF5IWtUEni+0mia2BCMHZ5ZkmFZqQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@snazzah/davey-win32-ia32-msvc@0.1.11': - resolution: {integrity: sha512-ualexn8SeLsiMHhWfzVrzRcjHgcBapg++FPaVgJJxoh2S/jCRiklXOu3luqIZdJdNKvhe2V9SwO/cImPeIIBKw==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - - '@snazzah/davey-win32-x64-msvc@0.1.11': - resolution: {integrity: sha512-muNhc8UKXtknzsH/w4AIkbPR2I8BuvApn0pDXar0IEvY8PCjqU/M8MPbOOEYwQVvQRMwVTgExtxzrkBPSXB4nA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - '@snazzah/davey@0.1.11': - resolution: {integrity: sha512-oBN+msHzPnm1M5DDx3wVD7iBwpNXFUtkh2MrAbUJu0OhKjliLChi28hq++mu1+qdMpAVQO5JKAvQQxYVbyneiw==} - engines: {node: '>= 10'} - '@soimy/dingtalk@3.5.3': resolution: {integrity: sha512-I8y57KVic6Gjg/BmaZBcoV+ktW/riM/TFW/XzqhqL4beNicYcy8nxuyxqpLuFQNw0f4KU363aZl0BkmW07Wmjg==} peerDependencies: @@ -2670,15 +2387,12 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@swc/helpers@0.5.21': - resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==} - '@szmarczak/http-timer@4.0.6': resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} - '@tencent-weixin/openclaw-weixin@2.1.8': - resolution: {integrity: sha512-YM2fumDI+NvslhFH4gsek+scgCwTdyz7eMDyfNadCTXPjh9hoosn8tcMF0P90gQJEGEud7AJXiKKd8IKGNCfRA==} + '@tencent-weixin/openclaw-weixin@2.1.10': + resolution: {integrity: sha512-cEG6Iw5g2qqlA+8/TcmV+E8aFUEX0ruxF0+a5LgVy5wv56/qP07KoapfRa7YTRPzhRW5UDaz6zsZQArt/4ZNnA==} engines: {node: '>=22'} '@testing-library/dom@10.4.1': @@ -2714,9 +2428,6 @@ packages: '@tootallnate/quickjs-emscripten@0.23.0': resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -2732,27 +2443,12 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/body-parser@1.19.6': - resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} - - '@types/bun@1.3.11': - resolution: {integrity: sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg==} - '@types/cacheable-request@6.0.3': resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@types/command-line-args@5.2.3': - resolution: {integrity: sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==} - - '@types/command-line-usage@5.0.4': - resolution: {integrity: sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==} - - '@types/connect@3.4.38': - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} @@ -2768,15 +2464,6 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/events@3.0.3': - resolution: {integrity: sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==} - - '@types/express-serve-static-core@5.1.1': - resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} - - '@types/express@5.0.6': - resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} - '@types/fs-extra@9.0.13': resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} @@ -2786,15 +2473,9 @@ packages: '@types/http-cache-semantics@4.2.0': resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} - '@types/http-errors@2.0.5': - resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} - '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/jsonwebtoken@9.0.10': - resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} - '@types/katex@0.16.8': resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==} @@ -2819,9 +2500,6 @@ packages: '@types/node@16.9.1': resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==} - '@types/node@20.19.39': - resolution: {integrity: sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==} - '@types/node@24.12.0': resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==} @@ -2834,12 +2512,6 @@ packages: '@types/plist@3.0.5': resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==} - '@types/qs@6.15.0': - resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} - - '@types/range-parser@1.2.7': - resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -2854,12 +2526,6 @@ packages: '@types/retry@0.12.0': resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} - '@types/send@1.2.1': - resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} - - '@types/serve-static@2.2.0': - resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} - '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -2937,6 +2603,11 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@vincentkoc/qrcode-tui@0.2.1': + resolution: {integrity: sha512-F2XVHMfasJ0q8G93gtcyU9Px0wMH6o6nIZLrZYSHc6dm9Pq3oCbHuVYYG/UQvJD0rhrGH3P9B6qgpCAqSDUw5w==} + engines: {node: '>=20'} + hasBin: true + '@vitejs/plugin-react@5.2.0': resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2972,14 +2643,11 @@ packages: '@vitest/utils@4.1.1': resolution: {integrity: sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==} - '@wasm-audio-decoders/common@9.0.7': - resolution: {integrity: sha512-WRaUuWSKV7pkttBygml/a6dIEpatq2nnZGFIoPTc5yPLkxL6Wk4YaslPM98OPQvWacvNZ+Py9xROGDtrFBDzag==} - '@wecom/aibot-node-sdk@1.0.6': resolution: {integrity: sha512-WZJN3Q+s+94Qjc0VW8d5W1cVkA3emYxiqf+mNRO9UEHoF40puHvizreNMtudjFhm7mmkYiK5ue/QzNiCk+xwLA==} - '@wecom/wecom-openclaw-plugin@2026.4.8': - resolution: {integrity: sha512-bGbS8493sHT34FAYaugep2OLjb4dvYfLXMSfZguK/4s8i5PM//2iy4XPUsniacl8LNSUxiF/FuQGM/LJxNwrFg==} + '@wecom/wecom-openclaw-plugin@2026.4.27': + resolution: {integrity: sha512-jCK9VDS3kmxyGdqPMNX78vI58iPwypW92eKNZI6T5RLyv9iBUkBMC6RcJmq6LLaq2/bCA3wBcmkjH44W42X2aw==} '@whiskeysockets/baileys@7.0.0-rc.9': resolution: {integrity: sha512-YFm5gKXfDP9byCXCW3OPHKXLzrAKzolzgVUlRosHHgwbnf2YOO3XknkMm6J7+F0ns8OA0uuSBhgkRHTDtqkacw==} @@ -3005,17 +2673,10 @@ packages: resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} engines: {node: '>=10.0.0'} - abbrev@1.1.1: - resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} - abbrev@3.0.1: resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} engines: {node: ^18.17.0 || >=20.5.0} - abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} - engines: {node: '>=6.5'} - accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -3057,9 +2718,6 @@ packages: ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} - another-json@0.2.0: - resolution: {integrity: sha512-/Ndrl68UQLhnCdsAzEXLMFuOR546o2qbYRqCglaNHbjXrwG1ayTcdwr3zkSGOGtGXDyR5X9nCFfnyG2AFJIsqg==} - ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -3090,10 +2748,6 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - apache-arrow@18.1.0: - resolution: {integrity: sha512-v/ShMp57iBnBp4lDgV8Jx3d3Q5/Hac25FWmQ98eMahUiHPXcvwIMKJD0hBIgclm/FCG+LwPkAKtkRO1O/W0YGg==} - hasBin: true - app-builder-bin@5.0.0-alpha.12: resolution: {integrity: sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==} @@ -3104,14 +2758,6 @@ packages: dmg-builder: 26.8.1 electron-builder-squirrel-windows: 26.8.1 - aproba@2.1.0: - resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} - - are-we-there-yet@2.0.0: - resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} - engines: {node: '>=10'} - deprecated: This package is no longer supported. - arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} @@ -3138,13 +2784,8 @@ packages: arktype@2.2.0: resolution: {integrity: sha512-t54MZ7ti5BhOEvzEkgKnWvqj+UbDfWig+DHr5I34xatymPusKLS0lQpNJd8M6DzmIto2QGszHfNKoFIT8tMCZQ==} - array-back@3.1.0: - resolution: {integrity: sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==} - engines: {node: '>=6'} - - array-back@6.2.3: - resolution: {integrity: sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw==} - engines: {node: '>=12.17'} + asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} assert-plus@1.0.0: resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} @@ -3210,9 +2851,6 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - base-x@5.0.1: - resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} - base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -3224,6 +2862,7 @@ packages: basic-ftp@5.2.0: resolution: {integrity: sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==} engines: {node: '>=10.0.0'} + deprecated: Security vulnerability fixed in 5.2.1, please upgrade bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} @@ -3244,20 +2883,17 @@ packages: bmp-ts@1.0.9: resolution: {integrity: sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==} + bn.js@4.12.3: + resolution: {integrity: sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==} + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} - boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - boolean@3.2.0: resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - bottleneck@2.19.5: - resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} - bowser@2.14.1: resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} @@ -3280,9 +2916,6 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - bs58@6.0.0: - resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} - buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} @@ -3302,9 +2935,6 @@ packages: builder-util@26.8.1: resolution: {integrity: sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==} - bun-types@1.3.11: - resolution: {integrity: sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg==} - bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -3336,6 +2966,10 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + caniuse-lite@1.0.30001781: resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==} @@ -3346,10 +2980,6 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} - chalk-template@0.4.0: - resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} - engines: {node: '>=12'} - chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -3430,6 +3060,9 @@ packages: resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} engines: {node: '>=8'} + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} @@ -3455,10 +3088,6 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - color-support@1.1.3: - resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} - hasBin: true - combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -3466,14 +3095,6 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} - command-line-args@5.2.1: - resolution: {integrity: sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==} - engines: {node: '>=4.0.0'} - - command-line-usage@7.0.4: - resolution: {integrity: sha512-85UdvzTNx/+s5CkSgBm/0hzP80RFHAa7PsfeADE5ezZF3uHz3/Tqj9gIKGT9PTtpycc3Ua64T0oVulGfKxzfqg==} - engines: {node: '>=12.20.0'} - commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} @@ -3505,9 +3126,6 @@ packages: resolution: {integrity: sha512-Uy5YN9KEu0WWDaZAVJ5FAmZoaJt9rdK6kH+utItPyGsCqCgaTKkrmZx3zoE0/3q6S3bcp3Ihkk+ZqPxWxFK5og==} engines: {node: '>=20'} - console-control-strings@1.1.0: - resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} - content-disposition@1.0.1: resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} engines: {node: '>=18'} @@ -3555,17 +3173,10 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - css-select@5.2.2: - resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} - css-tree@3.2.1: resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - css-what@6.2.2: - resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} - engines: {node: '>= 6'} - css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} @@ -3574,9 +3185,6 @@ packages: engines: {node: '>=4'} hasBin: true - cssom@0.5.0: - resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} - cssstyle@6.2.0: resolution: {integrity: sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==} engines: {node: '>=20'} @@ -3616,6 +3224,10 @@ packages: supports-color: optional: true + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} @@ -3658,9 +3270,6 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - delegates@1.0.0: - resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} - depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -3685,14 +3294,13 @@ packages: didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} - diff@8.0.3: - resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} - engines: {node: '>=0.3.1'} - diff@8.0.4: resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} engines: {node: '>=0.3.1'} + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + dingbat-to-unicode@1.0.1: resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==} @@ -3702,9 +3310,6 @@ packages: dir-compare@4.2.0: resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} - discord-api-types@0.38.45: - resolution: {integrity: sha512-DiI01i00FPv6n+hXcFkFxK8Y/rFRpKs6U6aP32N4T73nTbj37Eua3H/95TBpLktLWB6xnLXhYDGvyLq6zzYY2w==} - dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} @@ -3723,19 +3328,6 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} - dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - - domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - - domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} - - domutils@3.2.2: - resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - dot-prop@10.1.0: resolution: {integrity: sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==} engines: {node: '>=20'} @@ -3748,8 +3340,8 @@ packages: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} - dotenv@17.4.1: - resolution: {integrity: sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==} + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} duck@0.1.12: @@ -3827,10 +3419,6 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} - entities@7.0.1: - resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} - engines: {node: '>=0.12'} - env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -3957,20 +3545,9 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} - engines: {node: '>=6'} - - eventemitter3@4.0.7: - resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} - events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} - eventsource-parser@3.0.6: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} @@ -4011,10 +3588,6 @@ packages: resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==} engines: {'0': node >=0.6.0} - fake-indexeddb@6.2.5: - resolution: {integrity: sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==} - engines: {node: '>=18'} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -4043,12 +3616,15 @@ packages: fast-xml-builder@1.1.4: resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} + fast-xml-builder@1.1.5: + resolution: {integrity: sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==} + fast-xml-parser@5.5.10: resolution: {integrity: sha512-go2J2xODMc32hT+4Xr/bBGXMaIoiCwrwp2mMtAvKyvEFW6S/v5Gn2pBmE4nvbwNjGhpcAiOwEv7R6/GZ6XRa9w==} hasBin: true - fast-xml-parser@5.5.8: - resolution: {integrity: sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==} + fast-xml-parser@5.7.2: + resolution: {integrity: sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==} hasBin: true fastq@1.20.1: @@ -4096,9 +3672,9 @@ packages: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} - find-replace@3.0.0: - resolution: {integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==} - engines: {node: '>=4.0.0'} + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} @@ -4108,9 +3684,6 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatbuffers@24.12.23: - resolution: {integrity: sha512-dLVCAISd5mhls514keQzmEG6QHmUUsNuWsb4tFafIUwvvgDjXhtfAYSKOzt5SWOy+qByV5pbsDZ+Vb7HUOBEdA==} - flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} @@ -4204,23 +3777,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - gauge@3.0.2: - resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} - engines: {node: '>=10'} - deprecated: This package is no longer supported. - - gaxios@6.7.1: - resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} - engines: {node: '>=14'} - gaxios@7.1.4: resolution: {integrity: sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==} engines: {node: '>=18'} - gcp-metadata@6.1.1: - resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} - engines: {node: '>=14'} - gcp-metadata@8.1.2: resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} engines: {node: '>=18'} @@ -4301,14 +3861,6 @@ packages: resolution: {integrity: sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==} engines: {node: '>=18'} - google-auth-library@9.15.1: - resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} - engines: {node: '>=14'} - - google-logging-utils@0.0.2: - resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} - engines: {node: '>=14'} - google-logging-utils@1.1.3: resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} engines: {node: '>=14'} @@ -4324,14 +3876,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - grammy@1.42.0: - resolution: {integrity: sha512-1AdCge+AkjSdp2FwfICSFnVbl8Mq3KVHJDy+DgTI9+D6keJ0zWALPRKas5jv/8psiCzL4N2cEOcGW7O45Kn39g==} - engines: {node: ^12.20.0 || >=14.13.1} - - gtoken@7.1.0: - resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} - engines: {node: '>=14.0.0'} - has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -4347,9 +3891,6 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} - has-unicode@2.0.1: - resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} - hashery@1.5.1: resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} engines: {node: '>=20'} @@ -4376,9 +3917,6 @@ packages: hast-util-parse-selector@4.0.0: resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} - hast-util-to-html@9.0.5: - resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} - hast-util-to-jsx-runtime@2.3.6: resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} @@ -4422,21 +3960,12 @@ packages: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - html-escaper@3.0.3: - resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} - html-parse-stringify@3.0.1: resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} - html-void-elements@3.0.0: - resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - - htmlparser2@10.1.0: - resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} - http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} @@ -4456,6 +3985,10 @@ packages: resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} engines: {node: '>=10.19.0'} + http_ece@1.2.0: + resolution: {integrity: sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==} + engines: {node: '>=16'} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -4550,9 +4083,6 @@ packages: is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} - is-electron@2.2.2: - resolution: {integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==} - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -4594,10 +4124,6 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -4670,10 +4196,6 @@ packages: json-bigint@1.0.0: resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} - json-bignum@0.0.3: - resolution: {integrity: sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==} - engines: {node: '>=0.8'} - json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -4707,10 +4229,6 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} - jsonwebtoken@9.0.3: - resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} - engines: {node: '>=12', npm: '>=6'} - jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} @@ -4720,10 +4238,6 @@ packages: jws@4.0.1: resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} - jwt-decode@4.0.0: - resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} - engines: {node: '>=18'} - katex@0.16.45: resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==} hasBin: true @@ -4754,59 +4268,30 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - linkedom@0.18.12: - resolution: {integrity: sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q==} - engines: {node: '>=16'} - peerDependencies: - canvas: '>= 2' - peerDependenciesMeta: - canvas: - optional: true - linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash.camelcase@4.3.0: - resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} - lodash.escaperegexp@4.1.2: resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} lodash.identity@3.0.0: resolution: {integrity: sha512-AupTIzdLQxJS5wIYUQlgGyk2XRTfGXA+MCghDHqZk0pzUNYvd3EESS6dkChNauNYVIutcb0dfHw1ri9Q1yPV8Q==} - lodash.includes@4.3.0: - resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} - - lodash.isboolean@3.0.3: - resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} - lodash.isequal@4.5.0: resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. - lodash.isinteger@4.0.4: - resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} - - lodash.isnumber@3.0.3: - resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} - - lodash.isplainobject@4.0.6: - resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} - - lodash.isstring@4.0.1: - resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} - lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.once@4.1.1: - resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} - lodash.pickby@4.6.0: resolution: {integrity: sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==} @@ -4821,10 +4306,6 @@ packages: resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} engines: {node: '>=18'} - loglevel@1.9.2: - resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} - engines: {node: '>= 0.6.0'} - long@4.0.0: resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} @@ -4859,9 +4340,6 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} - lru_map@0.4.1: - resolution: {integrity: sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg==} - lucide-react@0.563.0: resolution: {integrity: sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==} peerDependencies: @@ -4874,10 +4352,6 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - make-dir@3.1.0: - resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} - engines: {node: '>=8'} - make-fetch-happen@14.0.3: resolution: {integrity: sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==} engines: {node: ^18.17.0 || >=20.5.0} @@ -4907,16 +4381,6 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - matrix-events-sdk@0.0.1: - resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==} - - matrix-js-sdk@41.3.0: - resolution: {integrity: sha512-QTNHpBQEKPH3WS4O92CBfFj6GxeyijT8osI/QxNvOrM3rE6CySXRtRRKnzR0ntFSdrk1CxrDGV6h2wmk7B3peQ==} - engines: {node: '>=22.0.0'} - - matrix-widget-api@1.17.0: - resolution: {integrity: sha512-5FHoo3iEP3Bdlv5jsYPWOqj+pGdFQNLWnJLiB0V7Ygne7bb+Gsj3ibyFyHWC6BVw+Z+tSW4ljHpO17I9TwStwQ==} - mdast-util-find-and-replace@3.0.2: resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} @@ -5125,6 +4589,9 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + minimatch@10.2.4: resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} engines: {node: 18 || 20 || >=22} @@ -5198,9 +4665,6 @@ packages: motion-utils@12.36.0: resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==} - mpg123-decoder@1.0.3: - resolution: {integrity: sha512-+fjxnWigodWJm3+4pndi+KUg9TBojgn31DPk85zEsim7C6s0X5Ztc/hQYdytXkwuGXH+aB0/aEkG40Emukv6oQ==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -5234,10 +4698,6 @@ packages: node-addon-api@1.7.2: resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} - node-addon-api@8.7.0: - resolution: {integrity: sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==} - engines: {node: ^18 || ^20 || >= 21} - node-api-version@0.2.1: resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==} @@ -5246,24 +4706,6 @@ packages: engines: {node: '>=10.5.0'} deprecated: Use your platform's native DOMException instead - node-downloader-helper@2.1.11: - resolution: {integrity: sha512-882fH2C9AWdiPCwz/2beq5t8FGMZK9Dx8TJUOIxzMCbvG7XUKM5BuJwN5f0NKo4SCQK6jR4p2TPm54mYGdGchQ==} - engines: {node: '>=14.18'} - hasBin: true - - node-edge-tts@1.2.10: - resolution: {integrity: sha512-bV2i4XU54D45+US0Zm1HcJRkifuB3W438dWyuJEHLQdKxnuqlI1kim2MOvR6Q3XUQZvfF9PoDyR1Rt7aeXhPdQ==} - hasBin: true - - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - node-fetch@3.3.2: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -5276,17 +4718,9 @@ packages: node-machine-id@1.1.12: resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==} - node-readable-to-web-readable-stream@0.4.2: - resolution: {integrity: sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==} - node-releases@2.0.36: resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} - nopt@5.0.0: - resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} - engines: {node: '>=6'} - hasBin: true - nopt@8.1.0: resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} engines: {node: ^18.17.0 || >=20.5.0} @@ -5300,24 +4734,6 @@ packages: resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} engines: {node: '>=10'} - nostr-tools@2.23.3: - resolution: {integrity: sha512-AALyt9k8xPdF4UV2mlLJ2mgCn4kpTB0DZ8t2r6wjdUh6anfx2cTVBsHUlo9U0EY/cKC5wcNyiMAmRJV5OVEalA==} - peerDependencies: - typescript: '>=5.0.0' - peerDependenciesMeta: - typescript: - optional: true - - nostr-wasm@0.1.0: - resolution: {integrity: sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==} - - npmlog@5.0.1: - resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} - deprecated: This package is no longer supported. - - nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -5337,10 +4753,6 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - oidc-client-ts@3.5.0: - resolution: {integrity: sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==} - engines: {node: '>=18'} - omggif@1.0.10: resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==} @@ -5363,12 +4775,6 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} - oniguruma-parser@0.12.1: - resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} - - oniguruma-to-es@4.3.5: - resolution: {integrity: sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==} - openai@6.26.0: resolution: {integrity: sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==} hasBin: true @@ -5393,16 +4799,10 @@ packages: zod: optional: true - openclaw@2026.4.15: - resolution: {integrity: sha512-vHH11WXuuHkTlA2Nfu+r7WzWo4uccraCgYjhMpzE+PjXUT4p+B3NMq0F24NbpBy+eNw5YjPPZyxxsY31qyFACw==} + openclaw@2026.4.26: + resolution: {integrity: sha512-KBKI7gu9d/6NxBfqnojTFcHNJvrOyyYGJ6oczaBKF7zUHVKdm78zZNQOBmwiaHQMrvhjNRo0ry9xXtaAJwhV3A==} engines: {node: '>=22.14.0'} hasBin: true - peerDependencies: - '@napi-rs/canvas': ^0.1.89 - node-llama-cpp: 3.18.1 - peerDependenciesMeta: - node-llama-cpp: - optional: true option@0.2.4: resolution: {integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==} @@ -5411,9 +4811,6 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - opusscript@0.1.1: - resolution: {integrity: sha512-mL0fZZOUnXdZ78woRXp18lApwpp0lF5tozJOD1Wut0dgrA9WuQTgSels/CSmFleaAZrJi/nci5KOVtbuxeWoQA==} - ora@5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} @@ -5430,14 +4827,18 @@ packages: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} - p-finally@1.0.0: - resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} - engines: {node: '>=4'} + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} @@ -5446,10 +4847,6 @@ packages: resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} engines: {node: '>=18'} - p-queue@6.6.2: - resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} - engines: {node: '>=8'} - p-queue@9.1.0: resolution: {integrity: sha512-O/ZPaXuQV29uSLbxWBGGZO1mCQXV2BLIwUr59JUU9SoH76mnYvtms7aafH/isNSNGwuEfP6W/4xD0/TJXxrizw==} engines: {node: '>=20'} @@ -5462,14 +4859,14 @@ packages: resolution: {integrity: sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==} engines: {node: '>=20'} - p-timeout@3.2.0: - resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} - engines: {node: '>=8'} - p-timeout@7.0.1: resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} engines: {node: '>=20'} + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + pac-proxy-agent@7.2.0: resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} engines: {node: '>= 14'} @@ -5536,6 +4933,10 @@ packages: resolution: {integrity: sha512-d7gQQmLvAKXKXE2GeP9apIGbMYKz88zWdsn/BN2HRWVQsDFdUY36WSLTY0Jvd4HWi7Fb30gQ62oAOzdgJA6fZw==} engines: {node: '>=14.0.0'} + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} + engines: {node: '>=14.0.0'} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -5570,10 +4971,6 @@ packages: resolution: {integrity: sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==} engines: {node: '>=20.16.0 || >=22.3.0'} - pdfjs-dist@5.6.205: - resolution: {integrity: sha512-tlUj+2IDa7G1SbvBNN74UHRLJybZDWYom+k6p5KIZl7huBvsA4APi6mKL+zCxd3tLjN5hOOEE9Tv7VdzO88pfg==} - engines: {node: '>=20.19.0 || >=22.13.0 || >=24'} - pe-library@0.4.1: resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==} engines: {node: '>=12', npm: '>=6'} @@ -5623,11 +5020,6 @@ packages: engines: {node: '>=18'} hasBin: true - playwright-core@1.59.1: - resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} - engines: {node: '>=18'} - hasBin: true - playwright@1.59.0: resolution: {integrity: sha512-wihGScriusvATUxmhfENxg0tj1vHEFeIwxlnPFKQTOQVd7aG08mUfvvniRP/PtQOC+2Bs52kBOC/Up1jTXeIbw==} engines: {node: '>=18'} @@ -5641,6 +5033,10 @@ packages: resolution: {integrity: sha512-GDEQJr8OG4e6JMp7mABtXFSEpgJa1CCpbQiAR+EjhkHJHnUL9zPPtbOrjsMD8gUbikgv3j7x404b0YJsV3aVFA==} hasBin: true + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + pngjs@6.0.0: resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==} engines: {node: '>=12.13.0'} @@ -5718,23 +5114,6 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - prism-media@1.3.5: - resolution: {integrity: sha512-IQdl0Q01m4LrkN1EGIE9lphov5Hy7WWlH6ulf5QdGePLlPas9p2mhgddTEHrlaXYjjFToM1/rWuwF37VF4taaA==} - peerDependencies: - '@discordjs/opus': '>=0.8.0 <1.0.0' - ffmpeg-static: ^5.0.2 || ^4.2.7 || ^3.0.0 || ^2.4.0 - node-opus: ^0.3.3 - opusscript: ^0.0.8 - peerDependenciesMeta: - '@discordjs/opus': - optional: true - ffmpeg-static: - optional: true - node-opus: - optional: true - opusscript: - optional: true - proc-log@5.0.0: resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} engines: {node: ^18.17.0 || >=20.5.0} @@ -5805,6 +5184,11 @@ packages: resolution: {integrity: sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==} hasBin: true + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + qs@6.15.0: resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} engines: {node: '>=0.6'} @@ -5945,18 +5329,6 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} - reflect-metadata@0.2.2: - resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} - - regex-recursion@6.0.2: - resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} - - regex-utilities@2.3.0: - resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} - - regex@6.1.0: - resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} - rehype-katex@7.0.1: resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==} @@ -5983,6 +5355,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + resedit@1.7.2: resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==} engines: {node: '>=12', npm: '>=6'} @@ -6023,11 +5398,6 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - roarr@2.15.4: resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} engines: {node: '>=8.0'} @@ -6071,10 +5441,6 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - sdp-transform@3.0.0: - resolution: {integrity: sha512-gfYVRGxjHkGF2NPeUWHw5u6T/KGFtS5/drPms73gaSuMaVHKCY3lpLnGDfswVQO0kddeePoti09AwhYP4zA8dQ==} - hasBin: true - semver-compare@1.0.0: resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} @@ -6127,9 +5493,6 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shiki@3.23.0: - resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==} - side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -6156,10 +5519,6 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - silk-wasm@3.7.1: - resolution: {integrity: sha512-mXPwLRtZxrYV3TZx41jMAeKc80wvmyrcXIcs8HctFxK15Ahz2OJQENYhNgEPeCEOdI6Mbx1NxQsqxzwc3DKerw==} - engines: {node: '>=16.11.0'} - simple-update-notifier@2.0.0: resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} engines: {node: '>=10'} @@ -6168,9 +5527,6 @@ packages: resolution: {integrity: sha512-mz9VXphOxQWX3eQ/uXCtm6upltoN0DLx8Zb5T4TFC4FHB7S9FDPGre8CfLWqPWQQH/GrQYd2AXhhVM5LDpYx6Q==} engines: {node: '>=20.12.2'} - simple-yenc@1.0.4: - resolution: {integrity: sha512-5gvxpSd79e9a3V4QDYUqnqxeD4HGlhCakVpb6gMnDD7lexJggSBJRBO5h52y/iJrdXRilX9UCuDaIJhSWm5OWw==} - sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -6316,6 +5672,9 @@ packages: strnum@2.2.2: resolution: {integrity: sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==} + strnum@2.2.3: + resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} + strtok3@10.3.5: resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} engines: {node: '>=18'} @@ -6352,10 +5711,6 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - table-layout@4.1.1: - resolution: {integrity: sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==} - engines: {node: '>=12.17'} - tagged-tag@1.0.0: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} @@ -6452,9 +5807,6 @@ packages: resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} engines: {node: '>=16'} - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tr46@6.0.0: resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} engines: {node: '>=20'} @@ -6487,10 +5839,6 @@ packages: resolution: {integrity: sha512-XuELoRpMR+sq8fuWwX7P0bcj+PRNiicOKDEb3fGNURhxWVyykCi9BNq7c4uVz7h7P0sj8qgBsr5SWS6yBClq3g==} engines: {node: '>=16'} - tsscmp@1.0.6: - resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} - engines: {node: '>=0.6.x'} - type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -6507,25 +5855,17 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} + typebox@1.1.33: + resolution: {integrity: sha512-+/MWwlQ1q2GSVwoxi/+u5JsHkgLQKcCN2Nsjree9c+K7GJu40qbaHrFETmfV1i9Fs1TcOVfynW+jJvIWcXtvjw==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true - typical@4.0.0: - resolution: {integrity: sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==} - engines: {node: '>=8'} - - typical@7.3.0: - resolution: {integrity: sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==} - engines: {node: '>=12.17'} - uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - uhyphen@0.2.0: - resolution: {integrity: sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==} - uint8array-extras@1.5.0: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} @@ -6533,9 +5873,6 @@ packages: underscore@1.13.8: resolution: {integrity: sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==} - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -6549,13 +5886,10 @@ packages: resolution: {integrity: sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA==} engines: {node: '>=20.18.1'} - undici@8.0.2: - resolution: {integrity: sha512-B9MeU5wuFhkFAuNeA19K2GDFcQXZxq33fL0nRy2Aq30wdufZbyyvxW3/ChaeipXVfy/wUweZyzovQGk39+9k2w==} + undici@8.1.0: + resolution: {integrity: sha512-E9MkTS4xXLnRPYqxH2e6Hr2/49e7WFDKczKcCaFH4VaZs2iNvHMqeIkyUAD9vM8kujy9TjVrRlQ5KkdEJxB2pw==} engines: {node: '>=22.19.0'} - unhomoglyph@1.0.6: - resolution: {integrity: sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==} - unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -6648,12 +5982,8 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - uuid@13.0.0: - resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} - hasBin: true - - uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + uuid@14.0.0: + resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} hasBin: true vary@1.1.2: @@ -6773,13 +6103,15 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + web-push@3.6.7: + resolution: {integrity: sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==} + engines: {node: '>= 16'} + hasBin: true + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webidl-conversions@8.0.1: resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} engines: {node: '>=20'} @@ -6792,12 +6124,12 @@ packages: resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - when-exit@2.1.5: resolution: {integrity: sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==} + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -6813,9 +6145,6 @@ packages: engines: {node: '>=8'} hasBin: true - wide-align@1.1.5: - resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} - win-guid@0.2.1: resolution: {integrity: sha512-gEIQU4mkgl2OPeoNrWflcJFJ3Ae2BPd4eCsHHA/XikslkIVms/nHhvnvzIZV7VLmBvtFlDOzLt9rrZT+n6D67A==} @@ -6823,9 +6152,9 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - wordwrapjs@5.1.1: - resolution: {integrity: sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==} - engines: {node: '>=12.17'} + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} @@ -6876,6 +6205,9 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -6895,6 +6227,10 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -6903,6 +6239,10 @@ packages: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + yargs@16.2.0: resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} engines: {node: '>=10'} @@ -6973,33 +6313,18 @@ snapshots: '@adobe/css-tools@4.4.4': {} - '@agentclientprotocol/sdk@0.18.2(zod@4.3.6)': + '@agentclientprotocol/sdk@0.20.0(zod@4.3.6)': dependencies: zod: 4.3.6 '@alloc/quick-lru@5.2.0': {} - '@anthropic-ai/sdk@0.73.0(zod@4.3.6)': + '@anthropic-ai/sdk@0.90.0(zod@4.3.6)': dependencies: json-schema-to-ts: 3.1.1 optionalDependencies: zod: 4.3.6 - '@anthropic-ai/sdk@0.80.0(zod@4.3.6)': - dependencies: - json-schema-to-ts: 3.1.1 - optionalDependencies: - zod: 4.3.6 - - '@anthropic-ai/vertex-sdk@0.15.0(encoding@0.1.13)(zod@4.3.6)': - dependencies: - '@anthropic-ai/sdk': 0.80.0(zod@4.3.6) - google-auth-library: 9.15.1(encoding@0.1.13) - transitivePeerDependencies: - - encoding - - supports-color - - zod - '@ark/schema@0.56.0': dependencies: '@ark/util': 0.56.0 @@ -7027,7 +6352,7 @@ snapshots: '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.7 + '@aws-sdk/types': 3.973.8 tslib: 2.8.1 '@aws-crypto/sha256-browser@5.2.0': @@ -7035,7 +6360,7 @@ snapshots: '@aws-crypto/sha256-js': 5.2.0 '@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.7 + '@aws-sdk/types': 3.973.8 '@aws-sdk/util-locate-window': 3.965.5 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 @@ -7043,7 +6368,7 @@ snapshots: '@aws-crypto/sha256-js@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.7 + '@aws-sdk/types': 3.973.8 tslib: 2.8.1 '@aws-crypto/supports-web-crypto@5.2.0': @@ -7052,502 +6377,385 @@ snapshots: '@aws-crypto/util@5.2.0': dependencies: - '@aws-sdk/types': 3.973.7 + '@aws-sdk/types': 3.973.8 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 - '@aws-sdk/client-bedrock-runtime@3.1028.0': + '@aws-sdk/client-bedrock-runtime@3.1038.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.973.27 - '@aws-sdk/credential-provider-node': 3.972.30 - '@aws-sdk/eventstream-handler-node': 3.972.13 - '@aws-sdk/middleware-eventstream': 3.972.9 - '@aws-sdk/middleware-host-header': 3.972.9 - '@aws-sdk/middleware-logger': 3.972.9 - '@aws-sdk/middleware-recursion-detection': 3.972.10 - '@aws-sdk/middleware-user-agent': 3.972.29 - '@aws-sdk/middleware-websocket': 3.972.15 - '@aws-sdk/region-config-resolver': 3.972.11 - '@aws-sdk/token-providers': 3.1028.0 - '@aws-sdk/types': 3.973.7 - '@aws-sdk/util-endpoints': 3.996.6 - '@aws-sdk/util-user-agent-browser': 3.972.9 - '@aws-sdk/util-user-agent-node': 3.973.15 - '@smithy/config-resolver': 4.4.14 - '@smithy/core': 3.23.14 - '@smithy/eventstream-serde-browser': 4.2.13 - '@smithy/eventstream-serde-config-resolver': 4.3.13 - '@smithy/eventstream-serde-node': 4.2.13 - '@smithy/fetch-http-handler': 5.3.16 - '@smithy/hash-node': 4.2.13 - '@smithy/invalid-dependency': 4.2.13 - '@smithy/middleware-content-length': 4.2.13 - '@smithy/middleware-endpoint': 4.4.29 - '@smithy/middleware-retry': 4.5.0 - '@smithy/middleware-serde': 4.2.17 - '@smithy/middleware-stack': 4.2.13 - '@smithy/node-config-provider': 4.3.13 - '@smithy/node-http-handler': 4.5.2 - '@smithy/protocol-http': 5.3.13 - '@smithy/smithy-client': 4.12.9 - '@smithy/types': 4.14.0 - '@smithy/url-parser': 4.2.13 + '@aws-sdk/core': 3.974.6 + '@aws-sdk/credential-provider-node': 3.972.37 + '@aws-sdk/eventstream-handler-node': 3.972.14 + '@aws-sdk/middleware-eventstream': 3.972.10 + '@aws-sdk/middleware-host-header': 3.972.10 + '@aws-sdk/middleware-logger': 3.972.10 + '@aws-sdk/middleware-recursion-detection': 3.972.11 + '@aws-sdk/middleware-user-agent': 3.972.36 + '@aws-sdk/middleware-websocket': 3.972.16 + '@aws-sdk/region-config-resolver': 3.972.13 + '@aws-sdk/token-providers': 3.1038.0 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.8 + '@aws-sdk/util-user-agent-browser': 3.972.10 + '@aws-sdk/util-user-agent-node': 3.973.22 + '@smithy/config-resolver': 4.4.17 + '@smithy/core': 3.23.17 + '@smithy/eventstream-serde-browser': 4.2.14 + '@smithy/eventstream-serde-config-resolver': 4.3.14 + '@smithy/eventstream-serde-node': 4.2.14 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/hash-node': 4.2.14 + '@smithy/invalid-dependency': 4.2.14 + '@smithy/middleware-content-length': 4.2.14 + '@smithy/middleware-endpoint': 4.4.32 + '@smithy/middleware-retry': 4.5.6 + '@smithy/middleware-serde': 4.2.20 + '@smithy/middleware-stack': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/node-http-handler': 4.6.1 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 '@smithy/util-base64': 4.3.2 '@smithy/util-body-length-browser': 4.2.2 '@smithy/util-body-length-node': 4.2.3 - '@smithy/util-defaults-mode-browser': 4.3.45 - '@smithy/util-defaults-mode-node': 4.2.49 - '@smithy/util-endpoints': 3.3.4 - '@smithy/util-middleware': 4.2.13 - '@smithy/util-retry': 4.3.0 - '@smithy/util-stream': 4.5.22 + '@smithy/util-defaults-mode-browser': 4.3.49 + '@smithy/util-defaults-mode-node': 4.2.54 + '@smithy/util-endpoints': 3.4.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.5 + '@smithy/util-stream': 4.5.25 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/client-bedrock@3.1028.0': + '@aws-sdk/core@3.974.6': dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.973.27 - '@aws-sdk/credential-provider-node': 3.972.30 - '@aws-sdk/middleware-host-header': 3.972.9 - '@aws-sdk/middleware-logger': 3.972.9 - '@aws-sdk/middleware-recursion-detection': 3.972.10 - '@aws-sdk/middleware-user-agent': 3.972.29 - '@aws-sdk/region-config-resolver': 3.972.11 - '@aws-sdk/token-providers': 3.1028.0 - '@aws-sdk/types': 3.973.7 - '@aws-sdk/util-endpoints': 3.996.6 - '@aws-sdk/util-user-agent-browser': 3.972.9 - '@aws-sdk/util-user-agent-node': 3.973.15 - '@smithy/config-resolver': 4.4.14 - '@smithy/core': 3.23.14 - '@smithy/fetch-http-handler': 5.3.16 - '@smithy/hash-node': 4.2.13 - '@smithy/invalid-dependency': 4.2.13 - '@smithy/middleware-content-length': 4.2.13 - '@smithy/middleware-endpoint': 4.4.29 - '@smithy/middleware-retry': 4.5.0 - '@smithy/middleware-serde': 4.2.17 - '@smithy/middleware-stack': 4.2.13 - '@smithy/node-config-provider': 4.3.13 - '@smithy/node-http-handler': 4.5.2 - '@smithy/protocol-http': 5.3.13 - '@smithy/smithy-client': 4.12.9 - '@smithy/types': 4.14.0 - '@smithy/url-parser': 4.2.13 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/xml-builder': 3.972.21 + '@smithy/core': 3.23.17 + '@smithy/node-config-provider': 4.3.14 + '@smithy/property-provider': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/signature-v4': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 '@smithy/util-base64': 4.3.2 - '@smithy/util-body-length-browser': 4.2.2 - '@smithy/util-body-length-node': 4.2.3 - '@smithy/util-defaults-mode-browser': 4.3.45 - '@smithy/util-defaults-mode-node': 4.2.49 - '@smithy/util-endpoints': 3.3.4 - '@smithy/util-middleware': 4.2.13 - '@smithy/util-retry': 4.3.0 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/client-cognito-identity@3.1027.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.973.27 - '@aws-sdk/credential-provider-node': 3.972.30 - '@aws-sdk/middleware-host-header': 3.972.9 - '@aws-sdk/middleware-logger': 3.972.9 - '@aws-sdk/middleware-recursion-detection': 3.972.10 - '@aws-sdk/middleware-user-agent': 3.972.29 - '@aws-sdk/region-config-resolver': 3.972.11 - '@aws-sdk/types': 3.973.7 - '@aws-sdk/util-endpoints': 3.996.6 - '@aws-sdk/util-user-agent-browser': 3.972.9 - '@aws-sdk/util-user-agent-node': 3.973.15 - '@smithy/config-resolver': 4.4.14 - '@smithy/core': 3.23.14 - '@smithy/fetch-http-handler': 5.3.16 - '@smithy/hash-node': 4.2.13 - '@smithy/invalid-dependency': 4.2.13 - '@smithy/middleware-content-length': 4.2.13 - '@smithy/middleware-endpoint': 4.4.29 - '@smithy/middleware-retry': 4.5.0 - '@smithy/middleware-serde': 4.2.17 - '@smithy/middleware-stack': 4.2.13 - '@smithy/node-config-provider': 4.3.13 - '@smithy/node-http-handler': 4.5.2 - '@smithy/protocol-http': 5.3.13 - '@smithy/smithy-client': 4.12.9 - '@smithy/types': 4.14.0 - '@smithy/url-parser': 4.2.13 - '@smithy/util-base64': 4.3.2 - '@smithy/util-body-length-browser': 4.2.2 - '@smithy/util-body-length-node': 4.2.3 - '@smithy/util-defaults-mode-browser': 4.3.45 - '@smithy/util-defaults-mode-node': 4.2.49 - '@smithy/util-endpoints': 3.3.4 - '@smithy/util-middleware': 4.2.13 - '@smithy/util-retry': 4.3.0 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/core@3.973.27': - dependencies: - '@aws-sdk/types': 3.973.7 - '@aws-sdk/xml-builder': 3.972.17 - '@smithy/core': 3.23.14 - '@smithy/node-config-provider': 4.3.13 - '@smithy/property-provider': 4.2.13 - '@smithy/protocol-http': 5.3.13 - '@smithy/signature-v4': 5.3.13 - '@smithy/smithy-client': 4.12.9 - '@smithy/types': 4.14.0 - '@smithy/util-base64': 4.3.2 - '@smithy/util-middleware': 4.2.13 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.5 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@aws-sdk/credential-provider-cognito-identity@3.972.22': + '@aws-sdk/credential-provider-env@3.972.32': dependencies: - '@aws-sdk/nested-clients': 3.996.19 - '@aws-sdk/types': 3.973.7 - '@smithy/property-provider': 4.2.13 - '@smithy/types': 4.14.0 + '@aws-sdk/core': 3.974.6 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.34': + dependencies: + '@aws-sdk/core': 3.974.6 + '@aws-sdk/types': 3.973.8 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/node-http-handler': 4.6.1 + '@smithy/property-provider': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/util-stream': 4.5.25 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.36': + dependencies: + '@aws-sdk/core': 3.974.6 + '@aws-sdk/credential-provider-env': 3.972.32 + '@aws-sdk/credential-provider-http': 3.972.34 + '@aws-sdk/credential-provider-login': 3.972.36 + '@aws-sdk/credential-provider-process': 3.972.32 + '@aws-sdk/credential-provider-sso': 3.972.36 + '@aws-sdk/credential-provider-web-identity': 3.972.36 + '@aws-sdk/nested-clients': 3.997.4 + '@aws-sdk/types': 3.973.8 + '@smithy/credential-provider-imds': 4.2.14 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-env@3.972.25': + '@aws-sdk/credential-provider-login@3.972.36': dependencies: - '@aws-sdk/core': 3.973.27 - '@aws-sdk/types': 3.973.7 - '@smithy/property-provider': 4.2.13 - '@smithy/types': 4.14.0 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-http@3.972.27': - dependencies: - '@aws-sdk/core': 3.973.27 - '@aws-sdk/types': 3.973.7 - '@smithy/fetch-http-handler': 5.3.16 - '@smithy/node-http-handler': 4.5.2 - '@smithy/property-provider': 4.2.13 - '@smithy/protocol-http': 5.3.13 - '@smithy/smithy-client': 4.12.9 - '@smithy/types': 4.14.0 - '@smithy/util-stream': 4.5.22 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-ini@3.972.29': - dependencies: - '@aws-sdk/core': 3.973.27 - '@aws-sdk/credential-provider-env': 3.972.25 - '@aws-sdk/credential-provider-http': 3.972.27 - '@aws-sdk/credential-provider-login': 3.972.29 - '@aws-sdk/credential-provider-process': 3.972.25 - '@aws-sdk/credential-provider-sso': 3.972.29 - '@aws-sdk/credential-provider-web-identity': 3.972.29 - '@aws-sdk/nested-clients': 3.996.19 - '@aws-sdk/types': 3.973.7 - '@smithy/credential-provider-imds': 4.2.13 - '@smithy/property-provider': 4.2.13 - '@smithy/shared-ini-file-loader': 4.4.8 - '@smithy/types': 4.14.0 + '@aws-sdk/core': 3.974.6 + '@aws-sdk/nested-clients': 3.997.4 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-login@3.972.29': + '@aws-sdk/credential-provider-node@3.972.37': dependencies: - '@aws-sdk/core': 3.973.27 - '@aws-sdk/nested-clients': 3.996.19 - '@aws-sdk/types': 3.973.7 - '@smithy/property-provider': 4.2.13 - '@smithy/protocol-http': 5.3.13 - '@smithy/shared-ini-file-loader': 4.4.8 - '@smithy/types': 4.14.0 + '@aws-sdk/credential-provider-env': 3.972.32 + '@aws-sdk/credential-provider-http': 3.972.34 + '@aws-sdk/credential-provider-ini': 3.972.36 + '@aws-sdk/credential-provider-process': 3.972.32 + '@aws-sdk/credential-provider-sso': 3.972.36 + '@aws-sdk/credential-provider-web-identity': 3.972.36 + '@aws-sdk/types': 3.973.8 + '@smithy/credential-provider-imds': 4.2.14 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-node@3.972.30': + '@aws-sdk/credential-provider-process@3.972.32': dependencies: - '@aws-sdk/credential-provider-env': 3.972.25 - '@aws-sdk/credential-provider-http': 3.972.27 - '@aws-sdk/credential-provider-ini': 3.972.29 - '@aws-sdk/credential-provider-process': 3.972.25 - '@aws-sdk/credential-provider-sso': 3.972.29 - '@aws-sdk/credential-provider-web-identity': 3.972.29 - '@aws-sdk/types': 3.973.7 - '@smithy/credential-provider-imds': 4.2.13 - '@smithy/property-provider': 4.2.13 - '@smithy/shared-ini-file-loader': 4.4.8 - '@smithy/types': 4.14.0 + '@aws-sdk/core': 3.974.6 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.36': + dependencies: + '@aws-sdk/core': 3.974.6 + '@aws-sdk/nested-clients': 3.997.4 + '@aws-sdk/token-providers': 3.1038.0 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-process@3.972.25': + '@aws-sdk/credential-provider-web-identity@3.972.36': dependencies: - '@aws-sdk/core': 3.973.27 - '@aws-sdk/types': 3.973.7 - '@smithy/property-provider': 4.2.13 - '@smithy/shared-ini-file-loader': 4.4.8 - '@smithy/types': 4.14.0 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-sso@3.972.29': - dependencies: - '@aws-sdk/core': 3.973.27 - '@aws-sdk/nested-clients': 3.996.19 - '@aws-sdk/token-providers': 3.1026.0 - '@aws-sdk/types': 3.973.7 - '@smithy/property-provider': 4.2.13 - '@smithy/shared-ini-file-loader': 4.4.8 - '@smithy/types': 4.14.0 + '@aws-sdk/core': 3.974.6 + '@aws-sdk/nested-clients': 3.997.4 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-web-identity@3.972.29': + '@aws-sdk/eventstream-handler-node@3.972.14': dependencies: - '@aws-sdk/core': 3.973.27 - '@aws-sdk/nested-clients': 3.996.19 - '@aws-sdk/types': 3.973.7 - '@smithy/property-provider': 4.2.13 - '@smithy/shared-ini-file-loader': 4.4.8 - '@smithy/types': 4.14.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-providers@3.1027.0': - dependencies: - '@aws-sdk/client-cognito-identity': 3.1027.0 - '@aws-sdk/core': 3.973.27 - '@aws-sdk/credential-provider-cognito-identity': 3.972.22 - '@aws-sdk/credential-provider-env': 3.972.25 - '@aws-sdk/credential-provider-http': 3.972.27 - '@aws-sdk/credential-provider-ini': 3.972.29 - '@aws-sdk/credential-provider-login': 3.972.29 - '@aws-sdk/credential-provider-node': 3.972.30 - '@aws-sdk/credential-provider-process': 3.972.25 - '@aws-sdk/credential-provider-sso': 3.972.29 - '@aws-sdk/credential-provider-web-identity': 3.972.29 - '@aws-sdk/nested-clients': 3.996.19 - '@aws-sdk/types': 3.973.7 - '@smithy/config-resolver': 4.4.14 - '@smithy/core': 3.23.14 - '@smithy/credential-provider-imds': 4.2.13 - '@smithy/node-config-provider': 4.3.13 - '@smithy/property-provider': 4.2.13 - '@smithy/types': 4.14.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/eventstream-handler-node@3.972.13': - dependencies: - '@aws-sdk/types': 3.973.7 - '@smithy/eventstream-codec': 4.2.13 - '@smithy/types': 4.14.0 + '@aws-sdk/types': 3.973.8 + '@smithy/eventstream-codec': 4.2.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/middleware-eventstream@3.972.9': + '@aws-sdk/middleware-eventstream@3.972.10': dependencies: - '@aws-sdk/types': 3.973.7 - '@smithy/protocol-http': 5.3.13 - '@smithy/types': 4.14.0 + '@aws-sdk/types': 3.973.8 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/middleware-host-header@3.972.9': + '@aws-sdk/middleware-host-header@3.972.10': dependencies: - '@aws-sdk/types': 3.973.7 - '@smithy/protocol-http': 5.3.13 - '@smithy/types': 4.14.0 + '@aws-sdk/types': 3.973.8 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/middleware-logger@3.972.9': + '@aws-sdk/middleware-logger@3.972.10': dependencies: - '@aws-sdk/types': 3.973.7 - '@smithy/types': 4.14.0 + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/middleware-recursion-detection@3.972.10': + '@aws-sdk/middleware-recursion-detection@3.972.11': dependencies: - '@aws-sdk/types': 3.973.7 + '@aws-sdk/types': 3.973.8 '@aws/lambda-invoke-store': 0.2.4 - '@smithy/protocol-http': 5.3.13 - '@smithy/types': 4.14.0 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/middleware-user-agent@3.972.29': + '@aws-sdk/middleware-sdk-s3@3.972.35': dependencies: - '@aws-sdk/core': 3.973.27 - '@aws-sdk/types': 3.973.7 - '@aws-sdk/util-endpoints': 3.996.6 - '@smithy/core': 3.23.14 - '@smithy/protocol-http': 5.3.13 - '@smithy/types': 4.14.0 - '@smithy/util-retry': 4.3.0 + '@aws-sdk/core': 3.974.6 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-arn-parser': 3.972.3 + '@smithy/core': 3.23.17 + '@smithy/node-config-provider': 4.3.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/signature-v4': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/util-config-provider': 4.2.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-stream': 4.5.25 + '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@aws-sdk/middleware-websocket@3.972.15': + '@aws-sdk/middleware-user-agent@3.972.36': dependencies: - '@aws-sdk/types': 3.973.7 - '@aws-sdk/util-format-url': 3.972.9 - '@smithy/eventstream-codec': 4.2.13 - '@smithy/eventstream-serde-browser': 4.2.13 - '@smithy/fetch-http-handler': 5.3.16 - '@smithy/protocol-http': 5.3.13 - '@smithy/signature-v4': 5.3.13 - '@smithy/types': 4.14.0 + '@aws-sdk/core': 3.974.6 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.8 + '@smithy/core': 3.23.17 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-retry': 4.3.5 + tslib: 2.8.1 + + '@aws-sdk/middleware-websocket@3.972.16': + dependencies: + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-format-url': 3.972.10 + '@smithy/eventstream-codec': 4.2.14 + '@smithy/eventstream-serde-browser': 4.2.14 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/protocol-http': 5.3.14 + '@smithy/signature-v4': 5.3.14 + '@smithy/types': 4.14.1 '@smithy/util-base64': 4.3.2 '@smithy/util-hex-encoding': 4.2.2 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@aws-sdk/nested-clients@3.996.19': + '@aws-sdk/nested-clients@3.997.4': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.973.27 - '@aws-sdk/middleware-host-header': 3.972.9 - '@aws-sdk/middleware-logger': 3.972.9 - '@aws-sdk/middleware-recursion-detection': 3.972.10 - '@aws-sdk/middleware-user-agent': 3.972.29 - '@aws-sdk/region-config-resolver': 3.972.11 - '@aws-sdk/types': 3.973.7 - '@aws-sdk/util-endpoints': 3.996.6 - '@aws-sdk/util-user-agent-browser': 3.972.9 - '@aws-sdk/util-user-agent-node': 3.973.15 - '@smithy/config-resolver': 4.4.14 - '@smithy/core': 3.23.14 - '@smithy/fetch-http-handler': 5.3.16 - '@smithy/hash-node': 4.2.13 - '@smithy/invalid-dependency': 4.2.13 - '@smithy/middleware-content-length': 4.2.13 - '@smithy/middleware-endpoint': 4.4.29 - '@smithy/middleware-retry': 4.5.0 - '@smithy/middleware-serde': 4.2.17 - '@smithy/middleware-stack': 4.2.13 - '@smithy/node-config-provider': 4.3.13 - '@smithy/node-http-handler': 4.5.2 - '@smithy/protocol-http': 5.3.13 - '@smithy/smithy-client': 4.12.9 - '@smithy/types': 4.14.0 - '@smithy/url-parser': 4.2.13 + '@aws-sdk/core': 3.974.6 + '@aws-sdk/middleware-host-header': 3.972.10 + '@aws-sdk/middleware-logger': 3.972.10 + '@aws-sdk/middleware-recursion-detection': 3.972.11 + '@aws-sdk/middleware-user-agent': 3.972.36 + '@aws-sdk/region-config-resolver': 3.972.13 + '@aws-sdk/signature-v4-multi-region': 3.996.23 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.8 + '@aws-sdk/util-user-agent-browser': 3.972.10 + '@aws-sdk/util-user-agent-node': 3.973.22 + '@smithy/config-resolver': 4.4.17 + '@smithy/core': 3.23.17 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/hash-node': 4.2.14 + '@smithy/invalid-dependency': 4.2.14 + '@smithy/middleware-content-length': 4.2.14 + '@smithy/middleware-endpoint': 4.4.32 + '@smithy/middleware-retry': 4.5.6 + '@smithy/middleware-serde': 4.2.20 + '@smithy/middleware-stack': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/node-http-handler': 4.6.1 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 '@smithy/util-base64': 4.3.2 '@smithy/util-body-length-browser': 4.2.2 '@smithy/util-body-length-node': 4.2.3 - '@smithy/util-defaults-mode-browser': 4.3.45 - '@smithy/util-defaults-mode-node': 4.2.49 - '@smithy/util-endpoints': 3.3.4 - '@smithy/util-middleware': 4.2.13 - '@smithy/util-retry': 4.3.0 + '@smithy/util-defaults-mode-browser': 4.3.49 + '@smithy/util-defaults-mode-node': 4.2.54 + '@smithy/util-endpoints': 3.4.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.5 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/region-config-resolver@3.972.11': + '@aws-sdk/region-config-resolver@3.972.13': dependencies: - '@aws-sdk/types': 3.973.7 - '@smithy/config-resolver': 4.4.14 - '@smithy/node-config-provider': 4.3.13 - '@smithy/types': 4.14.0 + '@aws-sdk/types': 3.973.8 + '@smithy/config-resolver': 4.4.17 + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/token-providers@3.1026.0': + '@aws-sdk/signature-v4-multi-region@3.996.23': dependencies: - '@aws-sdk/core': 3.973.27 - '@aws-sdk/nested-clients': 3.996.19 - '@aws-sdk/types': 3.973.7 - '@smithy/property-provider': 4.2.13 - '@smithy/shared-ini-file-loader': 4.4.8 - '@smithy/types': 4.14.0 + '@aws-sdk/middleware-sdk-s3': 3.972.35 + '@aws-sdk/types': 3.973.8 + '@smithy/protocol-http': 5.3.14 + '@smithy/signature-v4': 5.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1038.0': + dependencies: + '@aws-sdk/core': 3.974.6 + '@aws-sdk/nested-clients': 3.997.4 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/token-providers@3.1028.0': + '@aws-sdk/types@3.973.8': dependencies: - '@aws-sdk/core': 3.973.27 - '@aws-sdk/nested-clients': 3.996.19 - '@aws-sdk/types': 3.973.7 - '@smithy/property-provider': 4.2.13 - '@smithy/shared-ini-file-loader': 4.4.8 - '@smithy/types': 4.14.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/types@3.973.7': - dependencies: - '@smithy/types': 4.14.0 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/util-endpoints@3.996.6': + '@aws-sdk/util-arn-parser@3.972.3': dependencies: - '@aws-sdk/types': 3.973.7 - '@smithy/types': 4.14.0 - '@smithy/url-parser': 4.2.13 - '@smithy/util-endpoints': 3.3.4 tslib: 2.8.1 - '@aws-sdk/util-format-url@3.972.9': + '@aws-sdk/util-endpoints@3.996.8': dependencies: - '@aws-sdk/types': 3.973.7 - '@smithy/querystring-builder': 4.2.13 - '@smithy/types': 4.14.0 + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-endpoints': 3.4.2 + tslib: 2.8.1 + + '@aws-sdk/util-format-url@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/querystring-builder': 4.2.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 '@aws-sdk/util-locate-window@3.965.5': dependencies: tslib: 2.8.1 - '@aws-sdk/util-user-agent-browser@3.972.9': + '@aws-sdk/util-user-agent-browser@3.972.10': dependencies: - '@aws-sdk/types': 3.973.7 - '@smithy/types': 4.14.0 + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 bowser: 2.14.1 tslib: 2.8.1 - '@aws-sdk/util-user-agent-node@3.973.15': + '@aws-sdk/util-user-agent-node@3.973.22': dependencies: - '@aws-sdk/middleware-user-agent': 3.972.29 - '@aws-sdk/types': 3.973.7 - '@smithy/node-config-provider': 4.3.13 - '@smithy/types': 4.14.0 + '@aws-sdk/middleware-user-agent': 3.972.36 + '@aws-sdk/types': 3.973.8 + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 '@smithy/util-config-provider': 4.2.2 tslib: 2.8.1 - '@aws-sdk/xml-builder@3.972.17': + '@aws-sdk/xml-builder@3.972.21': dependencies: - '@smithy/types': 4.14.0 - fast-xml-parser: 5.5.8 + '@nodable/entities': 2.1.0 + '@smithy/types': 4.14.1 + fast-xml-parser: 5.7.2 tslib: 2.8.1 - '@aws/bedrock-token-generator@1.1.0': - dependencies: - '@aws-sdk/credential-providers': 3.1027.0 - '@aws-sdk/util-format-url': 3.972.9 - '@smithy/config-resolver': 4.4.14 - '@smithy/hash-node': 4.2.13 - '@smithy/invalid-dependency': 4.2.13 - '@smithy/node-config-provider': 4.3.13 - '@smithy/protocol-http': 5.3.13 - '@smithy/signature-v4': 5.3.13 - '@smithy/types': 4.14.0 - transitivePeerDependencies: - - aws-crt - '@aws/lambda-invoke-store@0.2.4': {} '@babel/code-frame@7.29.0': @@ -7670,28 +6878,6 @@ snapshots: dependencies: css-tree: 3.2.1 - '@buape/carbon@0.15.0(@discordjs/opus@0.10.0(encoding@0.1.13))(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1)(hono@4.12.12)(opusscript@0.1.1)': - dependencies: - '@types/node': 25.6.0 - discord-api-types: 0.38.45 - optionalDependencies: - '@cloudflare/workers-types': 4.20260405.1 - '@discordjs/voice': 0.19.2(@discordjs/opus@0.10.0(encoding@0.1.13))(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1)(opusscript@0.1.1) - '@hono/node-server': 1.19.13(hono@4.12.12) - '@types/bun': 1.3.11 - '@types/ws': 8.18.1 - ws: 8.20.0 - transitivePeerDependencies: - - '@discordjs/opus' - - '@emnapi/core' - - '@emnapi/runtime' - - bufferutil - - ffmpeg-static - - hono - - node-opus - - opusscript - - utf-8-validate - '@cacheable/memory@2.0.8': dependencies: '@cacheable/utils': 2.4.0 @@ -7733,9 +6919,6 @@ snapshots: fast-wrap-ansi: 0.1.6 sisteransi: 1.0.5 - '@cloudflare/workers-types@4.20260405.1': - optional: true - '@csstools/color-helpers@6.0.2': {} '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': @@ -7765,50 +6948,6 @@ snapshots: ajv: 6.14.0 ajv-keywords: 3.5.2(ajv@6.14.0) - '@discordjs/node-pre-gyp@0.4.5(encoding@0.1.13)': - dependencies: - detect-libc: 2.1.2 - https-proxy-agent: 7.0.6 - make-dir: 3.1.0 - node-fetch: 2.7.0(encoding@0.1.13) - nopt: 5.0.0 - npmlog: 5.0.1 - rimraf: 3.0.2 - semver: 7.7.4 - tar: 6.2.1 - transitivePeerDependencies: - - encoding - - supports-color - optional: true - - '@discordjs/opus@0.10.0(encoding@0.1.13)': - dependencies: - '@discordjs/node-pre-gyp': 0.4.5(encoding@0.1.13) - node-addon-api: 8.7.0 - transitivePeerDependencies: - - encoding - - supports-color - optional: true - - '@discordjs/voice@0.19.2(@discordjs/opus@0.10.0(encoding@0.1.13))(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1)(opusscript@0.1.1)': - dependencies: - '@snazzah/davey': 0.1.11(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1) - '@types/ws': 8.18.1 - discord-api-types: 0.38.45 - prism-media: 1.3.5(@discordjs/opus@0.10.0(encoding@0.1.13))(opusscript@0.1.1) - tslib: 2.8.1 - ws: 8.20.0 - transitivePeerDependencies: - - '@discordjs/opus' - - '@emnapi/core' - - '@emnapi/runtime' - - bufferutil - - ffmpeg-static - - node-opus - - opusscript - - utf-8-validate - optional: true - '@electron/asar@3.4.1': dependencies: commander: 5.1.0 @@ -7909,22 +7048,11 @@ snapshots: - supports-color optional: true - '@emnapi/core@1.9.2': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.9.1': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.2.1': - dependencies: - tslib: 2.8.1 - optional: true - '@esbuild/aix-ppc64@0.27.4': optional: true @@ -8003,8 +7131,6 @@ snapshots: '@esbuild/win32-x64@0.27.4': optional: true - '@eshaz/web-worker@1.2.2': {} - '@eslint-community/eslint-utils@4.9.1(eslint@10.1.0(jiti@1.21.7))': dependencies: eslint: 10.1.0(jiti@1.21.7) @@ -8073,33 +7199,12 @@ snapshots: - supports-color - utf-8-validate - '@grammyjs/runner@2.0.3(grammy@1.42.0(encoding@0.1.13))': - dependencies: - abort-controller: 3.0.0 - grammy: 1.42.0(encoding@0.1.13) - - '@grammyjs/transformer-throttler@1.2.1(grammy@1.42.0(encoding@0.1.13))': - dependencies: - bottleneck: 2.19.5 - grammy: 1.42.0(encoding@0.1.13) - - '@grammyjs/types@3.26.0': {} - '@hapi/boom@9.1.4': dependencies: '@hapi/hoek': 9.3.0 '@hapi/hoek@9.3.0': {} - '@homebridge/ciao@1.3.6': - dependencies: - debug: 4.4.3 - fast-deep-equal: 3.1.3 - source-map-support: 0.5.21 - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - '@hono/node-server@1.19.13(hono@4.12.12)': dependencies: hono: 4.12.12 @@ -8235,6 +7340,7 @@ snapshots: mime: 3.0.0 transitivePeerDependencies: - supports-color + optional: true '@jimp/diff@1.6.1': dependencies: @@ -8244,8 +7350,10 @@ snapshots: pixelmatch: 5.3.0 transitivePeerDependencies: - supports-color + optional: true - '@jimp/file-ops@1.6.1': {} + '@jimp/file-ops@1.6.1': + optional: true '@jimp/js-bmp@1.6.1': dependencies: @@ -8255,6 +7363,7 @@ snapshots: bmp-ts: 1.0.9 transitivePeerDependencies: - supports-color + optional: true '@jimp/js-gif@1.6.1': dependencies: @@ -8264,6 +7373,7 @@ snapshots: omggif: 1.0.10 transitivePeerDependencies: - supports-color + optional: true '@jimp/js-jpeg@1.6.1': dependencies: @@ -8272,6 +7382,7 @@ snapshots: jpeg-js: 0.4.4 transitivePeerDependencies: - supports-color + optional: true '@jimp/js-png@1.6.1': dependencies: @@ -8280,6 +7391,7 @@ snapshots: pngjs: 7.0.0 transitivePeerDependencies: - supports-color + optional: true '@jimp/js-tiff@1.6.1': dependencies: @@ -8288,12 +7400,14 @@ snapshots: utif2: 4.1.0 transitivePeerDependencies: - supports-color + optional: true '@jimp/plugin-blit@1.6.1': dependencies: '@jimp/types': 1.6.1 '@jimp/utils': 1.6.1 zod: 3.25.76 + optional: true '@jimp/plugin-blur@1.6.1': dependencies: @@ -8301,11 +7415,13 @@ snapshots: '@jimp/utils': 1.6.1 transitivePeerDependencies: - supports-color + optional: true '@jimp/plugin-circle@1.6.1': dependencies: '@jimp/types': 1.6.1 zod: 3.25.76 + optional: true '@jimp/plugin-color@1.6.1': dependencies: @@ -8316,6 +7432,7 @@ snapshots: zod: 3.25.76 transitivePeerDependencies: - supports-color + optional: true '@jimp/plugin-contain@1.6.1': dependencies: @@ -8327,6 +7444,7 @@ snapshots: zod: 3.25.76 transitivePeerDependencies: - supports-color + optional: true '@jimp/plugin-cover@1.6.1': dependencies: @@ -8337,6 +7455,7 @@ snapshots: zod: 3.25.76 transitivePeerDependencies: - supports-color + optional: true '@jimp/plugin-crop@1.6.1': dependencies: @@ -8346,27 +7465,32 @@ snapshots: zod: 3.25.76 transitivePeerDependencies: - supports-color + optional: true '@jimp/plugin-displace@1.6.1': dependencies: '@jimp/types': 1.6.1 '@jimp/utils': 1.6.1 zod: 3.25.76 + optional: true '@jimp/plugin-dither@1.6.1': dependencies: '@jimp/types': 1.6.1 + optional: true '@jimp/plugin-fisheye@1.6.1': dependencies: '@jimp/types': 1.6.1 '@jimp/utils': 1.6.1 zod: 3.25.76 + optional: true '@jimp/plugin-flip@1.6.1': dependencies: '@jimp/types': 1.6.1 zod: 3.25.76 + optional: true '@jimp/plugin-hash@1.6.1': dependencies: @@ -8382,11 +7506,13 @@ snapshots: any-base: 1.1.0 transitivePeerDependencies: - supports-color + optional: true '@jimp/plugin-mask@1.6.1': dependencies: '@jimp/types': 1.6.1 zod: 3.25.76 + optional: true '@jimp/plugin-print@1.6.1': dependencies: @@ -8402,11 +7528,13 @@ snapshots: zod: 3.25.76 transitivePeerDependencies: - supports-color + optional: true '@jimp/plugin-quantize@1.6.1': dependencies: image-q: 4.0.0 zod: 3.25.76 + optional: true '@jimp/plugin-resize@1.6.1': dependencies: @@ -8415,6 +7543,7 @@ snapshots: zod: 3.25.76 transitivePeerDependencies: - supports-color + optional: true '@jimp/plugin-rotate@1.6.1': dependencies: @@ -8426,6 +7555,7 @@ snapshots: zod: 3.25.76 transitivePeerDependencies: - supports-color + optional: true '@jimp/plugin-threshold@1.6.1': dependencies: @@ -8437,15 +7567,18 @@ snapshots: zod: 3.25.76 transitivePeerDependencies: - supports-color + optional: true '@jimp/types@1.6.1': dependencies: zod: 3.25.76 + optional: true '@jimp/utils@1.6.1': dependencies: '@jimp/types': 1.6.1 tinycolor2: 1.6.0 + optional: true '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -8474,48 +7607,14 @@ snapshots: '@keyv/serialize@1.1.1': {} - '@lancedb/lancedb-darwin-arm64@0.27.2': - optional: true - - '@lancedb/lancedb-linux-arm64-gnu@0.27.2': - optional: true - - '@lancedb/lancedb-linux-arm64-musl@0.27.2': - optional: true - - '@lancedb/lancedb-linux-x64-gnu@0.27.2': - optional: true - - '@lancedb/lancedb-linux-x64-musl@0.27.2': - optional: true - - '@lancedb/lancedb-win32-arm64-msvc@0.27.2': - optional: true - - '@lancedb/lancedb-win32-x64-msvc@0.27.2': - optional: true - - '@lancedb/lancedb@0.27.2(apache-arrow@18.1.0)': - dependencies: - apache-arrow: 18.1.0 - reflect-metadata: 0.2.2 - optionalDependencies: - '@lancedb/lancedb-darwin-arm64': 0.27.2 - '@lancedb/lancedb-linux-arm64-gnu': 0.27.2 - '@lancedb/lancedb-linux-arm64-musl': 0.27.2 - '@lancedb/lancedb-linux-x64-gnu': 0.27.2 - '@lancedb/lancedb-linux-x64-musl': 0.27.2 - '@lancedb/lancedb-win32-arm64-msvc': 0.27.2 - '@lancedb/lancedb-win32-x64-msvc': 0.27.2 - - '@larksuite/openclaw-lark@2026.4.8(openclaw@2026.4.15(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1)(@napi-rs/canvas@0.1.97)(@types/express@5.0.6)(apache-arrow@18.1.0)(encoding@0.1.13)(hono@4.12.12)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))': + '@larksuite/openclaw-lark@2026.4.8(openclaw@2026.4.26)': dependencies: '@larksuiteoapi/node-sdk': 1.60.0 '@sinclair/typebox': 0.34.48 image-size: 2.0.2 zod: 4.3.6 optionalDependencies: - openclaw: 2026.4.15(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1)(@napi-rs/canvas@0.1.97)(@types/express@5.0.6)(apache-arrow@18.1.0)(encoding@0.1.13)(hono@4.12.12)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + openclaw: 2026.4.26 transitivePeerDependencies: - bufferutil - debug @@ -8575,48 +7674,48 @@ snapshots: transitivePeerDependencies: - supports-color - '@mariozechner/clipboard-darwin-arm64@0.3.2': + '@mariozechner/clipboard-darwin-arm64@0.3.3': optional: true - '@mariozechner/clipboard-darwin-universal@0.3.2': + '@mariozechner/clipboard-darwin-universal@0.3.3': optional: true - '@mariozechner/clipboard-darwin-x64@0.3.2': + '@mariozechner/clipboard-darwin-x64@0.3.3': optional: true - '@mariozechner/clipboard-linux-arm64-gnu@0.3.2': + '@mariozechner/clipboard-linux-arm64-gnu@0.3.3': optional: true - '@mariozechner/clipboard-linux-arm64-musl@0.3.2': + '@mariozechner/clipboard-linux-arm64-musl@0.3.3': optional: true - '@mariozechner/clipboard-linux-riscv64-gnu@0.3.2': + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.3': optional: true - '@mariozechner/clipboard-linux-x64-gnu@0.3.2': + '@mariozechner/clipboard-linux-x64-gnu@0.3.3': optional: true - '@mariozechner/clipboard-linux-x64-musl@0.3.2': + '@mariozechner/clipboard-linux-x64-musl@0.3.3': optional: true - '@mariozechner/clipboard-win32-arm64-msvc@0.3.2': + '@mariozechner/clipboard-win32-arm64-msvc@0.3.3': optional: true - '@mariozechner/clipboard-win32-x64-msvc@0.3.2': + '@mariozechner/clipboard-win32-x64-msvc@0.3.3': optional: true - '@mariozechner/clipboard@0.3.2': + '@mariozechner/clipboard@0.3.3': optionalDependencies: - '@mariozechner/clipboard-darwin-arm64': 0.3.2 - '@mariozechner/clipboard-darwin-universal': 0.3.2 - '@mariozechner/clipboard-darwin-x64': 0.3.2 - '@mariozechner/clipboard-linux-arm64-gnu': 0.3.2 - '@mariozechner/clipboard-linux-arm64-musl': 0.3.2 - '@mariozechner/clipboard-linux-riscv64-gnu': 0.3.2 - '@mariozechner/clipboard-linux-x64-gnu': 0.3.2 - '@mariozechner/clipboard-linux-x64-musl': 0.3.2 - '@mariozechner/clipboard-win32-arm64-msvc': 0.3.2 - '@mariozechner/clipboard-win32-x64-msvc': 0.3.2 + '@mariozechner/clipboard-darwin-arm64': 0.3.3 + '@mariozechner/clipboard-darwin-universal': 0.3.3 + '@mariozechner/clipboard-darwin-x64': 0.3.3 + '@mariozechner/clipboard-linux-arm64-gnu': 0.3.3 + '@mariozechner/clipboard-linux-arm64-musl': 0.3.3 + '@mariozechner/clipboard-linux-riscv64-gnu': 0.3.3 + '@mariozechner/clipboard-linux-x64-gnu': 0.3.3 + '@mariozechner/clipboard-linux-x64-musl': 0.3.3 + '@mariozechner/clipboard-win32-arm64-msvc': 0.3.3 + '@mariozechner/clipboard-win32-x64-msvc': 0.3.3 optional: true '@mariozechner/jiti@2.6.5': @@ -8624,9 +7723,10 @@ snapshots: std-env: 3.10.0 yoctocolors: 2.1.2 - '@mariozechner/pi-agent-core@0.66.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)': + '@mariozechner/pi-agent-core@0.70.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)': dependencies: - '@mariozechner/pi-ai': 0.66.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) + '@mariozechner/pi-ai': 0.70.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) + typebox: 1.1.33 transitivePeerDependencies: - '@modelcontextprotocol/sdk' - aws-crt @@ -8636,19 +7736,17 @@ snapshots: - ws - zod - '@mariozechner/pi-ai@0.66.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)': + '@mariozechner/pi-ai@0.70.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)': dependencies: - '@anthropic-ai/sdk': 0.73.0(zod@4.3.6) - '@aws-sdk/client-bedrock-runtime': 3.1028.0 + '@anthropic-ai/sdk': 0.90.0(zod@4.3.6) + '@aws-sdk/client-bedrock-runtime': 3.1038.0 '@google/genai': 1.49.0(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6)) - '@mistralai/mistralai': 1.14.1 - '@sinclair/typebox': 0.34.49 - ajv: 8.18.0 - ajv-formats: 3.0.1(ajv@8.18.0) + '@mistralai/mistralai': 2.2.1 chalk: 5.6.2 openai: 6.26.0(ws@8.20.0)(zod@4.3.6) partial-json: 0.1.7 proxy-agent: 6.5.0 + typebox: 1.1.33 undici: 7.24.6 zod-to-json-schema: 3.25.1(zod@4.3.6) transitivePeerDependencies: @@ -8660,14 +7758,13 @@ snapshots: - ws - zod - '@mariozechner/pi-coding-agent@0.66.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)': + '@mariozechner/pi-coding-agent@0.70.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)': dependencies: '@mariozechner/jiti': 2.6.5 - '@mariozechner/pi-agent-core': 0.66.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) - '@mariozechner/pi-ai': 0.66.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) - '@mariozechner/pi-tui': 0.66.1 + '@mariozechner/pi-agent-core': 0.70.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) + '@mariozechner/pi-ai': 0.70.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) + '@mariozechner/pi-tui': 0.70.2 '@silvia-odwyer/photon-node': 0.3.4 - ajv: 8.18.0 chalk: 5.6.2 cli-highlight: 2.1.11 diff: 8.0.4 @@ -8680,10 +7777,12 @@ snapshots: minimatch: 10.2.4 proper-lockfile: 4.1.2 strip-ansi: 7.2.0 + typebox: 1.1.33 undici: 7.24.6 + uuid: 14.0.0 yaml: 2.8.3 optionalDependencies: - '@mariozechner/clipboard': 0.3.2 + '@mariozechner/clipboard': 0.3.3 transitivePeerDependencies: - '@modelcontextprotocol/sdk' - aws-crt @@ -8693,7 +7792,7 @@ snapshots: - ws - zod - '@mariozechner/pi-tui@0.66.1': + '@mariozechner/pi-tui@0.70.2': dependencies: '@types/mime-types': 2.1.4 chalk: 5.6.2 @@ -8703,17 +7802,7 @@ snapshots: optionalDependencies: koffi: 2.15.2 - '@matrix-org/matrix-sdk-crypto-nodejs@0.4.0': - dependencies: - https-proxy-agent: 7.0.6 - node-downloader-helper: 2.1.11 - transitivePeerDependencies: - - supports-color - optional: true - - '@matrix-org/matrix-sdk-crypto-wasm@18.0.0': {} - - '@mistralai/mistralai@1.14.1': + '@mistralai/mistralai@2.2.1': dependencies: ws: 8.20.0 zod: 4.3.6 @@ -8744,8 +7833,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@mozilla/readability@0.6.0': {} - '@napi-rs/canvas-android-arm64@0.1.80': optional: true @@ -8835,21 +7922,12 @@ snapshots: '@napi-rs/canvas-linux-x64-musl': 0.1.97 '@napi-rs/canvas-win32-arm64-msvc': 0.1.97 '@napi-rs/canvas-win32-x64-msvc': 0.1.97 - - '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1)': - dependencies: - '@emnapi/core': 1.9.2 - '@emnapi/runtime': 1.9.1 - '@tybys/wasm-util': 0.10.1 optional: true - '@noble/ciphers@2.1.1': {} + '@noble/hashes@2.0.1': + optional: true - '@noble/curves@2.0.1': - dependencies: - '@noble/hashes': 2.0.1 - - '@noble/hashes@2.0.1': {} + '@nodable/entities@2.1.0': {} '@nodelib/fs.scandir@2.1.5': dependencies: @@ -8877,19 +7955,6 @@ snapshots: dependencies: semver: 7.7.4 - '@pierre/diffs@1.1.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@pierre/theme': 0.0.28 - '@shikijs/transformers': 3.23.0 - diff: 8.0.3 - hast-util-to-html: 9.0.5 - lru_map: 0.4.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - shiki: 3.23.0 - - '@pierre/theme@0.0.28': {} - '@pinojs/redact@0.4.0': {} '@pkgjs/parseargs@0.11.0': @@ -9450,208 +8515,90 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.0': optional: true - '@scure/base@2.0.0': {} - - '@scure/bip32@2.0.1': - dependencies: - '@noble/curves': 2.0.1 - '@noble/hashes': 2.0.1 - '@scure/base': 2.0.0 - - '@scure/bip39@2.0.1': - dependencies: - '@noble/hashes': 2.0.1 - '@scure/base': 2.0.0 - - '@shikijs/core@3.23.0': - dependencies: - '@shikijs/types': 3.23.0 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - hast-util-to-html: 9.0.5 - - '@shikijs/engine-javascript@3.23.0': - dependencies: - '@shikijs/types': 3.23.0 - '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 4.3.5 - - '@shikijs/engine-oniguruma@3.23.0': - dependencies: - '@shikijs/types': 3.23.0 - '@shikijs/vscode-textmate': 10.0.2 - - '@shikijs/langs@3.23.0': - dependencies: - '@shikijs/types': 3.23.0 - - '@shikijs/themes@3.23.0': - dependencies: - '@shikijs/types': 3.23.0 - - '@shikijs/transformers@3.23.0': - dependencies: - '@shikijs/core': 3.23.0 - '@shikijs/types': 3.23.0 - - '@shikijs/types@3.23.0': - dependencies: - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - - '@shikijs/vscode-textmate@10.0.2': {} - '@silvia-odwyer/photon-node@0.3.4': {} '@sinclair/typebox@0.34.48': {} - '@sinclair/typebox@0.34.49': {} - '@sindresorhus/is@4.6.0': {} - '@slack/bolt@4.7.0(@types/express@5.0.6)': + '@smithy/config-resolver@4.4.17': dependencies: - '@slack/logger': 4.0.1 - '@slack/oauth': 3.0.5 - '@slack/socket-mode': 2.0.6 - '@slack/types': 2.20.1 - '@slack/web-api': 7.15.0 - '@types/express': 5.0.6 - axios: 1.13.6(debug@4.4.3) - express: 5.2.1 - path-to-regexp: 8.3.0 - raw-body: 3.0.2 - tsscmp: 1.0.6 - transitivePeerDependencies: - - bufferutil - - debug - - supports-color - - utf-8-validate - - '@slack/logger@4.0.1': - dependencies: - '@types/node': 25.6.0 - - '@slack/oauth@3.0.5': - dependencies: - '@slack/logger': 4.0.1 - '@slack/web-api': 7.15.0 - '@types/jsonwebtoken': 9.0.10 - '@types/node': 25.6.0 - jsonwebtoken: 9.0.3 - transitivePeerDependencies: - - debug - - '@slack/socket-mode@2.0.6': - dependencies: - '@slack/logger': 4.0.1 - '@slack/web-api': 7.15.0 - '@types/node': 25.6.0 - '@types/ws': 8.18.1 - eventemitter3: 5.0.4 - ws: 8.20.0 - transitivePeerDependencies: - - bufferutil - - debug - - utf-8-validate - - '@slack/types@2.20.1': {} - - '@slack/web-api@7.15.0': - dependencies: - '@slack/logger': 4.0.1 - '@slack/types': 2.20.1 - '@types/node': 25.6.0 - '@types/retry': 0.12.0 - axios: 1.13.6(debug@4.4.3) - eventemitter3: 5.0.4 - form-data: 4.0.5 - is-electron: 2.2.2 - is-stream: 2.0.1 - p-queue: 6.6.2 - p-retry: 4.6.2 - retry: 0.13.1 - transitivePeerDependencies: - - debug - - '@smithy/config-resolver@4.4.14': - dependencies: - '@smithy/node-config-provider': 4.3.13 - '@smithy/types': 4.14.0 + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 '@smithy/util-config-provider': 4.2.2 - '@smithy/util-endpoints': 3.3.4 - '@smithy/util-middleware': 4.2.13 + '@smithy/util-endpoints': 3.4.2 + '@smithy/util-middleware': 4.2.14 tslib: 2.8.1 - '@smithy/core@3.23.14': + '@smithy/core@3.23.17': dependencies: - '@smithy/protocol-http': 5.3.13 - '@smithy/types': 4.14.0 - '@smithy/url-parser': 4.2.13 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 '@smithy/util-base64': 4.3.2 '@smithy/util-body-length-browser': 4.2.2 - '@smithy/util-middleware': 4.2.13 - '@smithy/util-stream': 4.5.22 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-stream': 4.5.25 '@smithy/util-utf8': 4.2.2 '@smithy/uuid': 1.1.2 tslib: 2.8.1 - '@smithy/credential-provider-imds@4.2.13': + '@smithy/credential-provider-imds@4.2.14': dependencies: - '@smithy/node-config-provider': 4.3.13 - '@smithy/property-provider': 4.2.13 - '@smithy/types': 4.14.0 - '@smithy/url-parser': 4.2.13 + '@smithy/node-config-provider': 4.3.14 + '@smithy/property-provider': 4.2.14 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 tslib: 2.8.1 - '@smithy/eventstream-codec@4.2.13': + '@smithy/eventstream-codec@4.2.14': dependencies: '@aws-crypto/crc32': 5.2.0 - '@smithy/types': 4.14.0 + '@smithy/types': 4.14.1 '@smithy/util-hex-encoding': 4.2.2 tslib: 2.8.1 - '@smithy/eventstream-serde-browser@4.2.13': + '@smithy/eventstream-serde-browser@4.2.14': dependencies: - '@smithy/eventstream-serde-universal': 4.2.13 - '@smithy/types': 4.14.0 + '@smithy/eventstream-serde-universal': 4.2.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/eventstream-serde-config-resolver@4.3.13': + '@smithy/eventstream-serde-config-resolver@4.3.14': dependencies: - '@smithy/types': 4.14.0 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/eventstream-serde-node@4.2.13': + '@smithy/eventstream-serde-node@4.2.14': dependencies: - '@smithy/eventstream-serde-universal': 4.2.13 - '@smithy/types': 4.14.0 + '@smithy/eventstream-serde-universal': 4.2.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/eventstream-serde-universal@4.2.13': + '@smithy/eventstream-serde-universal@4.2.14': dependencies: - '@smithy/eventstream-codec': 4.2.13 - '@smithy/types': 4.14.0 + '@smithy/eventstream-codec': 4.2.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/fetch-http-handler@5.3.16': + '@smithy/fetch-http-handler@5.3.17': dependencies: - '@smithy/protocol-http': 5.3.13 - '@smithy/querystring-builder': 4.2.13 - '@smithy/types': 4.14.0 + '@smithy/protocol-http': 5.3.14 + '@smithy/querystring-builder': 4.2.14 + '@smithy/types': 4.14.1 '@smithy/util-base64': 4.3.2 tslib: 2.8.1 - '@smithy/hash-node@4.2.13': + '@smithy/hash-node@4.2.14': dependencies: - '@smithy/types': 4.14.0 + '@smithy/types': 4.14.1 '@smithy/util-buffer-from': 4.2.2 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@smithy/invalid-dependency@4.2.13': + '@smithy/invalid-dependency@4.2.14': dependencies: - '@smithy/types': 4.14.0 + '@smithy/types': 4.14.1 tslib: 2.8.1 '@smithy/is-array-buffer@2.2.0': @@ -9662,121 +8609,121 @@ snapshots: dependencies: tslib: 2.8.1 - '@smithy/middleware-content-length@4.2.13': + '@smithy/middleware-content-length@4.2.14': dependencies: - '@smithy/protocol-http': 5.3.13 - '@smithy/types': 4.14.0 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/middleware-endpoint@4.4.29': + '@smithy/middleware-endpoint@4.4.32': dependencies: - '@smithy/core': 3.23.14 - '@smithy/middleware-serde': 4.2.17 - '@smithy/node-config-provider': 4.3.13 - '@smithy/shared-ini-file-loader': 4.4.8 - '@smithy/types': 4.14.0 - '@smithy/url-parser': 4.2.13 - '@smithy/util-middleware': 4.2.13 + '@smithy/core': 3.23.17 + '@smithy/middleware-serde': 4.2.20 + '@smithy/node-config-provider': 4.3.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-middleware': 4.2.14 tslib: 2.8.1 - '@smithy/middleware-retry@4.5.0': + '@smithy/middleware-retry@4.5.6': dependencies: - '@smithy/core': 3.23.14 - '@smithy/node-config-provider': 4.3.13 - '@smithy/protocol-http': 5.3.13 - '@smithy/service-error-classification': 4.2.13 - '@smithy/smithy-client': 4.12.9 - '@smithy/types': 4.14.0 - '@smithy/util-middleware': 4.2.13 - '@smithy/util-retry': 4.3.0 + '@smithy/core': 3.23.17 + '@smithy/node-config-provider': 4.3.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/service-error-classification': 4.3.1 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.5 '@smithy/uuid': 1.1.2 tslib: 2.8.1 - '@smithy/middleware-serde@4.2.17': + '@smithy/middleware-serde@4.2.20': dependencies: - '@smithy/core': 3.23.14 - '@smithy/protocol-http': 5.3.13 - '@smithy/types': 4.14.0 + '@smithy/core': 3.23.17 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/middleware-stack@4.2.13': + '@smithy/middleware-stack@4.2.14': dependencies: - '@smithy/types': 4.14.0 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/node-config-provider@4.3.13': + '@smithy/node-config-provider@4.3.14': dependencies: - '@smithy/property-provider': 4.2.13 - '@smithy/shared-ini-file-loader': 4.4.8 - '@smithy/types': 4.14.0 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/node-http-handler@4.5.2': + '@smithy/node-http-handler@4.6.1': dependencies: - '@smithy/protocol-http': 5.3.13 - '@smithy/querystring-builder': 4.2.13 - '@smithy/types': 4.14.0 + '@smithy/protocol-http': 5.3.14 + '@smithy/querystring-builder': 4.2.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/property-provider@4.2.13': + '@smithy/property-provider@4.2.14': dependencies: - '@smithy/types': 4.14.0 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/protocol-http@5.3.13': + '@smithy/protocol-http@5.3.14': dependencies: - '@smithy/types': 4.14.0 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/querystring-builder@4.2.13': + '@smithy/querystring-builder@4.2.14': dependencies: - '@smithy/types': 4.14.0 + '@smithy/types': 4.14.1 '@smithy/util-uri-escape': 4.2.2 tslib: 2.8.1 - '@smithy/querystring-parser@4.2.13': + '@smithy/querystring-parser@4.2.14': dependencies: - '@smithy/types': 4.14.0 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/service-error-classification@4.2.13': + '@smithy/service-error-classification@4.3.1': dependencies: - '@smithy/types': 4.14.0 + '@smithy/types': 4.14.1 - '@smithy/shared-ini-file-loader@4.4.8': + '@smithy/shared-ini-file-loader@4.4.9': dependencies: - '@smithy/types': 4.14.0 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/signature-v4@5.3.13': + '@smithy/signature-v4@5.3.14': dependencies: '@smithy/is-array-buffer': 4.2.2 - '@smithy/protocol-http': 5.3.13 - '@smithy/types': 4.14.0 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 '@smithy/util-hex-encoding': 4.2.2 - '@smithy/util-middleware': 4.2.13 + '@smithy/util-middleware': 4.2.14 '@smithy/util-uri-escape': 4.2.2 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@smithy/smithy-client@4.12.9': + '@smithy/smithy-client@4.12.13': dependencies: - '@smithy/core': 3.23.14 - '@smithy/middleware-endpoint': 4.4.29 - '@smithy/middleware-stack': 4.2.13 - '@smithy/protocol-http': 5.3.13 - '@smithy/types': 4.14.0 - '@smithy/util-stream': 4.5.22 + '@smithy/core': 3.23.17 + '@smithy/middleware-endpoint': 4.4.32 + '@smithy/middleware-stack': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-stream': 4.5.25 tslib: 2.8.1 - '@smithy/types@4.14.0': + '@smithy/types@4.14.1': dependencies: tslib: 2.8.1 - '@smithy/url-parser@4.2.13': + '@smithy/url-parser@4.2.14': dependencies: - '@smithy/querystring-parser': 4.2.13 - '@smithy/types': 4.14.0 + '@smithy/querystring-parser': 4.2.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 '@smithy/util-base64@4.3.2': @@ -9807,49 +8754,49 @@ snapshots: dependencies: tslib: 2.8.1 - '@smithy/util-defaults-mode-browser@4.3.45': + '@smithy/util-defaults-mode-browser@4.3.49': dependencies: - '@smithy/property-provider': 4.2.13 - '@smithy/smithy-client': 4.12.9 - '@smithy/types': 4.14.0 + '@smithy/property-provider': 4.2.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/util-defaults-mode-node@4.2.49': + '@smithy/util-defaults-mode-node@4.2.54': dependencies: - '@smithy/config-resolver': 4.4.14 - '@smithy/credential-provider-imds': 4.2.13 - '@smithy/node-config-provider': 4.3.13 - '@smithy/property-provider': 4.2.13 - '@smithy/smithy-client': 4.12.9 - '@smithy/types': 4.14.0 + '@smithy/config-resolver': 4.4.17 + '@smithy/credential-provider-imds': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/property-provider': 4.2.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/util-endpoints@3.3.4': + '@smithy/util-endpoints@3.4.2': dependencies: - '@smithy/node-config-provider': 4.3.13 - '@smithy/types': 4.14.0 + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 '@smithy/util-hex-encoding@4.2.2': dependencies: tslib: 2.8.1 - '@smithy/util-middleware@4.2.13': + '@smithy/util-middleware@4.2.14': dependencies: - '@smithy/types': 4.14.0 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/util-retry@4.3.0': + '@smithy/util-retry@4.3.5': dependencies: - '@smithy/service-error-classification': 4.2.13 - '@smithy/types': 4.14.0 + '@smithy/service-error-classification': 4.3.1 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/util-stream@4.5.22': + '@smithy/util-stream@4.5.25': dependencies: - '@smithy/fetch-http-handler': 5.3.16 - '@smithy/node-http-handler': 4.5.2 - '@smithy/types': 4.14.0 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/node-http-handler': 4.6.1 + '@smithy/types': 4.14.1 '@smithy/util-base64': 4.3.2 '@smithy/util-buffer-from': 4.2.2 '@smithy/util-hex-encoding': 4.2.2 @@ -9874,75 +8821,7 @@ snapshots: dependencies: tslib: 2.8.1 - '@snazzah/davey-android-arm-eabi@0.1.11': - optional: true - - '@snazzah/davey-android-arm64@0.1.11': - optional: true - - '@snazzah/davey-darwin-arm64@0.1.11': - optional: true - - '@snazzah/davey-darwin-x64@0.1.11': - optional: true - - '@snazzah/davey-freebsd-x64@0.1.11': - optional: true - - '@snazzah/davey-linux-arm-gnueabihf@0.1.11': - optional: true - - '@snazzah/davey-linux-arm64-gnu@0.1.11': - optional: true - - '@snazzah/davey-linux-arm64-musl@0.1.11': - optional: true - - '@snazzah/davey-linux-x64-gnu@0.1.11': - optional: true - - '@snazzah/davey-linux-x64-musl@0.1.11': - optional: true - - '@snazzah/davey-wasm32-wasi@0.1.11(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1)': - dependencies: - '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - optional: true - - '@snazzah/davey-win32-arm64-msvc@0.1.11': - optional: true - - '@snazzah/davey-win32-ia32-msvc@0.1.11': - optional: true - - '@snazzah/davey-win32-x64-msvc@0.1.11': - optional: true - - '@snazzah/davey@0.1.11(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1)': - optionalDependencies: - '@snazzah/davey-android-arm-eabi': 0.1.11 - '@snazzah/davey-android-arm64': 0.1.11 - '@snazzah/davey-darwin-arm64': 0.1.11 - '@snazzah/davey-darwin-x64': 0.1.11 - '@snazzah/davey-freebsd-x64': 0.1.11 - '@snazzah/davey-linux-arm-gnueabihf': 0.1.11 - '@snazzah/davey-linux-arm64-gnu': 0.1.11 - '@snazzah/davey-linux-arm64-musl': 0.1.11 - '@snazzah/davey-linux-x64-gnu': 0.1.11 - '@snazzah/davey-linux-x64-musl': 0.1.11 - '@snazzah/davey-wasm32-wasi': 0.1.11(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1) - '@snazzah/davey-win32-arm64-msvc': 0.1.11 - '@snazzah/davey-win32-ia32-msvc': 0.1.11 - '@snazzah/davey-win32-x64-msvc': 0.1.11 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - optional: true - - '@soimy/dingtalk@3.5.3(openclaw@2026.4.15(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1)(@napi-rs/canvas@0.1.97)(@types/express@5.0.6)(apache-arrow@18.1.0)(encoding@0.1.13)(hono@4.12.12)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))': + '@soimy/dingtalk@3.5.3(openclaw@2026.4.26)': dependencies: axios: 1.13.6(debug@4.4.3) dingtalk-stream: 2.1.5 @@ -9951,7 +8830,7 @@ snapshots: pdf-parse: 2.4.5 zod: 4.3.6 optionalDependencies: - openclaw: 2026.4.15(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1)(@napi-rs/canvas@0.1.97)(@types/express@5.0.6)(apache-arrow@18.1.0)(encoding@0.1.13)(hono@4.12.12)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + openclaw: 2026.4.26 transitivePeerDependencies: - bufferutil - debug @@ -9960,15 +8839,11 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@swc/helpers@0.5.21': - dependencies: - tslib: 2.8.1 - '@szmarczak/http-timer@4.0.6': dependencies: defer-to-connect: 2.0.1 - '@tencent-weixin/openclaw-weixin@2.1.8': + '@tencent-weixin/openclaw-weixin@2.1.10': dependencies: qrcode-terminal: 0.12.0 zod: 4.3.6 @@ -10014,11 +8889,6 @@ snapshots: '@tootallnate/quickjs-emscripten@0.23.0': {} - '@tybys/wasm-util@0.10.1': - dependencies: - tslib: 2.8.1 - optional: true - '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': @@ -10042,21 +8912,11 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@types/body-parser@1.19.6': - dependencies: - '@types/connect': 3.4.38 - '@types/node': 25.6.0 - - '@types/bun@1.3.11': - dependencies: - bun-types: 1.3.11 - optional: true - '@types/cacheable-request@6.0.3': dependencies: '@types/http-cache-semantics': 4.2.0 '@types/keyv': 3.1.4 - '@types/node': 25.5.0 + '@types/node': 25.6.0 '@types/responselike': 1.0.3 '@types/chai@5.2.3': @@ -10064,14 +8924,6 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 - '@types/command-line-args@5.2.3': {} - - '@types/command-line-usage@5.0.4': {} - - '@types/connect@3.4.38': - dependencies: - '@types/node': 25.6.0 - '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 @@ -10086,24 +8938,9 @@ snapshots: '@types/estree@1.0.8': {} - '@types/events@3.0.3': {} - - '@types/express-serve-static-core@5.1.1': - dependencies: - '@types/node': 25.6.0 - '@types/qs': 6.15.0 - '@types/range-parser': 1.2.7 - '@types/send': 1.2.1 - - '@types/express@5.0.6': - dependencies: - '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 5.1.1 - '@types/serve-static': 2.2.0 - '@types/fs-extra@9.0.13': dependencies: - '@types/node': 25.5.0 + '@types/node': 25.6.0 '@types/hast@3.0.4': dependencies: @@ -10111,20 +8948,13 @@ snapshots: '@types/http-cache-semantics@4.2.0': {} - '@types/http-errors@2.0.5': {} - '@types/json-schema@7.0.15': {} - '@types/jsonwebtoken@9.0.10': - dependencies: - '@types/ms': 2.1.0 - '@types/node': 25.6.0 - '@types/katex@0.16.8': {} '@types/keyv@3.1.4': dependencies: - '@types/node': 25.5.0 + '@types/node': 25.6.0 '@types/long@4.0.2': {} @@ -10138,11 +8968,8 @@ snapshots: '@types/node@10.17.60': {} - '@types/node@16.9.1': {} - - '@types/node@20.19.39': - dependencies: - undici-types: 6.21.0 + '@types/node@16.9.1': + optional: true '@types/node@24.12.0': dependencies: @@ -10162,10 +8989,6 @@ snapshots: xmlbuilder: 15.1.1 optional: true - '@types/qs@6.15.0': {} - - '@types/range-parser@1.2.7': {} - '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: '@types/react': 19.2.14 @@ -10176,19 +8999,10 @@ snapshots: '@types/responselike@1.0.3': dependencies: - '@types/node': 25.5.0 + '@types/node': 25.6.0 '@types/retry@0.12.0': {} - '@types/send@1.2.1': - dependencies: - '@types/node': 25.6.0 - - '@types/serve-static@2.2.0': - dependencies: - '@types/http-errors': 2.0.5 - '@types/node': 25.6.0 - '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -10298,6 +9112,10 @@ snapshots: '@ungap/structured-clone@1.3.0': {} + '@vincentkoc/qrcode-tui@0.2.1': + dependencies: + qrcode: 1.5.4 + '@vitejs/plugin-react@5.2.0(vite@7.3.1(@types/node@25.5.0)(jiti@1.21.7)(yaml@2.8.3))': dependencies: '@babel/core': 7.29.0 @@ -10351,11 +9169,6 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - '@wasm-audio-decoders/common@9.0.7': - dependencies: - '@eshaz/web-worker': 1.2.2 - simple-yenc: 1.0.4 - '@wecom/aibot-node-sdk@1.0.6': dependencies: axios: 1.13.6(debug@4.4.3) @@ -10366,7 +9179,7 @@ snapshots: - debug - utf-8-validate - '@wecom/wecom-openclaw-plugin@2026.4.8': + '@wecom/wecom-openclaw-plugin@2026.4.27': dependencies: '@wecom/aibot-node-sdk': 1.0.6 fast-xml-parser: 5.5.10 @@ -10406,15 +9219,8 @@ snapshots: '@xmldom/xmldom@0.8.11': {} - abbrev@1.1.1: - optional: true - abbrev@3.0.1: {} - abort-controller@3.0.0: - dependencies: - event-target-shim: 5.0.1 - accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -10452,8 +9258,6 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - another-json@0.2.0: {} - ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -10466,7 +9270,8 @@ snapshots: ansi-styles@6.2.3: {} - any-base@1.1.0: {} + any-base@1.1.0: + optional: true any-promise@1.3.0: {} @@ -10475,18 +9280,6 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.2 - apache-arrow@18.1.0: - dependencies: - '@swc/helpers': 0.5.21 - '@types/command-line-args': 5.2.3 - '@types/command-line-usage': 5.0.4 - '@types/node': 20.19.39 - command-line-args: 5.2.1 - command-line-usage: 7.0.4 - flatbuffers: 24.12.23 - json-bignum: 0.0.3 - tslib: 2.8.1 - app-builder-bin@5.0.0-alpha.12: {} app-builder-lib@26.8.1(dmg-builder@26.8.1)(electron-builder-squirrel-windows@26.8.1): @@ -10532,15 +9325,6 @@ snapshots: transitivePeerDependencies: - supports-color - aproba@2.1.0: - optional: true - - are-we-there-yet@2.0.0: - dependencies: - delegates: 1.0.0 - readable-stream: 3.6.2 - optional: true - arg@5.0.2: {} argparse@1.0.10: @@ -10569,9 +9353,12 @@ snapshots: '@ark/util': 0.56.0 arkregex: 0.0.5 - array-back@3.1.0: {} - - array-back@6.2.3: {} + asn1.js@5.4.1: + dependencies: + bn.js: 4.12.3 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 assert-plus@1.0.0: optional: true @@ -10613,7 +9400,8 @@ snapshots: postcss: 8.5.8 postcss-value-parser: 4.2.0 - await-to-js@3.0.0: {} + await-to-js@3.0.0: + optional: true axios@1.13.6(debug@4.4.3): dependencies: @@ -10629,8 +9417,6 @@ snapshots: balanced-match@4.0.4: {} - base-x@5.0.1: {} - base64-js@1.5.1: {} baseline-browser-mapping@2.10.10: {} @@ -10653,7 +9439,10 @@ snapshots: bluebird@3.4.7: {} - bmp-ts@1.0.9: {} + bmp-ts@1.0.9: + optional: true + + bn.js@4.12.3: {} body-parser@2.2.2: dependencies: @@ -10669,13 +9458,9 @@ snapshots: transitivePeerDependencies: - supports-color - boolbase@1.0.0: {} - boolean@3.2.0: optional: true - bottleneck@2.19.5: {} - bowser@2.14.1: {} brace-expansion@1.1.12: @@ -10703,10 +9488,6 @@ snapshots: node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) - bs58@6.0.0: - dependencies: - base-x: 5.0.1 - buffer-crc32@0.2.13: {} buffer-equal-constant-time@1.0.1: {} @@ -10746,11 +9527,6 @@ snapshots: transitivePeerDependencies: - supports-color - bun-types@1.3.11: - dependencies: - '@types/node': 25.6.0 - optional: true - bytes@3.1.2: {} cacache@19.0.1: @@ -10800,16 +9576,14 @@ snapshots: camelcase-css@2.0.1: {} + camelcase@5.3.1: {} + caniuse-lite@1.0.30001781: {} ccount@2.0.1: {} chai@6.2.2: {} - chalk-template@0.4.0: - dependencies: - chalk: 4.1.2 - chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -10841,8 +9615,7 @@ snapshots: dependencies: readdirp: 5.0.0 - chownr@2.0.0: - optional: true + chownr@2.0.0: {} chownr@3.0.0: {} @@ -10897,6 +9670,12 @@ snapshots: string-width: 4.2.3 optional: true + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + cliui@7.0.4: dependencies: string-width: 4.2.3 @@ -10923,29 +9702,12 @@ snapshots: color-name@1.1.4: {} - color-support@1.1.3: - optional: true - combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 comma-separated-tokens@2.0.3: {} - command-line-args@5.2.1: - dependencies: - array-back: 3.1.0 - find-replace: 3.0.0 - lodash.camelcase: 4.3.0 - typical: 4.0.0 - - command-line-usage@7.0.4: - dependencies: - array-back: 6.2.3 - chalk-template: 0.4.0 - table-layout: 4.1.1 - typical: 7.3.0 - commander@14.0.3: {} commander@4.1.1: {} @@ -10973,9 +9735,6 @@ snapshots: semver: 7.7.4 uint8array-extras: 1.5.0 - console-control-strings@1.1.0: - optional: true - content-disposition@1.0.1: {} content-type@1.0.5: {} @@ -11014,27 +9773,15 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-select@5.2.2: - dependencies: - boolbase: 1.0.0 - css-what: 6.2.2 - domhandler: 5.0.3 - domutils: 3.2.2 - nth-check: 2.1.1 - css-tree@3.2.1: dependencies: mdn-data: 2.27.1 source-map-js: 1.2.1 - css-what@6.2.2: {} - css.escape@1.5.1: {} cssesc@3.0.0: {} - cssom@0.5.0: {} - cssstyle@6.2.0: dependencies: '@asamuzakjp/css-color': 5.0.1 @@ -11067,6 +9814,8 @@ snapshots: dependencies: ms: 2.1.3 + decamelize@1.2.0: {} + decimal.js@10.6.0: {} decode-named-character-reference@1.3.0: @@ -11114,9 +9863,6 @@ snapshots: delayed-stream@1.0.0: {} - delegates@1.0.0: - optional: true - depd@2.0.0: {} dequal@2.0.3: {} @@ -11134,10 +9880,10 @@ snapshots: didyoumean@1.2.2: {} - diff@8.0.3: {} - diff@8.0.4: {} + dijkstrajs@1.0.3: {} + dingbat-to-unicode@1.0.1: {} dingtalk-stream@2.1.5: @@ -11155,8 +9901,6 @@ snapshots: minimatch: 3.1.5 p-limit: 3.1.0 - discord-api-types@0.38.45: {} - dlv@1.1.3: {} dmg-builder@26.8.1(electron-builder-squirrel-windows@26.8.1): @@ -11188,24 +9932,6 @@ snapshots: dom-accessibility-api@0.6.3: {} - dom-serializer@2.0.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 - - domelementtype@2.3.0: {} - - domhandler@5.0.3: - dependencies: - domelementtype: 2.3.0 - - domutils@3.2.2: - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - dot-prop@10.1.0: dependencies: type-fest: 5.5.0 @@ -11216,7 +9942,7 @@ snapshots: dotenv@16.6.1: {} - dotenv@17.4.1: {} + dotenv@17.4.2: {} duck@0.1.12: dependencies: @@ -11337,8 +10063,6 @@ snapshots: entities@6.0.1: {} - entities@7.0.1: {} - env-paths@2.2.1: {} env-paths@3.0.0: {} @@ -11501,21 +10225,16 @@ snapshots: etag@1.8.1: {} - event-target-shim@5.0.1: {} - - eventemitter3@4.0.7: {} - eventemitter3@5.0.4: {} - events@3.3.0: {} - eventsource-parser@3.0.6: {} eventsource@3.0.7: dependencies: eventsource-parser: 3.0.6 - exif-parser@0.1.12: {} + exif-parser@0.1.12: + optional: true expect-type@1.3.0: {} @@ -11574,9 +10293,6 @@ snapshots: extsprintf@1.4.1: optional: true - fake-indexeddb@6.2.5: - optional: true - fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -11607,17 +10323,22 @@ snapshots: dependencies: path-expression-matcher: 1.2.1 + fast-xml-builder@1.1.5: + dependencies: + path-expression-matcher: 1.5.0 + fast-xml-parser@5.5.10: dependencies: fast-xml-builder: 1.1.4 path-expression-matcher: 1.2.1 strnum: 2.2.2 - fast-xml-parser@5.5.8: + fast-xml-parser@5.7.2: dependencies: - fast-xml-builder: 1.1.4 - path-expression-matcher: 1.2.1 - strnum: 2.2.2 + '@nodable/entities': 2.1.0 + fast-xml-builder: 1.1.5 + path-expression-matcher: 1.5.0 + strnum: 2.2.3 fastq@1.20.1: dependencies: @@ -11679,9 +10400,10 @@ snapshots: transitivePeerDependencies: - supports-color - find-replace@3.0.0: + find-up@4.1.0: dependencies: - array-back: 3.1.0 + locate-path: 5.0.0 + path-exists: 4.0.0 find-up@5.0.0: dependencies: @@ -11693,8 +10415,6 @@ snapshots: flatted: 3.4.2 keyv: 4.5.4 - flatbuffers@24.12.23: {} - flatted@3.4.2: {} follow-redirects@1.15.11(debug@4.4.3): @@ -11767,7 +10487,6 @@ snapshots: fs-minipass@2.1.0: dependencies: minipass: 3.3.6 - optional: true fs-minipass@3.0.3: dependencies: @@ -11783,30 +10502,6 @@ snapshots: function-bind@1.1.2: {} - gauge@3.0.2: - dependencies: - aproba: 2.1.0 - color-support: 1.1.3 - console-control-strings: 1.1.0 - has-unicode: 2.0.1 - object-assign: 4.1.1 - signal-exit: 3.0.7 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wide-align: 1.1.5 - optional: true - - gaxios@6.7.1(encoding@0.1.13): - dependencies: - extend: 3.0.2 - https-proxy-agent: 7.0.6 - is-stream: 2.0.1 - node-fetch: 2.7.0(encoding@0.1.13) - uuid: 9.0.1 - transitivePeerDependencies: - - encoding - - supports-color - gaxios@7.1.4: dependencies: extend: 3.0.2 @@ -11815,15 +10510,6 @@ snapshots: transitivePeerDependencies: - supports-color - gcp-metadata@6.1.1(encoding@0.1.13): - dependencies: - gaxios: 6.7.1(encoding@0.1.13) - google-logging-utils: 0.0.2 - json-bigint: 1.0.0 - transitivePeerDependencies: - - encoding - - supports-color - gcp-metadata@8.1.2: dependencies: gaxios: 7.1.4 @@ -11882,6 +10568,7 @@ snapshots: dependencies: image-q: 4.0.0 omggif: 1.0.10 + optional: true glob-parent@5.1.2: dependencies: @@ -11944,20 +10631,6 @@ snapshots: transitivePeerDependencies: - supports-color - google-auth-library@9.15.1(encoding@0.1.13): - dependencies: - base64-js: 1.5.1 - ecdsa-sig-formatter: 1.0.11 - gaxios: 6.7.1(encoding@0.1.13) - gcp-metadata: 6.1.1(encoding@0.1.13) - gtoken: 7.1.0(encoding@0.1.13) - jws: 4.0.1 - transitivePeerDependencies: - - encoding - - supports-color - - google-logging-utils@0.0.2: {} - google-logging-utils@1.1.3: {} gopd@1.2.0: {} @@ -11978,24 +10651,6 @@ snapshots: graceful-fs@4.2.11: {} - grammy@1.42.0(encoding@0.1.13): - dependencies: - '@grammyjs/types': 3.26.0 - abort-controller: 3.0.0 - debug: 4.4.3 - node-fetch: 2.7.0(encoding@0.1.13) - transitivePeerDependencies: - - encoding - - supports-color - - gtoken@7.1.0(encoding@0.1.13): - dependencies: - gaxios: 6.7.1(encoding@0.1.13) - jws: 4.0.1 - transitivePeerDependencies: - - encoding - - supports-color - has-flag@4.0.0: {} has-property-descriptors@1.0.2: @@ -12009,9 +10664,6 @@ snapshots: dependencies: has-symbols: 1.1.0 - has-unicode@2.0.1: - optional: true - hashery@1.5.1: dependencies: hookified: 1.15.1 @@ -12061,20 +10713,6 @@ snapshots: dependencies: '@types/hast': 3.0.4 - hast-util-to-html@9.0.5: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - ccount: 2.0.1 - comma-separated-tokens: 2.0.3 - hast-util-whitespace: 3.0.0 - html-void-elements: 3.0.0 - mdast-util-to-hast: 13.2.1 - property-information: 7.1.0 - space-separated-tokens: 2.0.2 - stringify-entities: 4.0.4 - zwitch: 2.0.4 - hast-util-to-jsx-runtime@2.3.6: dependencies: '@types/estree': 1.0.8 @@ -12142,23 +10780,12 @@ snapshots: transitivePeerDependencies: - '@noble/hashes' - html-escaper@3.0.3: {} - html-parse-stringify@3.0.1: dependencies: void-elements: 3.1.0 html-url-attributes@3.0.1: {} - html-void-elements@3.0.0: {} - - htmlparser2@10.1.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - entities: 7.0.1 - http-cache-semantics@4.2.0: {} http-errors@2.0.1: @@ -12188,6 +10815,8 @@ snapshots: quick-lru: 5.1.1 resolve-alpn: 1.2.1 + http_ece@1.2.0: {} + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 @@ -12224,6 +10853,7 @@ snapshots: image-q@4.0.0: dependencies: '@types/node': 16.9.1 + optional: true image-size@2.0.2: {} @@ -12265,8 +10895,6 @@ snapshots: is-decimal@2.0.1: {} - is-electron@2.2.2: {} - is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -12291,8 +10919,6 @@ snapshots: is-promise@4.0.0: {} - is-stream@2.0.1: {} - is-unicode-supported@0.1.0: {} is-unicode-supported@2.1.0: {} @@ -12348,6 +10974,7 @@ snapshots: '@jimp/utils': 1.6.1 transitivePeerDependencies: - supports-color + optional: true jiti@1.21.7: {} @@ -12355,7 +10982,8 @@ snapshots: jose@6.2.2: {} - jpeg-js@0.4.4: {} + jpeg-js@0.4.4: + optional: true js-tokens@4.0.0: {} @@ -12396,8 +11024,6 @@ snapshots: dependencies: bignumber.js: 9.3.1 - json-bignum@0.0.3: {} - json-buffer@3.0.1: {} json-schema-to-ts@3.1.1: @@ -12428,19 +11054,6 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 - jsonwebtoken@9.0.3: - dependencies: - jws: 4.0.1 - lodash.includes: 4.3.0 - lodash.isboolean: 3.0.3 - lodash.isinteger: 4.0.4 - lodash.isnumber: 3.0.3 - lodash.isplainobject: 4.0.6 - lodash.isstring: 4.0.1 - lodash.once: 4.1.1 - ms: 2.1.3 - semver: 7.7.4 - jszip@3.10.1: dependencies: lie: 3.3.0 @@ -12459,8 +11072,6 @@ snapshots: jwa: 2.0.1 safe-buffer: 5.2.1 - jwt-decode@4.0.0: {} - katex@0.16.45: dependencies: commander: 8.3.0 @@ -12491,46 +11102,26 @@ snapshots: lines-and-columns@1.2.4: {} - linkedom@0.18.12: - dependencies: - css-select: 5.2.2 - cssom: 0.5.0 - html-escaper: 3.0.3 - htmlparser2: 10.1.0 - uhyphen: 0.2.0 - linkify-it@5.0.0: dependencies: uc.micro: 2.1.0 + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 - lodash.camelcase@4.3.0: {} - lodash.escaperegexp@4.1.2: {} lodash.identity@3.0.0: {} - lodash.includes@4.3.0: {} - - lodash.isboolean@3.0.3: {} - lodash.isequal@4.5.0: {} - lodash.isinteger@4.0.4: {} - - lodash.isnumber@3.0.3: {} - - lodash.isplainobject@4.0.6: {} - - lodash.isstring@4.0.1: {} - lodash.merge@4.6.2: {} - lodash.once@4.1.1: {} - lodash.pickby@4.6.0: {} lodash@4.17.23: {} @@ -12545,8 +11136,6 @@ snapshots: is-unicode-supported: 2.1.0 yoctocolors: 2.1.2 - loglevel@1.9.2: {} - long@4.0.0: {} long@5.3.2: {} @@ -12575,8 +11164,6 @@ snapshots: lru-cache@7.18.3: {} - lru_map@0.4.1: {} - lucide-react@0.563.0(react@19.2.4): dependencies: react: 19.2.4 @@ -12587,11 +11174,6 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - make-dir@3.1.0: - dependencies: - semver: 6.3.1 - optional: true - make-fetch-happen@14.0.3: dependencies: '@npmcli/agent': 3.0.0 @@ -12641,30 +11223,6 @@ snapshots: math-intrinsics@1.1.0: {} - matrix-events-sdk@0.0.1: {} - - matrix-js-sdk@41.3.0: - dependencies: - '@babel/runtime': 7.29.2 - '@matrix-org/matrix-sdk-crypto-wasm': 18.0.0 - another-json: 0.2.0 - bs58: 6.0.0 - content-type: 1.0.5 - jwt-decode: 4.0.0 - loglevel: 1.9.2 - matrix-events-sdk: 0.0.1 - matrix-widget-api: 1.17.0 - oidc-client-ts: 3.5.0 - p-retry: 7.1.1 - sdp-transform: 3.0.0 - unhomoglyph: 1.0.6 - uuid: 13.0.0 - - matrix-widget-api@1.17.0: - dependencies: - '@types/events': 3.0.3 - events: 3.3.0 - mdast-util-find-and-replace@3.0.2: dependencies: '@types/mdast': 4.0.4 @@ -13060,7 +11618,8 @@ snapshots: mime@2.6.0: {} - mime@3.0.0: {} + mime@3.0.0: + optional: true mime@4.1.0: {} @@ -13074,6 +11633,8 @@ snapshots: min-indent@1.0.1: {} + minimalistic-assert@1.0.1: {} + minimatch@10.2.4: dependencies: brace-expansion: 5.0.5 @@ -13120,8 +11681,7 @@ snapshots: dependencies: yallist: 4.0.0 - minipass@5.0.0: - optional: true + minipass@5.0.0: {} minipass@7.1.3: {} @@ -13129,7 +11689,6 @@ snapshots: dependencies: minipass: 3.3.6 yallist: 4.0.0 - optional: true minizlib@3.1.0: dependencies: @@ -13139,8 +11698,7 @@ snapshots: dependencies: minimist: 1.2.8 - mkdirp@1.0.4: - optional: true + mkdirp@1.0.4: {} motion-dom@12.38.0: dependencies: @@ -13148,10 +11706,6 @@ snapshots: motion-utils@12.36.0: {} - mpg123-decoder@1.0.3: - dependencies: - '@wasm-audio-decoders/common': 9.0.7 - ms@2.1.3: {} music-metadata@11.12.3: @@ -13190,34 +11744,12 @@ snapshots: node-addon-api@1.7.2: optional: true - node-addon-api@8.7.0: - optional: true - node-api-version@0.2.1: dependencies: semver: 7.7.4 node-domexception@1.0.0: {} - node-downloader-helper@2.1.11: - optional: true - - node-edge-tts@1.2.10: - dependencies: - https-proxy-agent: 7.0.6 - ws: 8.20.0 - yargs: 17.7.2 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - node-fetch@2.7.0(encoding@0.1.13): - dependencies: - whatwg-url: 5.0.0 - optionalDependencies: - encoding: 0.1.13 - node-fetch@3.3.2: dependencies: data-uri-to-buffer: 4.0.1 @@ -13241,16 +11773,8 @@ snapshots: node-machine-id@1.1.12: {} - node-readable-to-web-readable-stream@0.4.2: - optional: true - node-releases@2.0.36: {} - nopt@5.0.0: - dependencies: - abbrev: 1.1.1 - optional: true - nopt@8.1.0: dependencies: abbrev: 3.0.1 @@ -13259,32 +11783,6 @@ snapshots: normalize-url@6.1.0: {} - nostr-tools@2.23.3(typescript@5.9.3): - dependencies: - '@noble/ciphers': 2.1.1 - '@noble/curves': 2.0.1 - '@noble/hashes': 2.0.1 - '@scure/base': 2.0.0 - '@scure/bip32': 2.0.1 - '@scure/bip39': 2.0.1 - nostr-wasm: 0.1.0 - optionalDependencies: - typescript: 5.9.3 - - nostr-wasm@0.1.0: {} - - npmlog@5.0.1: - dependencies: - are-we-there-yet: 2.0.0 - console-control-strings: 1.1.0 - gauge: 3.0.2 - set-blocking: 2.0.0 - optional: true - - nth-check@2.1.1: - dependencies: - boolbase: 1.0.0 - object-assign@4.1.1: {} object-hash@3.0.0: {} @@ -13296,11 +11794,8 @@ snapshots: obug@2.1.1: {} - oidc-client-ts@3.5.0: - dependencies: - jwt-decode: 4.0.0 - - omggif@1.0.10: {} + omggif@1.0.10: + optional: true on-exit-leak-free@2.1.2: {} @@ -13320,14 +11815,6 @@ snapshots: dependencies: mimic-function: 5.0.1 - oniguruma-parser@0.12.1: {} - - oniguruma-to-es@4.3.5: - dependencies: - oniguruma-parser: 0.12.1 - regex: 6.1.0 - regex-recursion: 6.0.2 - openai@6.26.0(ws@8.20.0)(zod@4.3.6): optionalDependencies: ws: 8.20.0 @@ -13338,104 +11825,48 @@ snapshots: ws: 8.20.0 zod: 4.3.6 - openclaw@2026.4.15(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1)(@napi-rs/canvas@0.1.97)(@types/express@5.0.6)(apache-arrow@18.1.0)(encoding@0.1.13)(hono@4.12.12)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3): + openclaw@2026.4.26: dependencies: - '@agentclientprotocol/sdk': 0.18.2(zod@4.3.6) - '@anthropic-ai/vertex-sdk': 0.15.0(encoding@0.1.13)(zod@4.3.6) - '@aws-sdk/client-bedrock': 3.1028.0 - '@aws-sdk/client-bedrock-runtime': 3.1028.0 - '@aws-sdk/credential-provider-node': 3.972.30 - '@aws/bedrock-token-generator': 1.1.0 - '@buape/carbon': 0.15.0(@discordjs/opus@0.10.0(encoding@0.1.13))(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1)(hono@4.12.12)(opusscript@0.1.1) + '@agentclientprotocol/sdk': 0.20.0(zod@4.3.6) '@clack/prompts': 1.2.0 - '@google/genai': 1.49.0(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6)) - '@grammyjs/runner': 2.0.3(grammy@1.42.0(encoding@0.1.13)) - '@grammyjs/transformer-throttler': 1.2.1(grammy@1.42.0(encoding@0.1.13)) - '@homebridge/ciao': 1.3.6 - '@lancedb/lancedb': 0.27.2(apache-arrow@18.1.0) - '@larksuiteoapi/node-sdk': 1.60.0 '@lydell/node-pty': 1.2.0-beta.12 - '@mariozechner/pi-agent-core': 0.66.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) - '@mariozechner/pi-ai': 0.66.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) - '@mariozechner/pi-coding-agent': 0.66.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) - '@mariozechner/pi-tui': 0.66.1 - '@matrix-org/matrix-sdk-crypto-wasm': 18.0.0 + '@mariozechner/pi-agent-core': 0.70.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) + '@mariozechner/pi-ai': 0.70.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) + '@mariozechner/pi-coding-agent': 0.70.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) + '@mariozechner/pi-tui': 0.70.2 '@modelcontextprotocol/sdk': 1.29.0(zod@4.3.6) - '@mozilla/readability': 0.6.0 - '@napi-rs/canvas': 0.1.97 - '@pierre/diffs': 1.1.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@sinclair/typebox': 0.34.49 - '@slack/bolt': 4.7.0(@types/express@5.0.6) - '@slack/web-api': 7.15.0 - '@whiskeysockets/baileys': 7.0.0-rc.9(jimp@1.6.1)(sharp@0.34.5) + '@vincentkoc/qrcode-tui': 0.2.1 ajv: 8.18.0 chalk: 5.6.2 chokidar: 5.0.0 - cli-highlight: 2.1.11 commander: 14.0.3 croner: 10.0.1 - discord-api-types: 0.38.45 - dotenv: 17.4.1 - express: 5.2.1 + dotenv: 17.4.2 file-type: 22.0.1 - gaxios: 7.1.4 - google-auth-library: 10.6.2 - grammy: 1.42.0(encoding@0.1.13) https-proxy-agent: 7.0.6 ipaddr.js: 2.3.0 - jimp: 1.6.1 jiti: 2.6.1 json5: 2.2.3 jszip: 3.10.1 - linkedom: 0.18.12 - long: 5.3.2 markdown-it: 14.1.1 - matrix-js-sdk: 41.3.0 - mpg123-decoder: 1.0.3 - node-edge-tts: 1.2.10 - nostr-tools: 2.23.3(typescript@5.9.3) openai: 6.34.0(ws@8.20.0)(zod@4.3.6) - opusscript: 0.1.1 osc-progress: 0.3.0 - pdfjs-dist: 5.6.205 - playwright-core: 1.59.1 proxy-agent: 8.0.1 - qrcode-terminal: 0.12.0 - sharp: 0.34.5 - silk-wasm: 3.7.1 + semver: 7.7.4 sqlite-vec: 0.1.9 tar: 7.5.13 tslog: 4.10.2 - undici: 8.0.2 - uuid: 13.0.0 + typebox: 1.1.33 + undici: 8.1.0 + web-push: 3.6.7 ws: 8.20.0 yaml: 2.8.3 zod: 4.3.6 - optionalDependencies: - '@discordjs/opus': 0.10.0(encoding@0.1.13) - '@matrix-org/matrix-sdk-crypto-nodejs': 0.4.0 - fake-indexeddb: 6.2.5 - music-metadata: 11.12.3 transitivePeerDependencies: - '@cfworker/json-schema' - - '@emnapi/core' - - '@emnapi/runtime' - - '@types/express' - - apache-arrow - - audio-decode - aws-crt - bufferutil - - canvas - - debug - - encoding - - ffmpeg-static - - hono - - link-preview-js - - node-opus - - react - - react-dom - supports-color - - typescript - utf-8-validate option@0.2.4: {} @@ -13449,8 +11880,6 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - opusscript@0.1.1: {} - ora@5.4.1: dependencies: bl: 4.1.0 @@ -13478,23 +11907,24 @@ snapshots: p-cancelable@2.1.1: {} - p-finally@1.0.0: {} + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + p-locate@5.0.0: dependencies: p-limit: 3.1.0 p-map@7.0.4: {} - p-queue@6.6.2: - dependencies: - eventemitter3: 4.0.7 - p-timeout: 3.2.0 - p-queue@9.1.0: dependencies: eventemitter3: 5.0.4 @@ -13509,12 +11939,10 @@ snapshots: dependencies: is-network-error: 1.3.1 - p-timeout@3.2.0: - dependencies: - p-finally: 1.0.0 - p-timeout@7.0.1: {} + p-try@2.2.0: {} + pac-proxy-agent@7.2.0: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 @@ -13556,14 +11984,17 @@ snapshots: pako@1.0.11: {} - parse-bmfont-ascii@1.0.6: {} + parse-bmfont-ascii@1.0.6: + optional: true - parse-bmfont-binary@1.0.6: {} + parse-bmfont-binary@1.0.6: + optional: true parse-bmfont-xml@1.1.6: dependencies: xml-parse-from-string: 1.0.1 xml2js: 0.5.0 + optional: true parse-entities@4.0.2: dependencies: @@ -13599,6 +12030,8 @@ snapshots: path-expression-matcher@1.2.1: {} + path-expression-matcher@1.5.0: {} + path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -13628,11 +12061,6 @@ snapshots: optionalDependencies: '@napi-rs/canvas': 0.1.97 - pdfjs-dist@5.6.205: - optionalDependencies: - '@napi-rs/canvas': 0.1.97 - node-readable-to-web-readable-stream: 0.4.2 - pe-library@0.4.1: {} pend@1.2.0: {} @@ -13670,13 +12098,12 @@ snapshots: pixelmatch@5.3.0: dependencies: pngjs: 6.0.0 + optional: true pkce-challenge@5.0.1: {} playwright-core@1.59.0: {} - playwright-core@1.59.1: {} - playwright@1.59.0: dependencies: playwright-core: 1.59.0 @@ -13691,9 +12118,13 @@ snapshots: png2icons@2.0.1: {} - pngjs@6.0.0: {} + pngjs@5.0.0: {} - pngjs@7.0.0: {} + pngjs@6.0.0: + optional: true + + pngjs@7.0.0: + optional: true postcss-import@15.1.0(postcss@8.5.8): dependencies: @@ -13750,12 +12181,6 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 - prism-media@1.3.5(@discordjs/opus@0.10.0(encoding@0.1.13))(opusscript@0.1.1): - optionalDependencies: - '@discordjs/opus': 0.10.0(encoding@0.1.13) - opusscript: 0.1.1 - optional: true - proc-log@5.0.0: {} process-nextick-args@2.0.1: {} @@ -13858,6 +12283,12 @@ snapshots: qrcode-terminal@0.12.0: {} + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + qs@6.15.0: dependencies: side-channel: 1.1.0 @@ -13999,18 +12430,6 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 - reflect-metadata@0.2.2: {} - - regex-recursion@6.0.2: - dependencies: - regex-utilities: 2.3.0 - - regex-utilities@2.3.0: {} - - regex@6.1.0: - dependencies: - regex-utilities: 2.3.0 - rehype-katex@7.0.1: dependencies: '@types/hast': 3.0.4 @@ -14068,6 +12487,8 @@ snapshots: require-from-string@2.0.2: {} + require-main-filename@2.0.0: {} + resedit@1.7.2: dependencies: pe-library: 0.4.1 @@ -14104,11 +12525,6 @@ snapshots: dependencies: glob: 7.2.3 - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - optional: true - roarr@2.15.4: dependencies: boolean: 3.2.0 @@ -14184,8 +12600,6 @@ snapshots: scheduler@0.27.0: {} - sdp-transform@3.0.0: {} - semver-compare@1.0.0: optional: true @@ -14225,8 +12639,7 @@ snapshots: transitivePeerDependencies: - supports-color - set-blocking@2.0.0: - optional: true + set-blocking@2.0.0: {} set-cookie-parser@2.7.2: {} @@ -14271,17 +12684,6 @@ snapshots: shebang-regex@3.0.0: {} - shiki@3.23.0: - dependencies: - '@shikijs/core': 3.23.0 - '@shikijs/engine-javascript': 3.23.0 - '@shikijs/engine-oniguruma': 3.23.0 - '@shikijs/langs': 3.23.0 - '@shikijs/themes': 3.23.0 - '@shikijs/types': 3.23.0 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -14316,15 +12718,12 @@ snapshots: signal-exit@4.1.0: {} - silk-wasm@3.7.1: {} - simple-update-notifier@2.0.0: dependencies: semver: 7.7.4 - simple-xml-to-json@1.2.7: {} - - simple-yenc@1.0.4: {} + simple-xml-to-json@1.2.7: + optional: true sisteransi@1.0.5: {} @@ -14468,6 +12867,8 @@ snapshots: strnum@2.2.2: {} + strnum@2.2.3: {} + strtok3@10.3.5: dependencies: '@tokenizer/token': 0.3.0 @@ -14510,11 +12911,6 @@ snapshots: symbol-tree@3.2.4: {} - table-layout@4.1.1: - dependencies: - array-back: 6.2.3 - wordwrapjs: 5.1.1 - tagged-tag@1.0.0: {} tailwind-merge@3.5.0: {} @@ -14559,7 +12955,6 @@ snapshots: minizlib: 2.1.2 mkdirp: 1.0.4 yallist: 4.0.0 - optional: true tar@7.5.13: dependencies: @@ -14599,7 +12994,8 @@ snapshots: tinybench@2.9.0: {} - tinycolor2@1.6.0: {} + tinycolor2@1.6.0: + optional: true tinyexec@1.0.4: {} @@ -14638,8 +13034,6 @@ snapshots: dependencies: tldts: 7.0.27 - tr46@0.0.3: {} - tr46@6.0.0: dependencies: punycode: 2.3.1 @@ -14664,8 +13058,6 @@ snapshots: tslog@4.10.2: {} - tsscmp@1.0.6: {} - type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -14683,22 +13075,16 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 + typebox@1.1.33: {} + typescript@5.9.3: {} - typical@4.0.0: {} - - typical@7.3.0: {} - uc.micro@2.1.0: {} - uhyphen@0.2.0: {} - uint8array-extras@1.5.0: {} underscore@1.13.8: {} - undici-types@6.21.0: {} - undici-types@7.16.0: {} undici-types@7.18.2: {} @@ -14707,9 +13093,7 @@ snapshots: undici@7.24.6: {} - undici@8.0.2: {} - - unhomoglyph@1.0.6: {} + undici@8.1.0: {} unified@11.0.5: dependencies: @@ -14806,12 +13190,11 @@ snapshots: utif2@4.1.0: dependencies: pako: 1.0.11 + optional: true util-deprecate@1.0.2: {} - uuid@13.0.0: {} - - uuid@9.0.1: {} + uuid@14.0.0: {} vary@1.1.2: {} @@ -14897,9 +13280,17 @@ snapshots: web-namespaces@2.0.1: {} - web-streams-polyfill@3.3.3: {} + web-push@3.6.7: + dependencies: + asn1.js: 5.4.1 + http_ece: 1.2.0 + https-proxy-agent: 7.0.6 + jws: 4.0.1 + minimist: 1.2.8 + transitivePeerDependencies: + - supports-color - webidl-conversions@3.0.1: {} + web-streams-polyfill@3.3.3: {} webidl-conversions@8.0.1: {} @@ -14913,13 +13304,10 @@ snapshots: transitivePeerDependencies: - '@noble/hashes' - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - when-exit@2.1.5: {} + which-module@2.0.1: {} + which@2.0.2: dependencies: isexe: 2.0.0 @@ -14933,16 +13321,15 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - wide-align@1.1.5: - dependencies: - string-width: 4.2.3 - optional: true - win-guid@0.2.1: {} word-wrap@1.2.5: {} - wordwrapjs@5.1.1: {} + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 wrap-ansi@7.0.0: dependencies: @@ -14962,21 +13349,26 @@ snapshots: xml-name-validator@5.0.0: {} - xml-parse-from-string@1.0.1: {} + xml-parse-from-string@1.0.1: + optional: true xml2js@0.5.0: dependencies: sax: 1.6.0 xmlbuilder: 11.0.1 + optional: true xmlbuilder@10.1.1: {} - xmlbuilder@11.0.1: {} + xmlbuilder@11.0.1: + optional: true xmlbuilder@15.1.1: {} xmlchars@2.2.0: {} + y18n@4.0.3: {} + y18n@5.0.8: {} yallist@3.1.1: {} @@ -14987,10 +13379,29 @@ snapshots: yaml@2.8.3: {} + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + yargs-parser@20.2.9: {} yargs-parser@21.1.1: {} + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + yargs@16.2.0: dependencies: cliui: 7.0.4 @@ -15028,7 +13439,8 @@ snapshots: dependencies: zod: 4.3.6 - zod@3.25.76: {} + zod@3.25.76: + optional: true zod@4.3.6: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 87d7926..1426b4a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,6 @@ packages: - '.' + - 'packages/*' ignoredBuiltDependencies: - electron diff --git a/public/icons/icon.png b/public/icons/icon.png new file mode 100644 index 0000000..2cf53f6 Binary files /dev/null and b/public/icons/icon.png differ diff --git a/public/icons/icon.svg b/public/icons/icon.svg new file mode 100644 index 0000000..ba41fd9 --- /dev/null +++ b/public/icons/icon.svg @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/128x128.png b/resources/icons/128x128.png index 0ff0d55..9bbdd32 100644 Binary files a/resources/icons/128x128.png and b/resources/icons/128x128.png differ diff --git a/resources/icons/16x16.png b/resources/icons/16x16.png index dfcbb74..990023e 100644 Binary files a/resources/icons/16x16.png and b/resources/icons/16x16.png differ diff --git a/resources/icons/256x256.png b/resources/icons/256x256.png index 82fee18..4f90899 100644 Binary files a/resources/icons/256x256.png and b/resources/icons/256x256.png differ diff --git a/resources/icons/32x32.png b/resources/icons/32x32.png index 31a4351..fd7cd63 100644 Binary files a/resources/icons/32x32.png and b/resources/icons/32x32.png differ diff --git a/resources/icons/48x48.png b/resources/icons/48x48.png index 97ce80c..6a7c292 100644 Binary files a/resources/icons/48x48.png and b/resources/icons/48x48.png differ diff --git a/resources/icons/512x512.png b/resources/icons/512x512.png index cdd2f22..2cf53f6 100644 Binary files a/resources/icons/512x512.png and b/resources/icons/512x512.png differ diff --git a/resources/icons/64x64.png b/resources/icons/64x64.png index 5443a39..9244a57 100644 Binary files a/resources/icons/64x64.png and b/resources/icons/64x64.png differ diff --git a/resources/icons/icon-plain.svg b/resources/icons/icon-plain.svg index aaf8d30..ba41fd9 100644 --- a/resources/icons/icon-plain.svg +++ b/resources/icons/icon-plain.svg @@ -1,3 +1,180 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/icon.icns b/resources/icons/icon.icns index fb4c959..93ccaf4 100644 Binary files a/resources/icons/icon.icns and b/resources/icons/icon.icns differ diff --git a/resources/icons/icon.ico b/resources/icons/icon.ico index 2579a56..5532c5b 100644 Binary files a/resources/icons/icon.ico and b/resources/icons/icon.ico differ diff --git a/resources/icons/icon.png b/resources/icons/icon.png index cdd2f22..2cf53f6 100644 Binary files a/resources/icons/icon.png and b/resources/icons/icon.png differ diff --git a/resources/icons/icon.svg b/resources/icons/icon.svg index c1c979f..ba41fd9 100644 --- a/resources/icons/icon.svg +++ b/resources/icons/icon.svg @@ -1,22 +1,180 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - - - - - + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/tray-icon-Template.png b/resources/icons/tray-icon-Template.png index 2f23da4..c071a8a 100644 Binary files a/resources/icons/tray-icon-Template.png and b/resources/icons/tray-icon-Template.png differ diff --git a/resources/icons/tray-icon-template.svg b/resources/icons/tray-icon-template.svg index 1fbdea0..a058d34 100644 --- a/resources/icons/tray-icon-template.svg +++ b/resources/icons/tray-icon-template.svg @@ -1,3 +1,6 @@ - - - + + + + + + diff --git a/resources/openclaw-plugins/cloud-sync/helpers/darwin-amd64/openclaw-sync-helper b/resources/openclaw-plugins/cloud-sync/helpers/darwin-amd64/openclaw-sync-helper new file mode 100755 index 0000000..5ccab80 Binary files /dev/null and b/resources/openclaw-plugins/cloud-sync/helpers/darwin-amd64/openclaw-sync-helper differ diff --git a/resources/openclaw-plugins/cloud-sync/helpers/darwin-arm64/openclaw-sync-helper b/resources/openclaw-plugins/cloud-sync/helpers/darwin-arm64/openclaw-sync-helper new file mode 100755 index 0000000..8477c07 Binary files /dev/null and b/resources/openclaw-plugins/cloud-sync/helpers/darwin-arm64/openclaw-sync-helper differ diff --git a/resources/openclaw-plugins/cloud-sync/openclaw.plugin.json b/resources/openclaw-plugins/cloud-sync/openclaw.plugin.json new file mode 100644 index 0000000..a9a6c77 --- /dev/null +++ b/resources/openclaw-plugins/cloud-sync/openclaw.plugin.json @@ -0,0 +1,28 @@ +{ + "id": "cloud-sync", + "name": "OpenClaw Cloud Sync", + "description": "Back up and restore OpenClaw configuration snapshots.", + "version": "0.1.0", + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "serverUrl": { + "type": "string" + }, + "helperPath": { + "type": "string" + } + }, + "required": ["serverUrl"] + }, + "uiHints": { + "serverUrl": { + "label": "Cloud Sync Server URL", + "placeholder": "https://sync.example.com" + }, + "helperPath": { + "label": "Helper binary path" + } + } +} diff --git a/resources/openclaw-plugins/cloud-sync/package.json b/resources/openclaw-plugins/cloud-sync/package.json new file mode 100644 index 0000000..391b8e5 --- /dev/null +++ b/resources/openclaw-plugins/cloud-sync/package.json @@ -0,0 +1,35 @@ +{ + "name": "@openclaw/cloud-sync", + "version": "0.1.0-yinian.2", + "type": "module", + "private": true, + "packageManager": "npm@10.9.3", + "engines": { + "node": ">=18" + }, + "files": [ + "dist/**", + "helpers/**", + "openclaw.plugin.json" + ], + "scripts": { + "test": "vitest run", + "build": "tsc -p tsconfig.json", + "package": "npm run build" + }, + "devDependencies": { + "@types/node": "^22.15.3", + "typescript": "^5.5.0", + "vitest": "^1.6.0" + }, + "openclaw": { + "extensions": [ + "./dist/index.js" + ], + "install": { + "minHostVersion": ">=2026.4.1" + } + }, + "main": "dist/index.js", + "module": "dist/index.js" +} diff --git a/scripts/after-pack.cjs b/scripts/after-pack.cjs index e3184e5..d6421de 100644 --- a/scripts/after-pack.cjs +++ b/scripts/after-pack.cjs @@ -596,6 +596,21 @@ exports.default = async function afterPack(context) { } } + const staticPluginRoot = join(__dirname, '..', 'resources', 'openclaw-plugins'); + if (existsSync(staticPluginRoot)) { + for (const entry of readdirSync(staticPluginRoot, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const sourceDir = join(staticPluginRoot, entry.name); + if (!existsSync(join(sourceDir, 'openclaw.plugin.json'))) continue; + const pluginDestDir = join(pluginsDestRoot, entry.name); + console.log(`[after-pack] Copying static plugin ${entry.name} -> ${pluginDestDir}`); + rmSync(pluginDestDir, { recursive: true, force: true }); + cpSync(sourceDir, pluginDestDir, { recursive: true, dereference: true }); + cleanupUnnecessaryFiles(pluginDestDir); + patchPluginIds(pluginDestDir, entry.name); + } + } + // 1.2 Copy built-in extension node_modules that electron-builder skipped. // OpenClaw 3.31+ ships built-in extensions (discord, qqbot, etc.) under // dist/extensions//node_modules/. These are skipped by extraResources diff --git a/scripts/bundle-openclaw-plugins.mjs b/scripts/bundle-openclaw-plugins.mjs index 1d34992..cd58e2d 100644 --- a/scripts/bundle-openclaw-plugins.mjs +++ b/scripts/bundle-openclaw-plugins.mjs @@ -23,6 +23,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, '..'); const OUTPUT_ROOT = path.join(ROOT, 'build', 'openclaw-plugins'); const NODE_MODULES = path.join(ROOT, 'node_modules'); +const STATIC_PLUGIN_ROOT = path.join(ROOT, 'resources', 'openclaw-plugins'); // On Windows, pnpm virtual store paths can exceed MAX_PATH (260 chars). // Adding \\?\ prefix bypasses the limit for Win32 fs calls. @@ -241,4 +242,17 @@ for (const plugin of PLUGINS) { bundleOnePlugin(plugin); } +if (fs.existsSync(STATIC_PLUGIN_ROOT)) { + for (const entry of fs.readdirSync(STATIC_PLUGIN_ROOT, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const sourceDir = path.join(STATIC_PLUGIN_ROOT, entry.name); + const outputDir = path.join(OUTPUT_ROOT, entry.name); + const manifestPath = path.join(sourceDir, 'openclaw.plugin.json'); + if (!fs.existsSync(manifestPath)) continue; + echo`📦 Copying static plugin ${entry.name} -> ${outputDir}`; + fs.rmSync(outputDir, { recursive: true, force: true }); + fs.cpSync(sourceDir, outputDir, { recursive: true, dereference: true }); + } +} + echo`✅ Plugin mirrors ready: ${OUTPUT_ROOT}`; diff --git a/scripts/generate-icons.mjs b/scripts/generate-icons.mjs index ba60125..9c01349 100644 --- a/scripts/generate-icons.mjs +++ b/scripts/generate-icons.mjs @@ -12,7 +12,7 @@ const PROJECT_ROOT = path.resolve(__dirname, '..'); const ICONS_DIR = path.join(PROJECT_ROOT, 'resources', 'icons'); const SVG_SOURCE = path.join(ICONS_DIR, 'icon.svg'); -echo`🎨 Generating ClawX icons using Node.js...`; +echo`🎨 Generating 智念助手 icons using Node.js...`; // Check if SVG source exists if (!fs.existsSync(SVG_SOURCE)) { diff --git a/scripts/installer.nsh b/scripts/installer.nsh index a6cbcbe..80afd42 100644 --- a/scripts/installer.nsh +++ b/scripts/installer.nsh @@ -283,9 +283,11 @@ _cu_pathDone: - ; Ask user if they want to remove AppData (preserves .openclaw) + ; Ask user if they want to remove AppData. + ; IMPORTANT: never remove .openclaw. It may contain a user-managed OpenClaw + ; installation, skills, channel state, and the app-managed fallback runtime. MessageBox MB_YESNO|MB_ICONQUESTION \ - "Do you want to remove ClawX application data?$\r$\n$\r$\nThis will delete:$\r$\n • AppData\Local\clawx (local app data)$\r$\n • AppData\Roaming\clawx (roaming app data)$\r$\n$\r$\nYour .openclaw folder (configuration & skills) will be preserved.$\r$\nSelect 'No' to keep all data for future reinstallation." \ + "Do you want to remove ClawX application data?$\r$\n$\r$\nThis will delete:$\r$\n • AppData\Local\clawx (local app data)$\r$\n • AppData\Roaming\clawx (roaming app data)$\r$\n$\r$\nYour .openclaw folder (OpenClaw runtime, configuration, skills, and channel data) will be preserved.$\r$\nSelect 'No' to keep all data for future reinstallation." \ /SD IDNO IDYES _cu_removeData IDNO _cu_skipRemove _cu_removeData: @@ -303,7 +305,7 @@ Sleep 2000 ; --- Always remove current user's AppData first --- - ; NOTE: .openclaw directory is intentionally preserved (user configuration & skills) + ; NOTE: .openclaw directory is intentionally preserved (OpenClaw runtime, user configuration, skills, and channel data) RMDir /r "$LOCALAPPDATA\clawx" RMDir /r "$APPDATA\clawx" diff --git a/scripts/linux/after-install.sh b/scripts/linux/after-install.sh index ffff478..17e095b 100644 --- a/scripts/linux/after-install.sh +++ b/scripts/linux/after-install.sh @@ -19,11 +19,16 @@ if [ -x /opt/ClawX/clawx ]; then ln -sf /opt/ClawX/clawx /usr/local/bin/clawx 2>/dev/null || true fi -# Create symbolic link for openclaw CLI +# Create symbolic link for the app-managed openclaw CLI only when it will not +# overwrite a user-managed OpenClaw installation. OPENCLAW_WRAPPER="/opt/ClawX/resources/cli/openclaw" if [ -f "$OPENCLAW_WRAPPER" ]; then chmod +x "$OPENCLAW_WRAPPER" 2>/dev/null || true - ln -sf "$OPENCLAW_WRAPPER" /usr/local/bin/openclaw 2>/dev/null || true + if [ ! -e /usr/local/bin/openclaw ] || [ "$(readlink /usr/local/bin/openclaw 2>/dev/null || true)" = "$OPENCLAW_WRAPPER" ]; then + ln -sf "$OPENCLAW_WRAPPER" /usr/local/bin/openclaw 2>/dev/null || true + else + echo "Keeping existing /usr/local/bin/openclaw; it is not managed by ClawX." + fi fi # Set chrome-sandbox permissions. diff --git a/scripts/linux/after-remove.sh b/scripts/linux/after-remove.sh index 9dbd238..c115354 100644 --- a/scripts/linux/after-remove.sh +++ b/scripts/linux/after-remove.sh @@ -6,7 +6,13 @@ set -e # Remove symbolic links rm -f /usr/local/bin/clawx 2>/dev/null || true -rm -f /usr/local/bin/openclaw 2>/dev/null || true + +# Only remove the app-managed OpenClaw CLI wrapper. If the user had OpenClaw +# installed separately, leave their command and ~/.openclaw data untouched. +OPENCLAW_WRAPPER="/opt/ClawX/resources/cli/openclaw" +if [ "$(readlink /usr/local/bin/openclaw 2>/dev/null || true)" = "$OPENCLAW_WRAPPER" ]; then + rm -f /usr/local/bin/openclaw 2>/dev/null || true +fi # Update desktop database if command -v update-desktop-database &> /dev/null; then diff --git a/shared/language.ts b/shared/language.ts index f2205c7..b0f6069 100644 --- a/shared/language.ts +++ b/shared/language.ts @@ -1,4 +1,4 @@ -export const SUPPORTED_LANGUAGE_CODES = ['en', 'zh', 'ja', 'ru'] as const; +export const SUPPORTED_LANGUAGE_CODES = ['en', 'zh'] as const; export type LanguageCode = (typeof SUPPORTED_LANGUAGE_CODES)[number]; diff --git a/shared/yinian.ts b/shared/yinian.ts new file mode 100644 index 0000000..dfb5686 --- /dev/null +++ b/shared/yinian.ts @@ -0,0 +1,146 @@ +export type YinianControlPlaneMode = 'mock' | 'http'; + +export interface YinianServerStatus { + mode: YinianControlPlaneMode; + apiBaseUrl?: string; + reachable: boolean; + checkedAt: number; + serverTime?: number; + version?: string; + message?: string; +} + +export interface YinianUser { + id: string; + name: string; + phone?: string; + email?: string; + avatar?: string; +} + +// Compatibility note: early M1 contracts used "hotel" as the tenant object +// name. The product surface now treats this as a generic B-end workspace or +// business object, while the field names remain stable for storage/API safety. +export interface YinianHotel { + id: string; + name: string; + brand?: string; +} + +export interface YinianAuthSession { + authenticated: true; + user: YinianUser; + hotels: YinianHotel[]; + currentHotelId: string; + accessTokenExpiresAt: number; +} + +export interface YinianUnauthenticatedSession { + authenticated: false; +} + +export type YinianSessionState = YinianAuthSession | YinianUnauthenticatedSession; + +export interface YinianPersistedSession { + mode: YinianControlPlaneMode; + user: YinianUser; + hotels: YinianHotel[]; + currentHotelId: string; + accessTokenExpiresAt: number; + refreshToken?: string; + updatedAt: number; +} + +export interface YinianLoginWithSmsInput { + phone: string; + code: string; + captchaCode?: string; + randomStr?: string; +} + +export interface YinianLoginWithPasswordInput { + account: string; + password: string; + captchaCode?: string; + randomStr?: string; +} + +export interface YinianImageCaptcha { + randomStr: string; + image?: string; + imageBase64?: string; + mimeType?: string; + raw?: Record; +} + +export interface YinianSavedCredentials { + account: string; + password?: string; + rememberPassword: boolean; + updatedAt: number; +} + +export interface YinianSkillEntitlement { + skillId: string; + name: string; + version: string; + enabled: boolean; + category: 'ota-monitoring' | 'reporting' | 'guest-comm' | 'ops-automation'; + triggers: Array<'manual' | 'scheduled' | 'webhook' | 'reply'>; + lastRunAt?: number; +} + +export type YinianSkillSyncStatus = 'installed' | 'updated' | 'skipped' | 'disabled' | 'failed'; + +export interface YinianLocalSkill { + skillId: string; + name: string; + version: string; + enabled: boolean; + installedAt: number; + lastSyncedAt: number; + status: YinianSkillSyncStatus; + source: 'mock' | 'nianxx'; + bundleSha256?: string; + error?: string; +} + +export interface YinianSkillSyncResult { + hotelId: string; + syncedAt: number; + skills: YinianLocalSkill[]; +} + +export interface YinianSkillRegistry { + hotelId: string; + updatedAt: number; + skills: YinianLocalSkill[]; +} + +export type YinianSkillRegistryByHotel = Record; + +export type YinianWorkspace = YinianHotel; +export type YinianSkillRegistryByWorkspace = YinianSkillRegistryByHotel; + +export interface YinianNotificationChannel { + id: string; + kind: string; + label: string; + recipient: string; + enabled: boolean; + source: 'kernel' | 'nianxx'; +} + +export interface YinianConfigSnapshot { + serverTime: number; + user: YinianUser; + hotel: YinianHotel; + hotels: YinianHotel[]; + entitlements: YinianSkillEntitlement[]; + notificationChannels: YinianNotificationChannel[]; + featureFlags: Record; + uiPolicy: { + defaultPage: 'today' | 'chat'; + showAdvancedSettings: boolean; + }; +} diff --git a/src/App.tsx b/src/App.tsx index 5ff791d..5c7c07a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,13 +13,17 @@ import { Models } from './pages/Models'; import { Chat } from './pages/Chat'; import { Agents } from './pages/Agents'; import { Channels } from './pages/Channels'; -import { Skills } from './pages/Skills'; +import { YinianSkills } from './pages/YinianSkills'; import { Cron } from './pages/Cron'; import { Settings } from './pages/Settings'; import { Setup } from './pages/Setup'; +import { Today } from './pages/Today'; +import { Knowledge } from './pages/Knowledge'; +import { YinianLogin } from './pages/YinianLogin'; import { useSettingsStore } from './stores/settings'; import { useGatewayStore } from './stores/gateway'; import { useProviderStore } from './stores/providers'; +import { useYinianStore } from './stores/yinian'; import { applyGatewayTransportPreference } from './lib/api-client'; import { rendererExtensionRegistry } from './extensions/registry'; import { loadExternalRendererExtensions } from './extensions/_ext-bridge.generated'; @@ -92,19 +96,31 @@ class ErrorBoundary extends Component< function App() { const navigate = useNavigate(); const location = useLocation(); - const skipSetupForE2E = typeof window !== 'undefined' - && new URLSearchParams(window.location.search).get('e2eSkipSetup') === '1'; + const rendererQuery = typeof window !== 'undefined' + ? new URLSearchParams(window.location.search) + : new URLSearchParams(); + const isE2EMode = rendererQuery.get('e2e') === '1'; + const skipSetupForE2E = rendererQuery.get('e2eSkipSetup') === '1'; const initSettings = useSettingsStore((state) => state.init); const theme = useSettingsStore((state) => state.theme); const language = useSettingsStore((state) => state.language); const setupComplete = useSettingsStore((state) => state.setupComplete); const initGateway = useGatewayStore((state) => state.init); + const startGateway = useGatewayStore((state) => state.start); + const gatewayState = useGatewayStore((state) => state.status.state); const initProviders = useProviderStore((state) => state.init); + const initYinian = useYinianStore((state) => state.init); + const yinianStatus = useYinianStore((state) => state.status); + const yinianSession = useYinianStore((state) => state.session); useEffect(() => { initSettings(); }, [initSettings]); + useEffect(() => { + initYinian(); + }, [initYinian]); + // Sync i18n language with persisted settings on mount useEffect(() => { if (language && language !== i18n.language) { @@ -112,22 +128,56 @@ function App() { } }, [language]); - // Initialize Gateway connection on mount + // Initialize Gateway connection after YINIAN authentication. The v1 product + // should not boot the agent kernel before a workspace context exists. useEffect(() => { - initGateway(); - }, [initGateway]); - - // Initialize provider snapshot on mount - useEffect(() => { - initProviders(); - }, [initProviders]); - - // Redirect to setup wizard if not complete - useEffect(() => { - if (!setupComplete && !skipSetupForE2E && !location.pathname.startsWith('/setup')) { - navigate('/setup'); + if (isE2EMode && (setupComplete || skipSetupForE2E)) { + void initGateway(); + return; } - }, [setupComplete, skipSetupForE2E, location.pathname, navigate]); + + if (yinianSession.authenticated) { + void initGateway().then(() => { + const state = useGatewayStore.getState().status.state; + if (state === 'stopped' || state === 'error') { + void startGateway(); + } + }); + } + }, [gatewayState, initGateway, isE2EMode, setupComplete, skipSetupForE2E, startGateway, yinianSession.authenticated]); + + // Initialize provider snapshot after login; provider UI remains available for + // implementation/admin paths while customer-facing model keys stay hidden. + useEffect(() => { + if (yinianSession.authenticated) { + initProviders(); + } + }, [initProviders, yinianSession.authenticated]); + + // YINIAN login replaces the ClawX first-run setup as the primary entry path. + // E2E keeps the legacy ClawX flow available so existing regression tests can + // exercise the original pages without authenticating against YINIAN first. + useEffect(() => { + if (yinianStatus === 'idle' || yinianStatus === 'loading') return; + + if (isE2EMode) { + if (!setupComplete && !skipSetupForE2E && !location.pathname.startsWith('/setup')) { + navigate('/setup'); + } + return; + } + + if (!yinianSession.authenticated && !location.pathname.startsWith('/login')) { + navigate('/login', { replace: true }); + return; + } + + if (yinianSession.authenticated && (location.pathname.startsWith('/login') || location.pathname.startsWith('/setup'))) { + navigate('/today', { replace: true }); + return; + } + + }, [isE2EMode, setupComplete, skipSetupForE2E, location.pathname, navigate, yinianSession.authenticated, yinianStatus]); // Listen for navigation events from main process useEffect(() => { @@ -176,21 +226,34 @@ function App() { const extraRoutes = rendererExtensionRegistry.getExtraRoutes(); + if (yinianStatus === 'idle' || yinianStatus === 'loading') { + return ( +
+ 正在载入智念助手... +
+ ); + } + return ( + } /> + {/* Setup wizard (shown on first launch) */} } /> {/* Main application routes */} }> - } /> + : } /> + } /> + } /> } /> } /> } /> - } /> + } /> } /> + } /> } /> {extraRoutes.map((r) => ( } /> diff --git a/src/assets/logo.svg b/src/assets/logo.svg index 2b7c359..ba41fd9 100644 --- a/src/assets/logo.svg +++ b/src/assets/logo.svg @@ -1,3 +1,180 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/channels/ChannelConfigModal.tsx b/src/components/channels/ChannelConfigModal.tsx index b3db691..5fc6219 100644 --- a/src/components/channels/ChannelConfigModal.tsx +++ b/src/components/channels/ChannelConfigModal.tsx @@ -81,7 +81,7 @@ export function ChannelConfigModal({ onChannelSaved, }: ChannelConfigModalProps) { const { t } = useTranslation('channels'); - const { channels, addChannel, fetchChannels } = useChannelsStore(); + const { fetchChannels } = useChannelsStore(); const [selectedType, setSelectedType] = useState(initialSelectedType); const [configValues, setConfigValues] = useState>({}); const [channelName, setChannelName] = useState(''); @@ -192,23 +192,9 @@ export function ChannelConfigModal({ }, [selectedType, loadingConfig, showChannelName]); const finishSave = useCallback(async (channelType: ChannelType) => { - const displayName = showChannelName && channelName.trim() - ? channelName.trim() - : CHANNEL_NAMES[channelType]; - const existingChannel = channels.find((channel) => channel.type === channelType); - - if (!existingChannel) { - await addChannel({ - type: channelType, - name: displayName, - token: meta?.configFields[0]?.key ? configValues[meta.configFields[0].key] : undefined, - }); - } else { - await fetchChannels(); - } - + await fetchChannels(); await onChannelSaved?.(channelType); - }, [addChannel, channelName, channels, configValues, fetchChannels, meta?.configFields, onChannelSaved, showChannelName]); + }, [fetchChannels, onChannelSaved]); const finishSaveRef = useRef(finishSave); const onCloseRef = useRef(onClose); diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx index 4f64bce..ab1639f 100644 --- a/src/components/layout/MainLayout.tsx +++ b/src/components/layout/MainLayout.tsx @@ -8,14 +8,14 @@ import { TitleBar } from './TitleBar'; export function MainLayout() { return ( -
+
{/* Title bar: drag region on macOS, icon + controls on Windows */} {/* Below the title bar: sidebar + content */}
-
+
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index b8e9577..bc72b9c 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -3,34 +3,42 @@ * Navigation sidebar with menu items. * No longer fixed - sits inside the flex layout below the title bar. */ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { NavLink, useLocation, useNavigate } from 'react-router-dom'; import { Network, Bot, Puzzle, Clock, + LibraryBig, Settings as SettingsIcon, PanelLeftClose, PanelLeft, Plus, - Terminal, - ExternalLink, Trash2, Cpu, + UserRound, + Terminal, + ExternalLink, + History as HistoryIcon, + MessageCircle, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { rendererExtensionRegistry } from '@/extensions/registry'; import { useSettingsStore } from '@/stores/settings'; -import { useChatStore } from '@/stores/chat'; +import { useChatStore, type ChatSession } from '@/stores/chat'; import { useGatewayStore } from '@/stores/gateway'; -import { useAgentsStore } from '@/stores/agents'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { hostApiFetch } from '@/lib/host-api'; import { useTranslation } from 'react-i18next'; import logoSvg from '@/assets/logo.svg'; +import { useYinianStore } from '@/stores/yinian'; +import { + getEffectiveChannelAccountName, + subscribeYinianLocalPrefs, +} from '@/lib/yinian-local-prefs'; type SessionBucketKey = | 'today' @@ -40,6 +48,31 @@ type SessionBucketKey = | 'withinMonth' | 'older'; +type HistorySessionSource = { + channelKey: string; + channelLabel: string; + accountKey: string; + accountLabel: string; +}; + +type HistoryAccountGroup = { + key: string; + channelKey: string; + channelLabel: string; + accountKey: string; + accountLabel: string; + sessions: ChatSession[]; + latestAt: number; +}; + +type HistoryChannelGroup = { + key: string; + label: string; + accounts: HistoryAccountGroup[]; + sessions: ChatSession[]; + latestAt: number; +}; + interface NavItemProps { to: string; icon: React.ReactNode; @@ -59,9 +92,9 @@ function NavItem({ to, icon, label, badge, collapsed, onClick, testId }: NavItem className={({ isActive }) => cn( 'flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-[14px] font-medium transition-colors', - 'hover:bg-black/5 dark:hover:bg-white/5 text-foreground/80', + 'hover:bg-slate-100 dark:hover:bg-white/5 text-foreground/80', isActive - ? 'bg-black/5 dark:bg-white/10 text-foreground' + ? 'bg-[#1E3A8A]/10 text-[#1E3A8A] dark:bg-[#1E3A8A]/30 dark:text-blue-100' : '', collapsed && 'justify-center px-0' ) @@ -105,18 +138,153 @@ function getSessionBucket(activityMs: number, nowMs: number): SessionBucketKey { return 'older'; } +function formatHistoryTime( + activityMs: number, + nowMs: number, + t: (key: string, options?: Record) => string, + language: string, +): string { + if (!activityMs || activityMs <= 0) return t('chat:history.emptyTime'); + const diffMs = Math.max(0, nowMs - activityMs); + const minuteMs = 60 * 1000; + const hourMs = 60 * minuteMs; + if (diffMs < minuteMs) return t('chat:history.justNow'); + if (diffMs < hourMs) return t('chat:history.minutesAgo', { count: Math.floor(diffMs / minuteMs) }); + if (diffMs < 24 * hourMs) return t('chat:history.hoursAgo', { count: Math.floor(diffMs / hourMs) }); + return new Intl.DateTimeFormat(language === 'zh' ? 'zh-CN' : 'en-US', { + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: false, + }).format(activityMs); +} + const INITIAL_NOW_MS = Date.now(); -function getAgentIdFromSessionKey(sessionKey: string): string { - if (!sessionKey.startsWith('agent:')) return 'main'; - const [, agentId] = sessionKey.split(':'); - return agentId || 'main'; +const CHANNEL_LABELS: Record = { + chat: '桌面对话', + cron: '定时任务', + wechat: '微信', + 'openclaw-weixin': '微信', + wecom: '企业微信', + dingtalk: '钉钉', + feishu: '飞书', + lark: '飞书', + telegram: 'Telegram', + whatsapp: 'WhatsApp', + discord: 'Discord', + signal: 'Signal', + line: 'LINE', + msteams: 'Teams', + googlechat: 'Google Chat', + mattermost: 'Mattermost', + qqbot: 'QQ', + agentbus: 'AgentBus', +}; + +function normalizeHistoryChannelKey(value: string | undefined): string { + const normalized = (value ?? '').toLowerCase(); + if (normalized === 'openclaw-weixin') return 'wechat'; + if (normalized === 'lark') return 'feishu'; + return normalized; +} + +function extractAccountFromManagedAgentId(agentId: string, channelKey: string): string { + const candidatePrefixes = [ + `channel-${channelKey}-`, + channelKey === 'wechat' ? 'channel-openclaw-weixin-' : '', + ].filter(Boolean); + + for (const prefix of candidatePrefixes) { + if (agentId.startsWith(prefix)) { + const accountId = agentId.slice(prefix.length).trim(); + return accountId || 'default'; + } + } + + if (agentId === `channel-${channelKey}`) return 'default'; + if (channelKey === 'wechat' && agentId === 'channel-openclaw-weixin') return 'default'; + return 'default'; +} + +function getAccountLabel(accountId: string, channelKey: string): string { + if (channelKey === 'chat') return '桌面端'; + if (channelKey === 'cron') return '任务会话'; + if (!accountId || accountId === 'default') return '默认账号'; + if (accountId.length > 22) return `${accountId.slice(0, 10)}…${accountId.slice(-6)}`; + return accountId; +} + +function getSessionSource(session: ChatSession): HistorySessionSource { + const parts = session.key.split(':'); + const agentId = parts.length >= 2 ? parts[1]?.toLowerCase() ?? '' : ''; + const suffixHead = parts.length >= 3 ? parts[2]?.toLowerCase() : ''; + const rawLabel = `${session.label ?? ''} ${session.displayName ?? ''}`.toLowerCase(); + const deliveryChannel = normalizeHistoryChannelKey(session.deliveryContext?.channel); + const deliveryAccountId = session.deliveryContext?.accountId?.trim(); + + if (suffixHead === 'cron' || rawLabel.includes('cron') || rawLabel.includes('定时')) { + return { + channelKey: 'cron', + channelLabel: CHANNEL_LABELS.cron, + accountKey: 'cron', + accountLabel: getAccountLabel('cron', 'cron'), + }; + } + + if (deliveryChannel && CHANNEL_LABELS[deliveryChannel] && deliveryChannel !== 'chat') { + const accountKey = deliveryAccountId || 'default'; + const fallbackAccountLabel = getAccountLabel(accountKey, deliveryChannel); + return { + channelKey: deliveryChannel, + channelLabel: CHANNEL_LABELS[deliveryChannel], + accountKey, + accountLabel: getEffectiveChannelAccountName(deliveryChannel, accountKey, fallbackAccountLabel), + }; + } + + const normalizedSuffix = normalizeHistoryChannelKey(suffixHead); + if (normalizedSuffix && CHANNEL_LABELS[normalizedSuffix] && normalizedSuffix !== 'chat') { + const accountKey = extractAccountFromManagedAgentId(agentId, normalizedSuffix); + const fallbackAccountLabel = getAccountLabel(accountKey, normalizedSuffix); + return { + channelKey: normalizedSuffix, + channelLabel: CHANNEL_LABELS[normalizedSuffix], + accountKey, + accountLabel: getEffectiveChannelAccountName(normalizedSuffix, accountKey, fallbackAccountLabel), + }; + } + + return { + channelKey: 'chat', + channelLabel: CHANNEL_LABELS.chat, + accountKey: 'desktop', + accountLabel: getAccountLabel('desktop', 'chat'), + }; +} + +function cleanHistorySessionLabel(label: string): string { + return label + .replace(/^\s*System\s*:\s*\[\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(?::\d{2})?\s+GMT[+-]\d+\]\s+[A-Za-z][\w -]*(?:\[[^\]]+\])?\s+(?:DM|Group|Room|Chat|Message)\s+\|\s+\S+(?:\s+\[msg:[^\]]+\])?\s*/i, '') + .trim(); +} + +function NavGroupLabel({ children, collapsed }: { children: React.ReactNode; collapsed?: boolean }) { + if (collapsed) return
; + return ( +
+ {children} +
+ ); } export function Sidebar() { const sidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed); const setSidebarCollapsed = useSettingsStore((state) => state.setSidebarCollapsed); const devModeUnlocked = useSettingsStore((state) => state.devModeUnlocked); + const yinianSession = useYinianStore((state) => state.session); + const yinianConfig = useYinianStore((state) => state.config); const sessions = useChatStore((s) => s.sessions); const currentSessionKey = useChatStore((s) => s.currentSessionKey); @@ -147,14 +315,16 @@ export function Sidebar() { cancelled = true; }; }, [isGatewayReady, loadHistory, loadSessions]); - const agents = useAgentsStore((s) => s.agents); - const fetchAgents = useAgentsStore((s) => s.fetchAgents); - const navigate = useNavigate(); - const isOnChat = useLocation().pathname === '/'; + const location = useLocation(); + const isOnChat = location.pathname === '/chat'; + const isE2EMode = new URLSearchParams(window.location.search).get('e2e') === '1'; + const showServiceContext = yinianSession.authenticated && yinianConfig; - const getSessionLabel = (key: string, displayName?: string, label?: string) => - sessionLabels[key] ?? label ?? displayName ?? key; + const getSessionLabel = (key: string, displayName?: string, label?: string) => { + const rawLabel = sessionLabels[key] ?? label ?? displayName ?? key; + return cleanHistorySessionLabel(rawLabel) || key; + }; const openDevConsole = async () => { try { @@ -173,83 +343,213 @@ export function Sidebar() { } }; - const { t } = useTranslation(['common', 'chat']); + const { t, i18n } = useTranslation(['common', 'chat']); const [sessionToDelete, setSessionToDelete] = useState<{ key: string; label: string } | null>(null); const [nowMs, setNowMs] = useState(INITIAL_NOW_MS); + const [historyOpen, setHistoryOpen] = useState(false); + const [activeHistoryAccount, setActiveHistoryAccount] = useState(''); + const [localPrefsVersion, setLocalPrefsVersion] = useState(0); + const historyCloseTimerRef = useRef(null); + const formatRelativeHistoryTime = (activityMs: number) => formatHistoryTime(activityMs, nowMs, t, i18n.language); + + const openHistoryPopover = () => { + if (historyCloseTimerRef.current) { + window.clearTimeout(historyCloseTimerRef.current); + historyCloseTimerRef.current = null; + } + setHistoryOpen(true); + }; + + const scheduleHistoryPopoverClose = () => { + if (historyCloseTimerRef.current) { + window.clearTimeout(historyCloseTimerRef.current); + } + historyCloseTimerRef.current = window.setTimeout(() => { + setHistoryOpen(false); + historyCloseTimerRef.current = null; + }, 180); + }; useEffect(() => { const timer = window.setInterval(() => { setNowMs(Date.now()); }, 60 * 1000); - return () => window.clearInterval(timer); + return () => { + window.clearInterval(timer); + if (historyCloseTimerRef.current) { + window.clearTimeout(historyCloseTimerRef.current); + } + }; }, []); - useEffect(() => { - void fetchAgents(); - }, [fetchAgents]); + useEffect(() => subscribeYinianLocalPrefs(() => { + setLocalPrefsVersion((version) => version + 1); + }), []); - const agentNameById = useMemo( - () => Object.fromEntries((agents ?? []).map((agent) => [agent.id, agent.name])), - [agents], + const sortedSessions = useMemo(() => [...sessions].sort((a, b) => { + const left = sessionLastActivity[b.key] ?? b.updatedAt ?? 0; + const right = sessionLastActivity[a.key] ?? a.updatedAt ?? 0; + return left - right; + }), [sessionLastActivity, sessions]); + + const historyChannels = useMemo(() => { + const groups = new Map(); + + for (const session of sortedSessions) { + const source = getSessionSource(session); + const channelLabel = t(`chat:historyChannels.${source.channelKey}`, { defaultValue: source.channelLabel }); + const accountLabel = source.channelKey === 'chat' + ? t('chat:historyAccounts.desktop') + : source.channelKey === 'cron' + ? t('chat:historyAccounts.cron') + : source.accountKey === 'default' + ? t('chat:historyAccounts.default') + : source.accountLabel; + const channel = groups.get(source.channelKey) ?? { + key: source.channelKey, + label: channelLabel, + accounts: [], + sessions: [], + latestAt: 0, + }; + const latestAt = sessionLastActivity[session.key] ?? session.updatedAt ?? 0; + const accountGroupKey = `${source.channelKey}:${source.accountKey}`; + let account = channel.accounts.find((item) => item.key === accountGroupKey); + if (!account) { + account = { + key: accountGroupKey, + channelKey: source.channelKey, + channelLabel, + accountKey: source.accountKey, + accountLabel, + sessions: [], + latestAt: 0, + }; + channel.accounts.push(account); + } + account.sessions.push(session); + account.latestAt = Math.max(account.latestAt, latestAt); + channel.sessions.push(session); + channel.latestAt = Math.max(channel.latestAt, latestAt); + groups.set(source.channelKey, channel); + } + + const channelGroups = [...groups.values()].sort((a, b) => { + if (a.key === 'chat') return -1; + if (b.key === 'chat') return 1; + return b.latestAt - a.latestAt; + }); + channelGroups.forEach((channel) => { + channel.accounts.sort((a, b) => { + if (a.accountKey === 'desktop' || a.accountKey === 'default') return -1; + if (b.accountKey === 'desktop' || b.accountKey === 'default') return 1; + return b.latestAt - a.latestAt; + }); + }); + return channelGroups; + }, [localPrefsVersion, sessionLastActivity, sortedSessions, t]); + + const historyAccountGroups = useMemo( + () => historyChannels.flatMap((channel) => channel.accounts), + [historyChannels], ); - const sessionBuckets: Array<{ key: SessionBucketKey; label: string; sessions: typeof sessions }> = [ - { key: 'today', label: t('chat:historyBuckets.today'), sessions: [] }, - { key: 'yesterday', label: t('chat:historyBuckets.yesterday'), sessions: [] }, - { key: 'withinWeek', label: t('chat:historyBuckets.withinWeek'), sessions: [] }, - { key: 'withinTwoWeeks', label: t('chat:historyBuckets.withinTwoWeeks'), sessions: [] }, - { key: 'withinMonth', label: t('chat:historyBuckets.withinMonth'), sessions: [] }, - { key: 'older', label: t('chat:historyBuckets.older'), sessions: [] }, - ]; - const sessionBucketMap = Object.fromEntries(sessionBuckets.map((bucket) => [bucket.key, bucket])) as Record< - SessionBucketKey, - (typeof sessionBuckets)[number] - >; - for (const session of [...sessions].sort((a, b) => - (sessionLastActivity[b.key] ?? 0) - (sessionLastActivity[a.key] ?? 0) - )) { - const bucketKey = getSessionBucket(sessionLastActivity[session.key] ?? 0, nowMs); - sessionBucketMap[bucketKey].sessions.push(session); - } + useEffect(() => { + if (!historyAccountGroups.length) { + if (activeHistoryAccount) setActiveHistoryAccount(''); + return; + } + if (!historyAccountGroups.some((account) => account.key === activeHistoryAccount)) { + setActiveHistoryAccount(historyAccountGroups[0].key); + } + }, [activeHistoryAccount, historyAccountGroups]); + + const activeHistoryGroup = historyAccountGroups.find((account) => account.key === activeHistoryAccount) + ?? historyAccountGroups[0]; + const activeHistorySessions = activeHistoryGroup?.sessions + ?? []; + const sessionBuckets = useMemo(() => { + const buckets: Array<{ key: SessionBucketKey; label: string; sessions: ChatSession[] }> = [ + { key: 'today', label: t('chat:historyBuckets.today'), sessions: [] }, + { key: 'yesterday', label: t('chat:historyBuckets.yesterday'), sessions: [] }, + { key: 'withinWeek', label: t('chat:historyBuckets.withinWeek'), sessions: [] }, + { key: 'withinTwoWeeks', label: t('chat:historyBuckets.withinTwoWeeks'), sessions: [] }, + { key: 'withinMonth', label: t('chat:historyBuckets.withinMonth'), sessions: [] }, + { key: 'older', label: t('chat:historyBuckets.older'), sessions: [] }, + ]; + const bucketMap = Object.fromEntries(buckets.map((bucket) => [bucket.key, bucket])) as Record< + SessionBucketKey, + (typeof buckets)[number] + >; + + for (const session of activeHistorySessions) { + const bucketKey = getSessionBucket(sessionLastActivity[session.key] ?? session.updatedAt ?? 0, nowMs); + bucketMap[bucketKey].sessions.push(session); + } + + return buckets; + }, [activeHistorySessions, nowMs, sessionLastActivity, t]); const hiddenRoutes = rendererExtensionRegistry.getHiddenRoutes(); const extraNavItems = rendererExtensionRegistry.getExtraNavItems(); - const coreNavItems = [ - { to: '/models', icon: , label: t('sidebar.models'), testId: 'sidebar-nav-models' }, - { to: '/agents', icon: , label: t('sidebar.agents'), testId: 'sidebar-nav-agents' }, - { to: '/channels', icon: , label: t('sidebar.channels'), testId: 'sidebar-nav-channels' }, - { to: '/skills', icon: , label: t('sidebar.skills'), testId: 'sidebar-nav-skills' }, - { to: '/cron', icon: , label: t('sidebar.cronTasks'), testId: 'sidebar-nav-cron' }, + const openLatestSession = () => { + const latestSession = activeHistorySessions[0] ?? sortedSessions[0]; + if (latestSession) { + switchSession(latestSession.key); + } + navigate('/chat'); + }; + + const navGroups = [ + { + label: t('common:sidebar.quickUse'), + items: [ + { to: '/skills', icon: , label: t('common:sidebar.skills'), testId: 'sidebar-nav-skills' }, + { to: '/cron', icon: , label: t('common:sidebar.cronTasks'), testId: 'sidebar-nav-cron' }, + { to: '/knowledge', icon: , label: t('common:sidebar.knowledge'), testId: 'sidebar-nav-knowledge' }, + ], + }, + ...(isE2EMode ? [{ + label: t('common:sidebar.compatibility'), + items: [ + { to: '/channels', icon: , label: t('sidebar.channels'), testId: 'sidebar-nav-channels' }, + { to: '/agents', icon: , label: t('sidebar.agents'), testId: 'sidebar-nav-agents' }, + { to: '/models', icon: , label: t('sidebar.models'), testId: 'sidebar-nav-models' }, + ], + }] : []), ]; - const navItems = [ - ...coreNavItems.filter((item) => !hiddenRoutes.has(item.to)), - ...extraNavItems.map((item) => ({ - to: item.to, - icon: , - label: item.labelI18nKey ? t(item.labelI18nKey) : item.label, - testId: item.testId, - })), - ]; + const extensionNavItems = extraNavItems.map((item) => ({ + to: item.to, + icon: , + label: item.labelI18nKey ? t(item.labelI18nKey) : item.label, + testId: item.testId, + })).filter((item) => !hiddenRoutes.has(item.to)); return (
@@ -1025,7 +1002,7 @@ export function Cron() {
-

{pausedJobs.length}

+

{pausedJobs.length}

{t('stats.paused')}

@@ -1037,7 +1014,7 @@ export function Cron() {
-

{failedJobs.length}

+

{failedJobs.length}

{t('stats.failed')}

diff --git a/src/pages/Knowledge/index.tsx b/src/pages/Knowledge/index.tsx new file mode 100644 index 0000000..6dbb64b --- /dev/null +++ b/src/pages/Knowledge/index.tsx @@ -0,0 +1,231 @@ +import { useEffect, useMemo, useState } from 'react'; +import { + FileText, + FolderUp, + Search, + ShieldCheck, + UploadCloud, +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; +import { toast } from 'sonner'; +import { invokeIpc, toUserMessage } from '@/lib/api-client'; +import { hostApiFetch } from '@/lib/host-api'; +import { useYinianStore } from '@/stores/yinian'; +import { + YinianEmptyState, + YinianInfoRow, + YinianPageHeader, + YinianPageShell, + YinianPanel, + yinianPrimaryButton, +} from '@/components/yinian/ui'; +import { useTranslation } from 'react-i18next'; + +type KnowledgeFile = { + id: string; + workspaceId: string; + name: string; + mimeType: string; + size: number; + storedPath: string; + textPath?: string; + originalPath?: string; + importedAt: number; + status: 'stored'; +}; + +const knowledgeFileExtensions = [ + 'txt', + 'md', + 'markdown', + 'csv', + 'tsv', + 'json', + 'jsonl', + 'xml', + 'html', + 'htm', + 'yaml', + 'yml', + 'log', + 'ini', + 'conf', + 'css', + 'js', + 'jsx', + 'ts', + 'tsx', + 'py', + 'sql', + 'docx', +]; + +function formatFileSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / 1024 / 1024).toFixed(1)} MB`; +} + +function formatTime(timestamp: number, language: string): string { + return new Intl.DateTimeFormat(language === 'zh' ? 'zh-CN' : 'en-US', { + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }).format(timestamp); +} + +export function Knowledge() { + const { t, i18n } = useTranslation('skills'); + const [files, setFiles] = useState([]); + const [query, setQuery] = useState(''); + const [loading, setLoading] = useState(false); + const config = useYinianStore((state) => state.config); + const workspaceId = config?.hotel.id ?? 'default'; + + const visibleFiles = useMemo(() => { + const normalizedQuery = query.trim().toLowerCase(); + if (!normalizedQuery) return files; + return files.filter((file) => file.name.toLowerCase().includes(normalizedQuery)); + }, [files, query]); + + const refreshFiles = async () => { + const result = await hostApiFetch<{ documents: KnowledgeFile[] }>(`/api/knowledge/files?workspaceId=${encodeURIComponent(workspaceId)}`); + setFiles(result.documents); + }; + + useEffect(() => { + void refreshFiles().catch((error) => { + toast.error(t('knowledge.toast.loadFailed', { message: toUserMessage(error) })); + }); + }, [workspaceId]); + + const handleUpload = async () => { + try { + const dialogResult = await invokeIpc('dialog:open', { + properties: ['openFile', 'multiSelections'], + filters: [{ + name: t('knowledge.fileFilter'), + extensions: knowledgeFileExtensions, + }], + }) as { canceled: boolean; filePaths?: string[] }; + + if (dialogResult.canceled || !dialogResult.filePaths?.length) return; + setLoading(true); + + const result = await hostApiFetch<{ + success: boolean; + documents: KnowledgeFile[]; + rejected: Array<{ filePath: string; reason: string }>; + }>('/api/knowledge/import-paths', { + method: 'POST', + body: JSON.stringify({ + workspaceId, + filePaths: dialogResult.filePaths, + }), + }); + + await refreshFiles(); + + if (result.documents.length > 0) { + toast.success(t('knowledge.toast.saved', { count: result.documents.length })); + } + if (result.rejected.length > 0) { + toast.warning(t('knowledge.toast.rejected', { count: result.rejected.length, reason: result.rejected[0].reason })); + } + } catch (error) { + toast.error(t('knowledge.toast.uploadFailed', { message: toUserMessage(error) })); + } finally { + setLoading(false); + } + }; + + return ( + + +
+

+ {t('knowledge.title')} +

+
+
+ +
+
+ +
+ + file.status === 'stored').length })} icon={ShieldCheck} /> + +
+ + +
+
+

{t('knowledge.filesTitle')}

+

+ {t('knowledge.description')} +

+
+
+ + setQuery(event.target.value)} + placeholder={t('knowledge.searchPlaceholder')} + className="pl-9" + /> +
+
+ + {files.length === 0 ? ( + +
+ +
{t('knowledge.empty.title')}
+
{t('knowledge.empty.description')}
+
+
+ ) : visibleFiles.length === 0 ? ( + +
+ +
{t('knowledge.noResults.title')}
+
{t('knowledge.noResults.description')}
+
+
+ ) : ( +
+ {visibleFiles.map((file) => ( +
+
+ +
+
+
{file.name}
+
+ {formatFileSize(file.size)} + {file.mimeType} + {file.textPath && {t('knowledge.extractedText')}} + {formatTime(file.importedAt, i18n.language)} +
+
+ {t('knowledge.backedUp')} +
+ ))} +
+ )} +
+
+ ); +} diff --git a/src/pages/Models/index.tsx b/src/pages/Models/index.tsx index c171702..b8974c4 100644 --- a/src/pages/Models/index.tsx +++ b/src/pages/Models/index.tsx @@ -278,7 +278,7 @@ export function Models() { {/* Header */}
-

+

{t('dashboard:models.title')}

@@ -295,7 +295,7 @@ export function Models() { {/* Token Usage History Section */}

-

+

{t('dashboard:recentTokenHistory.title', 'Token Usage History')}

diff --git a/src/pages/Settings/index.tsx b/src/pages/Settings/index.tsx index fa4c21a..cbc4582 100644 --- a/src/pages/Settings/index.tsx +++ b/src/pages/Settings/index.tsx @@ -3,7 +3,9 @@ * Application configuration */ import { useEffect, useMemo, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { + BriefcaseBusiness, Sun, Moon, Monitor, @@ -11,13 +13,17 @@ import { ExternalLink, Copy, FileText, + LogOut, + Network, + UserRound, + SlidersHorizontal, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; -import { Separator } from '@/components/ui/separator'; import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { toast } from 'sonner'; import { useSettingsStore } from '@/stores/settings'; import { useGatewayStore } from '@/stores/gateway'; @@ -40,6 +46,8 @@ import { useTranslation } from 'react-i18next'; import { SUPPORTED_LANGUAGES } from '@/i18n'; import { hostApiFetch } from '@/lib/host-api'; import { cn } from '@/lib/utils'; +import { useYinianStore } from '@/stores/yinian'; +import { Channels } from '@/pages/Channels'; type ControlUiInfo = { url: string; token: string; @@ -47,6 +55,7 @@ type ControlUiInfo = { }; export function Settings() { + const navigate = useNavigate(); const { t } = useTranslation('settings'); const { theme, @@ -80,6 +89,13 @@ export function Settings() { } = useSettingsStore(); const { status: gatewayStatus, restart: restartGateway } = useGatewayStore(); + const yinianSession = useYinianStore((state) => state.session); + const yinianConfig = useYinianStore((state) => state.config); + const yinianStatus = useYinianStore((state) => state.status); + const refreshYinianConfig = useYinianStore((state) => state.refreshConfig); + const logoutYinian = useYinianStore((state) => state.logout); + const updateDesktopUserName = useYinianStore((state) => state.updateDesktopUserName); + const updateWorkspaceDisplayName = useYinianStore((state) => state.updateWorkspaceDisplayName); const currentVersion = useUpdateStore((state) => state.currentVersion); const updateSetAutoDownload = useUpdateStore((state) => state.setAutoDownload); const [controlUiInfo, setControlUiInfo] = useState(null); @@ -95,6 +111,10 @@ export function Settings() { const [wsDiagnosticEnabled, setWsDiagnosticEnabled] = useState(false); const [showTelemetryViewer, setShowTelemetryViewer] = useState(false); const [telemetryEntries, setTelemetryEntries] = useState([]); + const [adminPasswordInput, setAdminPasswordInput] = useState(''); + const [adminPasswordError, setAdminPasswordError] = useState(null); + const [desktopUserNameDraft, setDesktopUserNameDraft] = useState(''); + const [workspaceDisplayNameDraft, setWorkspaceDisplayNameDraft] = useState(''); const isWindows = window.electron.platform === 'win32'; const showCliTools = true; @@ -114,13 +134,55 @@ export function Settings() { error?: string; } | null>(null); + useEffect(() => { + setDesktopUserNameDraft(yinianConfig?.user.name ?? ''); + }, [yinianConfig?.user.name]); + + useEffect(() => { + setWorkspaceDisplayNameDraft(yinianConfig?.hotel.name ?? ''); + }, [yinianConfig?.hotel.name]); + + const handleRefreshService = async () => { + try { + await refreshYinianConfig(); + toast.success(t('account.toast.configSynced')); + } catch (error) { + toast.error(t('account.toast.syncFailed', { message: toUserMessage(error) })); + } + }; + + const handleLogout = async () => { + await logoutYinian(); + navigate('/login', { replace: true }); + }; + + const handleSaveDesktopUserName = () => { + const nextName = desktopUserNameDraft.trim(); + if (!nextName) { + toast.error(t('account.toast.userNameRequired')); + return; + } + updateDesktopUserName(nextName); + toast.success(t('account.toast.userNameSaved')); + }; + + const handleSaveWorkspaceDisplayName = () => { + const nextName = workspaceDisplayNameDraft.trim(); + if (!nextName) { + toast.error(t('account.toast.workspaceNameRequired')); + return; + } + updateWorkspaceDisplayName(nextName); + toast.success(t('account.toast.workspaceNameSaved')); + }; + const handleShowLogs = async () => { try { const logs = await hostApiFetch<{ content: string }>('/api/logs?tailLines=100'); setLogContent(logs.content); setShowLogs(true); } catch { - setLogContent('(Failed to load logs)'); + setLogContent(t('gateway.logLoadFailed')); setShowLogs(true); } }; @@ -169,7 +231,7 @@ export function Settings() { exitCode: null, stdout: '', stderr: '', - command: 'openclaw doctor', + command: 'agent doctor', cwd: '', durationMs: 0, error: message, @@ -198,7 +260,7 @@ export function Settings() { await navigator.clipboard.writeText(payload); toast.success(t('developer.doctorCopied')); } catch (error) { - toast.error(`Failed to copy doctor output: ${String(error)}`); + toast.error(t('developer.copyDoctorFailed', { message: String(error) })); } }; @@ -226,7 +288,7 @@ export function Settings() { await navigator.clipboard.writeText(controlUiInfo.token); toast.success(t('developer.tokenCopied')); } catch (error) { - toast.error(`Failed to copy token: ${String(error)}`); + toast.error(t('developer.copyTokenFailed', { message: String(error) })); } }; @@ -247,7 +309,7 @@ export function Settings() { setOpenclawCliError(null); } else { setOpenclawCliCommand(''); - setOpenclawCliError(result.error || 'OpenClaw CLI unavailable'); + setOpenclawCliError(result.error || t('developer.cliUnavailable')); } } catch (error) { if (cancelled) return; @@ -265,7 +327,7 @@ export function Settings() { await navigator.clipboard.writeText(openclawCliCommand); toast.success(t('developer.cmdCopied')); } catch (error) { - toast.error(`Failed to copy command: ${String(error)}`); + toast.error(t('developer.copyCommandFailed', { message: String(error) })); } }; @@ -274,7 +336,7 @@ export function Settings() { 'openclaw:cli-installed', (...args: unknown[]) => { const installedPath = typeof args[0] === 'string' ? args[0] : ''; - toast.success(`openclaw CLI installed at ${installedPath}`); + toast.success(t('developer.cliInstalled', { path: installedPath })); }, ); return () => { unsubscribe?.(); }; @@ -471,28 +533,203 @@ export function Settings() { ); }; + const handleUnlockAdvancedMode = () => { + if (adminPasswordInput.trim() !== 'admin') { + setAdminPasswordError(t('advanced.adminPasswordInvalid')); + return; + } + + setDevModeUnlocked(true); + setAdminPasswordInput(''); + setAdminPasswordError(null); + toast.success(t('advanced.devModeUnlocked')); + }; + + const handleLockAdvancedMode = () => { + setDevModeUnlocked(false); + setAdminPasswordInput(''); + setAdminPasswordError(null); + setShowTelemetryViewer(false); + toast.success(t('advanced.devModeLocked')); + }; + return (
-
+
+ + + + + {t('tabs.account')} + + + + {t('tabs.channels')} + + + + {t('tabs.preferences')} + + + + {t('tabs.runtime')} + + - {/* Header */} -
-
-

- {t('title')} -

-

- {t('subtitle')} -

-
-
+
+ - {/* Content Area */} -
+ {/* Account and service */} + {yinianSession.authenticated && yinianConfig && ( + <> +
+

+ {t('account.title')} +

+
+
+
+
+
+
{t('account.currentWorkspace')}
+
+ {yinianConfig.hotel.name} +
+
+
+
+ + {yinianConfig.user.name} +
+
+
+
+
+ + setWorkspaceDisplayNameDraft(event.target.value)} + placeholder={t('account.workspaceNamePlaceholder')} + className="h-10 rounded-lg border-slate-200 bg-white dark:border-white/10 dark:bg-slate-950" + /> +
+ +
+ +
+
+ + setDesktopUserNameDraft(event.target.value)} + placeholder={t('account.desktopUserNamePlaceholder')} + className="h-10 rounded-lg border-slate-200 bg-white dark:border-white/10 dark:bg-slate-950" + /> +
+ +
+

+ {t('account.localNameHelp')} +

+
+ +
+ + +
+
+ +
+

{t('updates.title')}

+
+ +
+
+ +

+ {t('updates.autoCheckDesc')} +

+
+ +
+ +
+
+ +

+ {t('updates.autoDownloadDesc')} +

+
+ { + setAutoDownloadUpdate(value); + updateSetAutoDownload(value); + }} + /> +
+
+
+ {t('about.appName')} + · {t('about.version', { version: currentVersion })} +
+
+
+
+ + )} + + + + + + + + {/* Appearance */}
-

+

{t('appearance.title')}

@@ -555,11 +792,12 @@ export function Settings() {
- +
+ {/* Gateway */}
-

+

{t('gateway.title')}

@@ -628,51 +866,88 @@ export function Settings() {
-
-
- -

- {t('advanced.devModeDesc')} -

+
+
+
+ +

+ {t('advanced.devModeDesc')} +

+
+ {devModeUnlocked ? ( + + ) : ( +
+
+ { + setAdminPasswordInput(event.target.value); + setAdminPasswordError(null); + }} + onKeyDown={(event) => { + if (event.key === 'Enter') { + handleUnlockAdvancedMode(); + } + }} + placeholder={t('advanced.adminPasswordPlaceholder')} + className="h-9 rounded-lg bg-slate-50 dark:bg-white/5" + /> + +
+ {adminPasswordError && ( +

{adminPasswordError}

+ )} +
+ )}
-
-
-
- -

- {t('advanced.telemetryDesc')} -

+ {devModeUnlocked && ( +
+
+ +

+ {t('advanced.telemetryDesc')} +

+
+
- -
+ )}
- {/* Developer */} {devModeUnlocked && ( - <> -
-

+

{t('developer.title')}

- {/* Gateway Proxy */} + {/* Agent Proxy */}
- +

{t('gateway.proxyDesc')}

@@ -1030,90 +1305,12 @@ export function Settings() {
- )} - + - {/* Updates */} -
-

- {t('updates.title')} -

-
- - -
-
- -

- {t('updates.autoCheckDesc')} -

-
- -
- -
-
- -

- {t('updates.autoDownloadDesc')} -

-
- { - setAutoDownloadUpdate(value); - updateSetAutoDownload(value); - }} - /> -
-
- - - - {/* About */} -
-

- {t('about.title')} -

-
-

- {t('about.appName')} - {t('about.tagline')} -

-

{t('about.basedOn')}

-

{t('about.version', { version: currentVersion })}

-
- - - -
-
-
- -
+
); diff --git a/src/pages/Setup/index.tsx b/src/pages/Setup/index.tsx index 9c7ea51..5762f76 100644 --- a/src/pages/Setup/index.tsx +++ b/src/pages/Setup/index.tsx @@ -80,7 +80,7 @@ const getDefaultSkills = (t: TFunction): DefaultSkill[] => [ { id: 'terminal', name: t('defaultSkills.terminal.name'), description: t('defaultSkills.terminal.description') }, ]; -import clawxIcon from '@/assets/logo.svg'; +import zhinianLogo from '@/assets/logo.svg'; // NOTE: Channel types moved to Settings > Channels page // NOTE: Skill bundles moved to Settings > Skills page - auto-install essential skills during setup @@ -268,7 +268,7 @@ function WelcomeContent() { return (
- ClawX + 智念助手

{t('welcome.title')}

diff --git a/src/pages/Skills/index.tsx b/src/pages/Skills/index.tsx index dfe13b6..b09c5a0 100644 --- a/src/pages/Skills/index.tsx +++ b/src/pages/Skills/index.tsx @@ -399,7 +399,7 @@ function SkillDetailDialog({ skill, isOpen, onClose, onToggle, onUninstall, onOp ); } -export function Skills() { +export function ClawSkills() { const { skills, loading, @@ -962,4 +962,4 @@ export function Skills() { ); } -export default Skills; +export default ClawSkills; diff --git a/src/pages/Today/index.tsx b/src/pages/Today/index.tsx new file mode 100644 index 0000000..4782aa6 --- /dev/null +++ b/src/pages/Today/index.tsx @@ -0,0 +1,512 @@ +import { useEffect, useState } from 'react'; +import { + ArrowRight, + BookOpen, + Clock3, + Layers3, + MessageSquarePlus, + RefreshCw, +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { useYinianStore } from '@/stores/yinian'; +import { useChannelsStore } from '@/stores/channels'; +import { useCronStore } from '@/stores/cron'; +import { useSkillsStore } from '@/stores/skills'; +import { hostApiFetch } from '@/lib/host-api'; +import { cn } from '@/lib/utils'; +import { + YinianHeaderActions, + YinianPageHeader, + YinianPageShell, + YinianPanel, +} from '@/components/yinian/ui'; +import { CHANNEL_NAMES } from '@/types/channel'; +import type { Channel } from '@/types/channel'; +import type { CronJob } from '@/types/cron'; +import { + getEffectiveChannelAccountName, + subscribeYinianLocalPrefs, +} from '@/lib/yinian-local-prefs'; +import { useTranslation } from 'react-i18next'; + +type KnowledgeFilesResponse = { + documents?: Array<{ id: string; name: string; status?: string; importedAt?: number }>; +}; + +type KnowledgeDocument = NonNullable[number]; + +type ChannelAccountStatus = 'connected' | 'connecting' | 'degraded' | 'disconnected' | 'error'; + +type ChannelAccountItem = { + accountId: string; + name: string; + configured: boolean; + status: ChannelAccountStatus; + isDefault?: boolean; +}; + +type ChannelGroupItem = { + channelType: string; + status: ChannelAccountStatus; + accounts: ChannelAccountItem[]; +}; + +type ChannelAccountsResponse = { + success: boolean; + channels?: ChannelGroupItem[]; + error?: string; +}; + +function navigateTo(path: string): void { + window.location.hash = `#${path}`; +} + +function getChannelTypeLabel(type: string): string { + if (type === 'agentbus') return 'AgentBus'; + return CHANNEL_NAMES[type as keyof typeof CHANNEL_NAMES] ?? type; +} + +function getChannelAccountLabel(channel: Channel, defaultAccountLabel: string): string { + const channelName = channel.name?.trim(); + const typeLabel = getChannelTypeLabel(channel.type); + if (channelName && channelName !== typeLabel) return channelName; + if (channel.accountId && channel.accountId !== 'default') return channel.accountId; + return defaultAccountLabel; +} + +function formatDashboardTime(value: string | number | undefined, language: string, fallback: string): string { + if (!value) return fallback; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return fallback; + return new Intl.DateTimeFormat(language === 'zh' ? 'zh-CN' : 'en-US', { + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: false, + }).format(date); +} + +function isTodayRun(job: CronJob, now = new Date()): boolean { + if (!job.lastRun?.time) return false; + const runAt = new Date(job.lastRun.time); + if (Number.isNaN(runAt.getTime())) return false; + return runAt.getFullYear() === now.getFullYear() + && runAt.getMonth() === now.getMonth() + && runAt.getDate() === now.getDate(); +} + +function EmptyDashboardText({ children }: { children: React.ReactNode }) { + return ( +

+ {children} +
+ ); +} + +function DashboardCount({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
{value}
+
+ ); +} + +function ChannelGroupCard({ + group, + accountUnit, + defaultAccountLabel, +}: { + group: ChannelGroupItem; + accountUnit: string; + defaultAccountLabel: string; +}) { + return ( +
+
+
{getChannelTypeLabel(group.channelType)}
+ + {group.accounts.length} {accountUnit} + +
+
+ {group.accounts.map((account) => ( +
+
+ {getEffectiveChannelAccountName( + group.channelType, + account.accountId, + account.name && account.name !== account.accountId ? account.name : account.accountId || defaultAccountLabel, + )} +
+ +
+ ))} +
+
+ ); +} + +function AppShelf({ + title, + count, + apps, + emptyText, + countUnit, + moreLabel, +}: { + title: string; + count: number; + apps: Array<{ id: string; name: string }>; + emptyText: string; + countUnit: string; + moreLabel: (count: number) => string; +}) { + const hiddenCount = Math.max(0, count - apps.length); + + return ( +
+
+
{title}
+
{count} {countUnit}
+
+
+ {apps.length === 0 ? ( +
{emptyText}
+ ) : apps.map((app, index) => ( +
+ + {index + 1} + + {app.name} +
+ ))} + {hiddenCount > 0 && ( +
{moreLabel(hiddenCount)}
+ )} +
+
+ ); +} + +function TaskRunRow({ name, time, success }: { name: string; time: string; success?: boolean }) { + return ( +
+ + {name} + {time} +
+ ); +} + +function KnowledgeFileRow({ name, index }: { name: string; index: number }) { + return ( +
+ + {index + 1} + + {name} +
+ ); +} + +function DashboardModule({ + icon: Icon, + actionLabel, + actionPath, + actionDetail, + children, +}: { + icon: React.ComponentType<{ className?: string }>; + actionLabel: string; + actionPath: string; + actionDetail: string; + children: React.ReactNode; +}) { + return ( + + +
+ {children} +
+
+ ); +} + +export function Today() { + const { t, i18n } = useTranslation('dashboard'); + const status = useYinianStore((state) => state.status); + const config = useYinianStore((state) => state.config); + const refreshConfig = useYinianStore((state) => state.refreshConfig); + const configError = useYinianStore((state) => state.error); + const channels = useChannelsStore((state) => state.channels); + const fetchChannels = useChannelsStore((state) => state.fetchChannels); + const cronJobs = useCronStore((state) => state.jobs); + const fetchJobs = useCronStore((state) => state.fetchJobs); + const openClawSkills = useSkillsStore((state) => state.skills); + const fetchOpenClawSkills = useSkillsStore((state) => state.fetchSkills); + const [knowledgeDocuments, setKnowledgeDocuments] = useState([]); + const [channelGroups, setChannelGroups] = useState([]); + const [, setLocalPrefsVersion] = useState(0); + + const workspaceId = config?.hotel.id ?? 'default'; + + useEffect(() => subscribeYinianLocalPrefs(() => { + setLocalPrefsVersion((version) => version + 1); + }), []); + + useEffect(() => { + void fetchChannels(); + void fetchJobs(); + void fetchOpenClawSkills(); + }, [fetchChannels, fetchJobs, fetchOpenClawSkills]); + + useEffect(() => { + let active = true; + hostApiFetch('/api/channels/accounts') + .then((result) => { + if (!active) return; + setChannelGroups(result.success && Array.isArray(result.channels) ? result.channels : []); + }) + .catch(() => { + if (active) setChannelGroups([]); + }); + return () => { + active = false; + }; + }, []); + + useEffect(() => { + let active = true; + hostApiFetch(`/api/knowledge/files?workspaceId=${encodeURIComponent(workspaceId)}`) + .then((result) => { + if (active) setKnowledgeDocuments(Array.isArray(result.documents) ? result.documents : []); + }) + .catch(() => { + if (active) setKnowledgeDocuments([]); + }); + return () => { + active = false; + }; + }, [workspaceId]); + + if (!config) { + return ( +
+ {status === 'error' ? (configError ?? t('overview.loadFailed')) : t('overview.loading')} +
+ ); + } + + const enabledEntitlements = config.entitlements.filter((skill) => skill.enabled); + const channelAccountItems = channelGroups.flatMap((group) => ( + group.accounts.map((account) => ({ + channelType: group.channelType, + accountId: account.accountId, + name: account.name, + status: account.status, + })) + )); + const channelAccounts = channelAccountItems.length > 0 + ? channelAccountItems + : channels.map((channel) => ({ + channelType: channel.type, + accountId: channel.accountId ?? 'default', + name: getChannelAccountLabel(channel, t('overview.defaultAccount')), + status: channel.status, + })); + const displayChannelGroups = channelGroups.length > 0 + ? channelGroups + : Object.values(channelAccounts.reduce>((groups, account) => { + const group = groups[account.channelType] ?? { + channelType: account.channelType, + status: account.status, + accounts: [], + }; + group.accounts.push({ + accountId: account.accountId, + name: account.name, + configured: true, + status: account.status, + }); + groups[account.channelType] = group; + return groups; + }, {})); + const enabledCronJobs = cronJobs.filter((job) => job.enabled); + const todayRuns = cronJobs.filter((job) => isTodayRun(job)); + const visibleOpenClawSkills = openClawSkills.filter((skill) => !skill.isCore); + const recentIssuedApps = [...enabledEntitlements] + .sort((a, b) => (b.lastRunAt ?? 0) - (a.lastRunAt ?? 0)) + .slice(0, 5); + const recentLocalApps = [...visibleOpenClawSkills] + .slice(0, 5); + const recentCronRuns = cronJobs + .filter((job) => job.lastRun?.time) + .sort((a, b) => new Date(b.lastRun?.time ?? 0).getTime() - new Date(a.lastRun?.time ?? 0).getTime()) + .slice(0, 5); + const recentKnowledgeDocuments = [...knowledgeDocuments] + .sort((a, b) => (b.importedAt ?? 0) - (a.importedAt ?? 0)) + .slice(0, 10); + const cronRunCount = cronJobs.filter((job) => job.lastRun?.time).length; + const hiddenCronRunCount = Math.max(0, cronRunCount - recentCronRuns.length); + const hiddenKnowledgeCount = Math.max(0, knowledgeDocuments.length - recentKnowledgeDocuments.length); + + return ( + + +
+

+ {config.hotel.name} +

+

+ {config.user.name} +

+
+ + + +
+ +
+ +
+ +
+ {displayChannelGroups.length === 0 ? ( + {t('overview.chat.empty')} + ) : ( + displayChannelGroups.map((group) => ( + + )) + )} +
+
+
+ + +
+ ({ id: skill.skillId, name: skill.name }))} + emptyText={t('overview.apps.emptyIssued')} + countUnit={t('overview.itemUnit')} + moreLabel={(count) => t('overview.moreItems', { count })} + /> + ({ id: skill.id, name: skill.name || skill.id }))} + emptyText={t('overview.apps.emptyLocal')} + countUnit={t('overview.itemUnit')} + moreLabel={(count) => t('overview.moreItems', { count })} + /> +
+
+ + +
+
+ + + +
+ {recentCronRuns.length === 0 ? ( + {t('overview.tasks.emptyRuns')} + ) : ( +
+
+ {recentCronRuns.map((job) => ( + + ))} + {hiddenCronRunCount > 0 && ( +
{t('overview.moreRecords', { count: hiddenCronRunCount })}
+ )} +
+
+ )} +
+
+ + +
+
+
{t('overview.knowledge.total')}
+
{t('overview.count', { count: knowledgeDocuments.length })}
+
+ {recentKnowledgeDocuments.length === 0 ? ( + {t('overview.knowledge.empty')} + ) : ( +
+ {recentKnowledgeDocuments.map((document, index) => ( + + ))} + {hiddenKnowledgeCount > 0 && ( +
{t('overview.moreRecords', { count: hiddenKnowledgeCount })}
+ )} +
+ )} +
+
+
+
+ ); +} + +export default Today; diff --git a/src/pages/YinianLogin/index.tsx b/src/pages/YinianLogin/index.tsx new file mode 100644 index 0000000..762348f --- /dev/null +++ b/src/pages/YinianLogin/index.tsx @@ -0,0 +1,226 @@ +import { FormEvent, useEffect, useMemo, useState } from 'react'; +import { Loader2, LockKeyhole, RefreshCw } from 'lucide-react'; +import { useNavigate } from 'react-router-dom'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useYinianStore } from '@/stores/yinian'; +import { YinianPanel, yinianPrimaryButton } from '@/components/yinian/ui'; +import logoSvg from '@/assets/logo.svg'; +import type { YinianImageCaptcha } from '../../../shared/yinian'; +import type { YinianServerStatus } from '../../../shared/yinian'; +import { useTranslation } from 'react-i18next'; + +export function YinianLogin() { + const navigate = useNavigate(); + const { t } = useTranslation('setup'); + const status = useYinianStore((state) => state.status); + const error = useYinianStore((state) => state.error); + const loginWithPassword = useYinianStore((state) => state.loginWithPassword); + + const [account, setAccount] = useState(''); + const [password, setPassword] = useState(''); + const [rememberPassword, setRememberPassword] = useState(false); + const [captchaCode, setCaptchaCode] = useState(''); + const [captcha, setCaptcha] = useState(null); + const [captchaLoading, setCaptchaLoading] = useState(false); + const [serverStatus, setServerStatus] = useState(null); + const isLoading = status === 'loading'; + const serverConfigured = Boolean(serverStatus?.apiBaseUrl) || serverStatus?.mode === 'mock'; + const captchaSrc = useMemo(() => { + if (!captcha) return ''; + if (captcha.image?.startsWith('data:')) return captcha.image; + if (captcha.image) return captcha.image; + if (captcha.imageBase64) { + return `data:${captcha.mimeType || 'image/png'};base64,${captcha.imageBase64}`; + } + return ''; + }, [captcha]); + const captchaRequired = Boolean(captchaSrc); + + const refreshCaptcha = async () => { + setCaptchaLoading(true); + try { + const randomStr = crypto.randomUUID(); + const next = await window.yinian.auth.createImageCaptcha(randomStr); + setCaptcha(next); + setCaptchaCode(''); + } catch { + setCaptcha(null); + } finally { + setCaptchaLoading(false); + } + }; + + useEffect(() => { + void window.yinian.app.getServerStatus() + .then(setServerStatus) + .catch(() => setServerStatus(null)); + void refreshCaptcha(); + void window.yinian.auth.getSavedCredentials() + .then((credentials) => { + if (!credentials) return; + setAccount(credentials.account); + setPassword(credentials.password ?? ''); + setRememberPassword(credentials.rememberPassword); + }) + .catch(() => undefined); + }, []); + + const handlePasswordLogin = async (event: FormEvent) => { + event.preventDefault(); + await loginWithPassword({ + account: account.trim(), + password, + captchaCode: captchaCode.trim(), + randomStr: captcha?.randomStr, + }); + if (rememberPassword) { + void window.yinian.auth.saveCredentials({ + account: account.trim(), + password, + rememberPassword: true, + }).catch(() => undefined); + } else { + void window.yinian.auth.clearSavedCredentials().catch(() => undefined); + } + navigate('/today', { replace: true }); + }; + + return ( +
+
+
+ + {t('login.appName')} + +

{t('login.slogan')}

+
+
+ +
+ +
+
+ {t('login.appName')} + {t('login.appName')} +
+

{t('login.title')}

+

{t('login.slogan')}

+ {serverStatus && ( +
+ {serverConfigured + ? t('login.serverEnabled', { baseUrl: serverStatus.apiBaseUrl ? `: ${serverStatus.apiBaseUrl}` : '' }) + : serverStatus.message} +
+ )} +
+
+
+
+ + setAccount(event.target.value)} + placeholder={t('login.accountPlaceholder')} + autoComplete="username" + /> +
+
+ + setPassword(event.target.value)} + placeholder={t('login.passwordPlaceholder')} + autoComplete="current-password" + /> +
+ + + {error &&

{error}

} + + +
+
+
+
+ ); +} + +function CaptchaField({ + value, + onChange, + imageSrc, + loading, + onRefresh, + label, + placeholder, + refreshTitle, +}: { + value: string; + onChange: (value: string) => void; + imageSrc: string; + loading: boolean; + onRefresh: () => void | Promise; + label: string; + placeholder: string; + refreshTitle: string; +}) { + if (!imageSrc) return null; + + return ( +
+ +
+ onChange(event.target.value)} + placeholder={placeholder} + /> + +
+
+ ); +} + +export default YinianLogin; diff --git a/src/pages/YinianSkills/index.tsx b/src/pages/YinianSkills/index.tsx new file mode 100644 index 0000000..81a3478 --- /dev/null +++ b/src/pages/YinianSkills/index.tsx @@ -0,0 +1,388 @@ +import { useEffect, useState } from 'react'; +import { + AlertCircle, + ChevronRight, + CheckCircle2, + CloudDownload, + Hash, + HardDrive, + History, + Play, + RefreshCw, + ShieldCheck, +} from 'lucide-react'; +import { toast } from 'sonner'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet'; +import { useYinianStore } from '@/stores/yinian'; +import { useYinianSkillsStore } from '@/stores/yinian-skills'; +import { useSkillsStore } from '@/stores/skills'; +import { cn } from '@/lib/utils'; +import { + YinianEmptyState, + YinianHeaderActions, + YinianInfoRow, + YinianMetricCard, + YinianNotice, + YinianPageHeader, + YinianPageShell, + YinianPanel, + yinianAccent, + yinianPrimaryButton, +} from '@/components/yinian/ui'; +import type { YinianLocalSkill, YinianSkillEntitlement } from '../../../shared/yinian'; +import type { Skill } from '@/types/skill'; +import { useTranslation } from 'react-i18next'; + +type SkillManagerState = + | 'disabled' + | 'not-synced' + | 'installed' + | 'updated' + | 'skipped' + | 'update-available' + | 'failed'; + +function formatTime(timestamp: number | undefined, language: string, fallback: string): string { + if (!timestamp) return fallback; + return new Intl.DateTimeFormat(language === 'zh' ? 'zh-CN' : 'en-US', { + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }).format(timestamp); +} + +function getManagerState( + entitlement: YinianSkillEntitlement, + local: YinianLocalSkill | undefined, +): SkillManagerState { + if (!entitlement.enabled) return 'disabled'; + if (!local) return 'not-synced'; + if (local.status === 'failed') return 'failed'; + if (!local.enabled || local.status === 'disabled') return 'disabled'; + if (local.version !== entitlement.version) return 'update-available'; + return local.status; +} + +function getBadgeVariant(state: SkillManagerState): 'success' | 'warning' | 'destructive' | 'secondary' | 'outline' { + if (state === 'installed' || state === 'updated' || state === 'skipped') return 'success'; + if (state === 'failed') return 'destructive'; + if (state === 'disabled') return 'secondary'; + if (state === 'update-available') return 'warning'; + return 'outline'; +} + +function canRun(state: SkillManagerState): boolean { + return state === 'installed' || state === 'updated' || state === 'skipped'; +} + +function getLocalSkillSourceLabel(skill: Skill, t: (key: string, options?: Record) => string): string { + if (skill.isCore) return t('business.localSource.core'); + if (skill.isBundled) return t('business.localSource.bundled'); + if (skill.source === 'clawx-preinstalled') return t('business.localSource.preinstalled'); + if (skill.source === 'openclaw-managed') return t('business.localSource.local'); + if (skill.source === 'agents-skills-personal') return t('business.localSource.personal'); + if (skill.source === 'agents-skills-project') return t('business.localSource.project'); + return skill.source || t('business.localSource.local'); +} + +export function YinianSkills() { + const { t, i18n } = useTranslation('skills'); + const [activeTab, setActiveTab] = useState<'server' | 'local'>('server'); + const [selectedServerSkillId, setSelectedServerSkillId] = useState(null); + const [selectedLocalSkillId, setSelectedLocalSkillId] = useState(null); + const config = useYinianStore((state) => state.config); + const localSkills = useYinianSkillsStore((state) => state.localSkills); + const lastSync = useYinianSkillsStore((state) => state.lastSync); + const loading = useYinianSkillsStore((state) => state.loading); + const error = useYinianSkillsStore((state) => state.error); + const getRegistry = useYinianSkillsStore((state) => state.getRegistry); + const sync = useYinianSkillsStore((state) => state.sync); + const openClawSkills = useSkillsStore((state) => state.skills); + const openClawSkillsLoading = useSkillsStore((state) => state.loading); + const fetchOpenClawSkills = useSkillsStore((state) => state.fetchSkills); + + useEffect(() => { + if (config?.hotel.id) { + void getRegistry(config.hotel.id); + } + }, [config?.hotel.id, getRegistry]); + + useEffect(() => { + void fetchOpenClawSkills(); + }, [fetchOpenClawSkills]); + + if (!config) { + return ( +
+ {t('business.loading')} +
+ ); + } + + const localById = new Map(localSkills.map((skill) => [skill.skillId, skill])); + const visibleOpenClawSkills = openClawSkills + .filter((skill) => !skill.isCore) + .sort((a, b) => a.name.localeCompare(b.name, i18n.language === 'zh' ? 'zh-CN' : 'en-US')); + const states = config.entitlements.map((skill) => getManagerState(skill, localById.get(skill.skillId))); + const runnableCount = states.filter(canRun).length; + const notSyncedCount = states.filter((state) => state === 'not-synced').length; + const updateCount = states.filter((state) => state === 'update-available').length; + const failedCount = states.filter((state) => state === 'failed').length; + const disabledCount = states.filter((state) => state === 'disabled').length; + const selectedServerSkill = config.entitlements.find((skill) => skill.skillId === selectedServerSkillId); + const selectedServerLocal = selectedServerSkill ? localById.get(selectedServerSkill.skillId) : undefined; + const selectedServerState = selectedServerSkill ? getManagerState(selectedServerSkill, selectedServerLocal) : null; + const selectedLocalSkill = visibleOpenClawSkills.find((skill) => skill.id === selectedLocalSkillId); + + const handleSync = async () => { + try { + await sync(); + toast.success(t('business.toast.synced')); + } catch { + toast.error(t('business.toast.syncFailed')); + } + }; + + return ( + + +
+

{t('business.title')}

+
+ +
+
{t('business.header.readyServer', { count: localSkills.length })}
+
{t('business.header.localInstalled', { count: visibleOpenClawSkills.length })}
+
{t('business.header.lastSync', { time: formatTime(lastSync?.syncedAt, i18n.language, t('business.neverSynced')) })}
+
+ +
+
+ + {error && ( + +
+ + {error} +
+ +
+ )} + +
+ {[ + { label: t('business.metrics.enabled'), value: String(config.entitlements.length), icon: ShieldCheck, tone: yinianAccent }, + { label: t('business.metrics.runnable'), value: String(runnableCount), icon: CheckCircle2, tone: 'text-emerald-600' }, + { label: t('business.metrics.notSynced'), value: String(notSyncedCount), icon: CloudDownload, tone: 'text-slate-600 dark:text-slate-300' }, + { label: t('business.metrics.updates'), value: String(updateCount), icon: RefreshCw, tone: 'text-amber-600' }, + { label: t('business.metrics.issues'), value: String(failedCount + disabledCount), icon: AlertCircle, tone: failedCount ? 'text-red-600' : 'text-slate-500' }, + ].map((item) => ( + + ))} +
+ +
+ {[ + { key: 'server' as const, label: t('business.tabs.server'), count: config.entitlements.length }, + { key: 'local' as const, label: t('business.tabs.local'), count: visibleOpenClawSkills.length }, + ].map((tab) => ( + + ))} +
+ + {activeTab === 'server' && ( + config.entitlements.length === 0 ? ( + + {t('business.empty.server')} + + ) : ( + +
+ {t('business.columns.app')} + {t('business.columns.category')} + {t('business.columns.cloudVersion')} + {t('business.columns.status')} + +
+
+ {config.entitlements.map((skill) => { + const local = localById.get(skill.skillId); + const state = getManagerState(skill, local); + + return ( + + ); + })} +
+
+ ) + )} + + {activeTab === 'local' && ( + visibleOpenClawSkills.length === 0 ? ( + + {openClawSkillsLoading ? t('business.empty.localLoading') : t('business.empty.local')} + + ) : ( + +
+ {t('business.columns.app')} + {t('business.columns.version')} + {t('business.columns.source')} + {t('business.columns.status')} + +
+
+ {visibleOpenClawSkills.map((skill) => ( + + ))} +
+
+ ) + )} + + !open && setSelectedServerSkillId(null)}> + + {selectedServerSkill && selectedServerState && ( +
+ + {selectedServerSkill.name} + + {t(`business.category.${selectedServerSkill.category}`)} · {t('business.cloudVersion', { version: selectedServerSkill.version })} + + +
+ + {selectedServerSkill.enabled ? t('business.entitlement.enabled') : t('business.entitlement.disabled')} + + {t(`business.state.${selectedServerState}`)} +
+

{t(`business.stateDescription.${selectedServerState}`)}

+
+ {selectedServerSkill.triggers.length === 0 ? ( + {t('business.noTriggers')} + ) : selectedServerSkill.triggers.map((trigger) => ( + {t(`business.trigger.${trigger}`)} + ))} +
+
+ + + + + + {selectedServerLocal?.error && ( +
+ {selectedServerLocal.error} +
+ )} +
+
+ + +
+
+ )} +
+
+ + !open && setSelectedLocalSkillId(null)}> + + {selectedLocalSkill && ( +
+ + {selectedLocalSkill.name} + + {t('business.localVersionLine', { version: selectedLocalSkill.version ?? 'unknown', source: getLocalSkillSourceLabel(selectedLocalSkill, t) })} + + + + {selectedLocalSkill.enabled ? t('business.availability.available') : t('business.availability.disabled')} + +

+ {selectedLocalSkill.description || t('business.localLongDescription')} +

+
+ + + + + +
+
+ )} +
+
+ + {activeTab === 'server' && localSkills.length === 0 && config.entitlements.length > 0 && ( + + {t('business.emptyRegistry')} + + )} +
+ ); +} + +export default YinianSkills; diff --git a/src/stores/chat.ts b/src/stores/chat.ts index 3c6e0a0..793d997 100644 --- a/src/stores/chat.ts +++ b/src/stores/chat.ts @@ -68,6 +68,11 @@ const HISTORY_POLL_SILENCE_WINDOW_MS = 2_500; const CHAT_EVENT_DEDUPE_TTL_MS = 30_000; const _chatEventDedupe = new Map(); +type ChannelAccountAliasesResponse = { + success: boolean; + aliases?: Array<{ accountId: string; userId: string }>; +}; + function clearErrorRecoveryTimer(): void { if (_errorRecoveryTimer) { clearTimeout(_errorRecoveryTimer); @@ -82,6 +87,27 @@ function clearHistoryPoll(): void { } } +function normalizeSessionChannel(channel: string | undefined): string { + const normalized = (channel ?? '').trim().toLowerCase(); + if (normalized === 'openclaw-weixin') return 'wechat'; + if (normalized === 'lark') return 'feishu'; + return normalized; +} + +async function loadWeChatAccountAliasByUserId(): Promise> { + try { + const result = await hostApiFetch('/api/channels/account-aliases?channelType=wechat'); + if (!result.success || !Array.isArray(result.aliases)) return new Map(); + return new Map( + result.aliases + .map((alias) => [alias.userId.trim().toLowerCase(), alias.accountId.trim()] as const) + .filter(([userId, accountId]) => Boolean(userId && accountId)), + ); + } catch { + return new Map(); + } +} + function forceNextHistoryLoad(sessionKey: string): void { _forceNextHistoryLoadBySession.add(sessionKey); } @@ -255,7 +281,9 @@ function stripGatewayUserMetadata(text: string): string { .replace(/\s*\[media attached:[^\]]*\]/g, '') .replace(/\s*\[message_id:\s*[^\]]+\]/g, '') .replace(/^Conversation info\s*\([^)]*\):\s*```[a-z]*\n[\s\S]*?```\s*/i, '') - .replace(/^Conversation info\s*\([^)]*\):\s*\{[\s\S]*?\}\s*/i, ''); + .replace(/^Conversation info\s*\([^)]*\):\s*\{[\s\S]*?\}\s*/i, '') + .replace(/^\s*System\s*:\s*\[\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(?::\d{2})?\s+GMT[+-]\d+\]\s+[A-Za-z][\w -]*(?:\[[^\]]+\])?\s+(?:DM|Group|Room|Chat|Message)\s+\|\s+\S+(?:\s+\[msg:[^\]]+\])?\s*/i, '') + .trim(); } function normalizeComparableUserText(content: unknown): string { @@ -264,6 +292,10 @@ function normalizeComparableUserText(content: unknown): string { .trim(); } +function getUserDisplayText(content: unknown): string { + return stripGatewayUserMetadata(getMessageText(content)).trim(); +} + function getComparableAttachmentSignature(message: Pick): string { const files = (message._attachedFiles || []) .map((file) => file.filePath || `${file.fileName}|${file.mimeType}|${file.fileSize}`) @@ -289,13 +321,19 @@ function matchesOptimisticUserMessage( const hasOptimisticTimestamp = Number.isFinite(optimisticTimestampMs) && optimisticTimestampMs > 0; const hasCandidateTimestamp = candidate.timestamp != null; + const candidateTimestampMs = hasCandidateTimestamp ? toMs(candidate.timestamp as number) : 0; const timestampMatches = hasOptimisticTimestamp && hasCandidateTimestamp - ? Math.abs(toMs(candidate.timestamp as number) - optimisticTimestampMs) < 5000 + ? Math.abs(candidateTimestampMs - optimisticTimestampMs) < 5000 + : false; + const looseTimestampMatches = hasOptimisticTimestamp && hasCandidateTimestamp + ? candidateTimestampMs >= optimisticTimestampMs - 10_000 + && Math.abs(candidateTimestampMs - optimisticTimestampMs) < 120_000 : false; if (sameText && sameAttachments) return true; if (sameText && (!optimisticAttachments || !candidateAttachments) && (timestampMatches || !hasCandidateTimestamp)) return true; if (sameAttachments && (!optimisticText || !candidateText) && (timestampMatches || !hasCandidateTimestamp)) return true; + if (sameText && looseTimestampMatches) return true; return false; } @@ -854,6 +892,28 @@ function ensureSessionEntry(sessions: ChatSession[], sessionKey: string): ChatSe return [...sessions, { key: sessionKey, displayName: sessionKey }]; } +async function buildKnowledgeContext(options?: { + useKnowledgeBase?: boolean; + workspaceId?: string; + selectedKnowledgeDocumentIds?: string[]; +}): Promise { + if (!options?.useKnowledgeBase || !options.workspaceId) return ''; + const selectedIds = new Set(options.selectedKnowledgeDocumentIds ?? []); + if (selectedIds.size === 0) return ''; + + const result = await hostApiFetch<{ context?: string }>( + '/api/knowledge/context', + { + method: 'POST', + body: JSON.stringify({ + workspaceId: options.workspaceId, + documentIds: [...selectedIds], + }), + }, + ); + return result.context?.trim() ?? ''; +} + function clearSessionEntryFromMap>(entries: T, sessionKey: string): T { return Object.fromEntries(Object.entries(entries).filter(([key]) => key !== sessionKey)) as T; } @@ -984,6 +1044,11 @@ function isRuntimeSystemInjection(text: string): boolean { if (!text) return false; const normalized = text.trim(); if (/^\s*System\s*\(untrusted\)\s*:/i.test(normalized)) return true; + if ( + /^\s*System\s*:\s*\[\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(?::\d{2})?\s+GMT[+-]\d+\]\s+[A-Za-z][\w -]*(?:\[[^\]]+\])?\s+(?:DM|Group|Room|Chat|Message)\s+\|\s+\S+(?:\s+\[msg:[^\]]+\])?\s*$/i.test(normalized) + ) { + return true; + } if ( /An async command you ran earlier has completed/i.test(normalized) && /Do not relay it to the user unless explicitly requested/i.test(normalized) @@ -1245,14 +1310,33 @@ export const useChatStore = create((set, get) => ({ const data = await useGatewayStore.getState().rpc>('sessions.list', {}); if (data) { const rawSessions = Array.isArray(data.sessions) ? data.sessions : []; - const sessions: ChatSession[] = rawSessions.map((s: Record) => ({ - key: String(s.key || ''), - label: s.label ? String(s.label) : undefined, - displayName: s.displayName ? String(s.displayName) : undefined, - thinkingLevel: s.thinkingLevel ? String(s.thinkingLevel) : undefined, - model: s.model ? String(s.model) : undefined, - updatedAt: parseSessionUpdatedAtMs(s.updatedAt), - })).filter((s: ChatSession) => s.key); + const weChatAccountAliasByUserId = await loadWeChatAccountAliasByUserId(); + const sessions: ChatSession[] = rawSessions.map((s: Record) => { + const deliveryContext = s.deliveryContext && typeof s.deliveryContext === 'object' + ? s.deliveryContext as Record + : null; + const channel = typeof deliveryContext?.channel === 'string' ? deliveryContext.channel : undefined; + const to = typeof deliveryContext?.to === 'string' ? deliveryContext.to : undefined; + const accountId = typeof deliveryContext?.accountId === 'string' ? deliveryContext.accountId : undefined; + const resolvedAccountId = normalizeSessionChannel(channel) === 'wechat' && to + ? (weChatAccountAliasByUserId.get(to.trim().toLowerCase()) ?? accountId) + : accountId; + return { + key: String(s.key || ''), + label: s.label ? String(s.label) : undefined, + displayName: s.displayName ? String(s.displayName) : undefined, + thinkingLevel: s.thinkingLevel ? String(s.thinkingLevel) : undefined, + model: s.model ? String(s.model) : undefined, + updatedAt: parseSessionUpdatedAtMs(s.updatedAt), + deliveryContext: deliveryContext + ? { + channel, + accountId: resolvedAccountId, + to, + } + : undefined, + }; + }).filter((s: ChatSession) => s.key); const canonicalBySuffix = new Map(); for (const session of sessions) { @@ -1340,7 +1424,7 @@ export const useChatStore = create((set, get) => ({ set((s) => { const next: Partial = {}; if (firstUser) { - const labelText = getMessageText(firstUser.content).trim(); + const labelText = getUserDisplayText(firstUser.content); if (labelText) { const truncated = labelText.length > 50 ? `${labelText.slice(0, 50)}…` : labelText; next.sessionLabels = { ...s.sessionLabels, [session.key]: truncated }; @@ -1630,7 +1714,7 @@ export const useChatStore = create((set, get) => ({ if (!isMainSession) { const firstUserMsg = finalMessages.find((m) => m.role === 'user'); if (firstUserMsg) { - const labelText = getMessageText(firstUserMsg.content).trim(); + const labelText = getUserDisplayText(firstUserMsg.content); if (labelText) { const truncated = labelText.length > 50 ? `${labelText.slice(0, 50)}…` : labelText; set((s) => ({ @@ -1809,6 +1893,7 @@ export const useChatStore = create((set, get) => ({ text: string, attachments?: Array<{ fileName: string; mimeType: string; fileSize: number; stagedPath: string; preview: string | null }>, targetAgentId?: string | null, + options?: { useKnowledgeBase?: boolean; workspaceId?: string; selectedKnowledgeDocumentIds?: string[] }, ) => { const trimmed = text.trim(); if (!trimmed && (!attachments || attachments.length === 0)) return; @@ -1934,6 +2019,9 @@ export const useChatStore = create((set, get) => ({ // Longer timeout for chat sends to tolerate high-latency networks (avoids connect error) const CHAT_SEND_TIMEOUT_MS = 120_000; + const knowledgeContext = await buildKnowledgeContext(options); + const messageToSend = [trimmed, knowledgeContext].filter(Boolean).join('\n\n'); + if (hasMedia) { result = await hostApiFetch<{ success: boolean; result?: { runId?: string }; error?: string }>( '/api/chat/send-with-media', @@ -1941,7 +2029,7 @@ export const useChatStore = create((set, get) => ({ method: 'POST', body: JSON.stringify({ sessionKey: currentSessionKey, - message: trimmed || 'Process the attached file(s).', + message: messageToSend || 'Process the attached file(s).', deliver: false, idempotencyKey, media: attachments.map((a) => ({ @@ -1957,7 +2045,7 @@ export const useChatStore = create((set, get) => ({ 'chat.send', { sessionKey: currentSessionKey, - message: trimmed, + message: messageToSend, deliver: false, idempotencyKey, }, diff --git a/src/stores/chat/helpers.ts b/src/stores/chat/helpers.ts index 737fa88..c7150fe 100644 --- a/src/stores/chat/helpers.ts +++ b/src/stores/chat/helpers.ts @@ -154,7 +154,9 @@ function stripGatewayUserMetadata(text: string): string { .replace(/\s*\[media attached:[^\]]*\]/g, '') .replace(/\s*\[message_id:\s*[^\]]+\]/g, '') .replace(/^Conversation info\s*\([^)]*\):\s*```[a-z]*\n[\s\S]*?```\s*/i, '') - .replace(/^Conversation info\s*\([^)]*\):\s*\{[\s\S]*?\}\s*/i, ''); + .replace(/^Conversation info\s*\([^)]*\):\s*\{[\s\S]*?\}\s*/i, '') + .replace(/^\s*System\s*:\s*\[\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(?::\d{2})?\s+GMT[+-]\d+\]\s+[A-Za-z][\w -]*(?:\[[^\]]+\])?\s+(?:DM|Group|Room|Chat|Message)\s+\|\s+\S+(?:\s+\[msg:[^\]]+\])?\s*/i, '') + .trim(); } function normalizeComparableUserText(content: unknown): string { @@ -163,6 +165,10 @@ function normalizeComparableUserText(content: unknown): string { .trim(); } +function getUserDisplayText(content: unknown): string { + return stripGatewayUserMetadata(getMessageText(content)).trim(); +} + function getComparableAttachmentSignature(message: Pick): string { const files = (message._attachedFiles || []) .map((file) => file.filePath || `${file.fileName}|${file.mimeType}|${file.fileSize}`) @@ -188,13 +194,19 @@ function matchesOptimisticUserMessage( const hasOptimisticTimestamp = Number.isFinite(optimisticTimestampMs) && optimisticTimestampMs > 0; const hasCandidateTimestamp = candidate.timestamp != null; + const candidateTimestampMs = hasCandidateTimestamp ? toMs(candidate.timestamp as number) : 0; const timestampMatches = hasOptimisticTimestamp && hasCandidateTimestamp - ? Math.abs(toMs(candidate.timestamp as number) - optimisticTimestampMs) < 5000 + ? Math.abs(candidateTimestampMs - optimisticTimestampMs) < 5000 + : false; + const looseTimestampMatches = hasOptimisticTimestamp && hasCandidateTimestamp + ? candidateTimestampMs >= optimisticTimestampMs - 10_000 + && Math.abs(candidateTimestampMs - optimisticTimestampMs) < 120_000 : false; if (sameText && sameAttachments) return true; if (sameText && (!optimisticAttachments || !candidateAttachments) && (timestampMatches || !hasCandidateTimestamp)) return true; if (sameAttachments && (!optimisticText || !candidateText) && (timestampMatches || !hasCandidateTimestamp)) return true; + if (sameText && looseTimestampMatches) return true; return false; } @@ -790,6 +802,15 @@ function isRuntimeSystemInjection(text: string): boolean { // "System (untrusted): ..." at the start (with optional leading whitespace) if (/^\s*System\s*\(untrusted\)\s*:/i.test(normalized)) return true; + // Channel ingress metadata can arrive as a user-role message, for example: + // "System: [2026-04-27 11:09:29 GMT+8] Feishu[default] DM | ou_xxx [msg:om_xxx]" + // Hide only the pure metadata line; actual user text should remain visible. + if ( + /^\s*System\s*:\s*\[\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(?::\d{2})?\s+GMT[+-]\d+\]\s+[A-Za-z][\w -]*(?:\[[^\]]+\])?\s+(?:DM|Group|Room|Chat|Message)\s+\|\s+\S+(?:\s+\[msg:[^\]]+\])?\s*$/i.test(normalized) + ) { + return true; + } + // Async command completion notice + internal relay directive commonly arrive together. // Require both markers to avoid hiding normal conversational text that quotes one phrase. if ( @@ -1053,6 +1074,7 @@ export { clearHistoryPoll, extractImagesAsAttachedFiles, getMessageText, + getUserDisplayText, extractMediaRefs, extractRawFilePaths, makeAttachedFile, diff --git a/src/stores/chat/history-actions.ts b/src/stores/chat/history-actions.ts index 83acbd6..b109482 100644 --- a/src/stores/chat/history-actions.ts +++ b/src/stores/chat/history-actions.ts @@ -6,6 +6,7 @@ import { enrichWithToolResultFiles, getLatestOptimisticUserMessage, getMessageText, + getUserDisplayText, isInternalMessage, isToolResultRole, loadMissingPreviews, @@ -119,7 +120,7 @@ export function createHistoryActions( if (!isMainSession) { const firstUserMsg = finalMessages.find((m) => m.role === 'user'); if (firstUserMsg) { - const labelText = getMessageText(firstUserMsg.content).trim(); + const labelText = getUserDisplayText(firstUserMsg.content); if (labelText) { const truncated = labelText.length > 50 ? `${labelText.slice(0, 50)}…` : labelText; set((s) => ({ diff --git a/src/stores/chat/runtime-send-actions.ts b/src/stores/chat/runtime-send-actions.ts index ddb2da6..b7c6a21 100644 --- a/src/stores/chat/runtime-send-actions.ts +++ b/src/stores/chat/runtime-send-actions.ts @@ -1,4 +1,5 @@ import { invokeIpc } from '@/lib/api-client'; +import { hostApiFetch } from '@/lib/host-api'; import { useAgentsStore } from '@/stores/agents'; import { clearErrorRecoveryTimer, @@ -44,12 +45,35 @@ function ensureSessionEntry(sessions: ChatSession[], sessionKey: string): ChatSe let sendGeneration = 0; +async function buildKnowledgeContext(options?: { + useKnowledgeBase?: boolean; + workspaceId?: string; + selectedKnowledgeDocumentIds?: string[]; +}): Promise { + if (!options?.useKnowledgeBase || !options.workspaceId) return ''; + const selectedIds = new Set(options.selectedKnowledgeDocumentIds ?? []); + if (selectedIds.size === 0) return ''; + + const result = await hostApiFetch<{ context?: string }>( + '/api/knowledge/context', + { + method: 'POST', + body: JSON.stringify({ + workspaceId: options.workspaceId, + documentIds: [...selectedIds], + }), + }, + ); + return result.context?.trim() ?? ''; +} + export function createRuntimeSendActions(set: ChatSet, get: ChatGet): Pick { return { sendMessage: async ( text: string, attachments?: Array<{ fileName: string; mimeType: string; fileSize: number; stagedPath: string; preview: string | null }>, targetAgentId?: string | null, + options?: { useKnowledgeBase?: boolean; workspaceId?: string; selectedKnowledgeDocumentIds?: string[] }, ) => { const trimmed = text.trim(); if (!trimmed && (!attachments || attachments.length === 0)) return; @@ -195,12 +219,15 @@ export function createRuntimeSendActions(set: ChatSet, get: ChatGet): Pick ({ @@ -216,7 +243,7 @@ export function createRuntimeSendActions(set: ChatSet, get: ChatGet): Pick { const next: Partial = {}; if (firstUser) { - const labelText = getMessageText(firstUser.content).trim(); + const labelText = getUserDisplayText(firstUser.content); if (labelText) { const truncated = labelText.length > 50 ? `${labelText.slice(0, 50)}…` : labelText; next.sessionLabels = { ...s.sessionLabels, [session.key]: truncated }; diff --git a/src/stores/chat/types.ts b/src/stores/chat/types.ts index e5f7e67..c29ada0 100644 --- a/src/stores/chat/types.ts +++ b/src/stores/chat/types.ts @@ -46,6 +46,11 @@ export interface ChatSession { thinkingLevel?: string; model?: string; updatedAt?: number; + deliveryContext?: { + channel?: string; + accountId?: string; + to?: string; + }; } export interface ToolStatus { @@ -104,6 +109,7 @@ export interface ChatState { preview: string | null; }>, targetAgentId?: string | null, + options?: { useKnowledgeBase?: boolean; workspaceId?: string; selectedKnowledgeDocumentIds?: string[] }, ) => Promise; abortRun: () => Promise; handleChatEvent: (event: Record) => void; diff --git a/src/stores/cron.ts b/src/stores/cron.ts index 349adde..cdafb0b 100644 --- a/src/stores/cron.ts +++ b/src/stores/cron.ts @@ -9,6 +9,21 @@ import type { CronJob, CronJobCreateInput, CronJobUpdateInput } from '../types/c let _fetchJobsInFlight: Promise | null = null; +function normalizeCronJobsResponse(result: unknown): CronJob[] { + if (Array.isArray(result)) return result as CronJob[]; + + if (result && typeof result === 'object') { + const record = result as { jobs?: unknown; data?: unknown; error?: unknown; success?: unknown }; + if (Array.isArray(record.jobs)) return record.jobs as CronJob[]; + if (Array.isArray(record.data)) return record.data as CronJob[]; + if (record.success === false && typeof record.error === 'string') { + throw new Error(record.error); + } + } + + throw new Error('Invalid cron jobs response'); +} + interface CronState { jobs: CronJob[]; loading: boolean; @@ -45,14 +60,15 @@ export const useCronStore = create((set) => ({ } try { - const result = await hostApiFetch('/api/cron/jobs'); + const result = await hostApiFetch('/api/cron/jobs'); + const jobs = normalizeCronJobsResponse(result); // Gateway now correctly returns agentId for all jobs. // If Gateway returned fewer jobs than we have (e.g. race condition), preserve // the extra ones from current state to avoid losing data. - const resultIds = new Set(result.map((j) => j.id)); + const resultIds = new Set(jobs.map((j) => j.id)); const extraJobs = currentJobs.filter((j) => !resultIds.has(j.id)); - const allJobs = [...result, ...extraJobs]; + const allJobs = [...jobs, ...extraJobs]; set({ jobs: allJobs, loading: false }); } catch (error) { @@ -140,8 +156,8 @@ export const useCronStore = create((set) => ({ }); // Refresh jobs after trigger to update lastRun/nextRun state try { - const result = await hostApiFetch('/api/cron/jobs'); - set({ jobs: result }); + const result = await hostApiFetch('/api/cron/jobs'); + set({ jobs: normalizeCronJobsResponse(result) }); } catch { // Ignore refresh error } diff --git a/src/stores/settings.ts b/src/stores/settings.ts index 7c89e41..9d998c9 100644 --- a/src/stores/settings.ts +++ b/src/stores/settings.ts @@ -65,9 +65,27 @@ interface SettingsState { resetSettings: () => void; } +function getDefaultLanguage(): string { + if (typeof window !== 'undefined' && new URLSearchParams(window.location.search).get('e2e') === '1') { + return 'en'; + } + return resolveSupportedLanguage(typeof navigator !== 'undefined' ? navigator.language : undefined); +} + +function applyLanguage(language: string): string { + const resolvedLanguage = resolveSupportedLanguage(language); + void i18n.changeLanguage(resolvedLanguage); + + if (typeof document !== 'undefined') { + document.documentElement.lang = resolvedLanguage === 'zh' ? 'zh-CN' : 'en'; + } + + return resolvedLanguage; +} + const defaultSettings = { theme: 'system' as Theme, - language: resolveSupportedLanguage(typeof navigator !== 'undefined' ? navigator.language : undefined), + language: getDefaultLanguage(), startMinimized: false, launchAtStartup: false, telemetryEnabled: true, @@ -87,26 +105,24 @@ const defaultSettings = { setupComplete: false, }; +applyLanguage(defaultSettings.language); + export const useSettingsStore = create()( persist( - (set) => ({ + (set, get) => ({ ...defaultSettings, init: async () => { try { const settings = await hostApiFetch>('/api/settings'); - const resolvedLanguage = settings.language - ? resolveSupportedLanguage(settings.language) - : undefined; + const resolvedLanguage = applyLanguage(settings.language ?? get().language); set((state) => ({ ...state, ...settings, - ...(resolvedLanguage ? { language: resolvedLanguage } : {}), + language: resolvedLanguage, })); - if (resolvedLanguage) { - i18n.changeLanguage(resolvedLanguage); - } } catch { + applyLanguage(get().language); // Keep renderer-persisted settings as a fallback when the main // process store is not reachable. } @@ -120,8 +136,7 @@ export const useSettingsStore = create()( }).catch(() => { }); }, setLanguage: (language) => { - const resolvedLanguage = resolveSupportedLanguage(language); - i18n.changeLanguage(resolvedLanguage); + const resolvedLanguage = applyLanguage(language); set({ language: resolvedLanguage }); void hostApiFetch('/api/settings/language', { method: 'PUT', @@ -175,10 +190,22 @@ export const useSettingsStore = create()( }).catch(() => { }); }, markSetupComplete: () => set({ setupComplete: true }), - resetSettings: () => set(defaultSettings), + resetSettings: () => { + const language = applyLanguage(defaultSettings.language); + set({ ...defaultSettings, language }); + }, }), { name: 'clawx-settings', + merge: (persistedState, currentState) => { + const persisted = persistedState as Partial | undefined; + const language = applyLanguage(persisted?.language ?? currentState.language); + return { + ...currentState, + ...persisted, + language, + }; + }, } ) ); diff --git a/src/stores/update.ts b/src/stores/update.ts index e082255..962f47f 100644 --- a/src/stores/update.ts +++ b/src/stores/update.ts @@ -29,6 +29,33 @@ export type UpdateStatus = | 'downloaded' | 'error'; +function isDevModeUpdateSkip(error?: string | null): boolean { + return !!error && /dev mode|not packaged/i.test(error) && /update check skipped/i.test(error); +} + +function normalizeUpdateStatus(status: { + status: UpdateStatus; + info?: UpdateInfo; + progress?: ProgressInfo; + error?: string; +}) { + if (status.status === 'error' && isDevModeUpdateSkip(status.error)) { + return { + status: 'idle' as UpdateStatus, + updateInfo: null, + progress: null, + error: null, + }; + } + + return { + status: status.status, + updateInfo: status.info || null, + progress: status.progress || null, + error: status.error || null, + }; +} + interface UpdateState { status: UpdateStatus; currentVersion: string; @@ -78,12 +105,7 @@ export const useUpdateStore = create((set, get) => ({ progress?: ProgressInfo; error?: string; }>('update:status'); - set({ - status: status.status, - updateInfo: status.info || null, - progress: status.progress || null, - error: status.error || null, - }); + set(normalizeUpdateStatus(status)); } catch (error) { console.error('Failed to get update status:', error); } @@ -98,12 +120,7 @@ export const useUpdateStore = create((set, get) => ({ progress?: ProgressInfo; error?: string; }; - set({ - status: status.status, - updateInfo: status.info || null, - progress: status.progress || null, - error: status.error || null, - }); + set(normalizeUpdateStatus(status)); }); window.electron.ipcRenderer.on('update:auto-install-countdown', (data) => { @@ -131,6 +148,7 @@ export const useUpdateStore = create((set, get) => ({ checkForUpdates: async () => { set({ status: 'checking', error: null }); + let completedWithoutUpdaterEvent = false; try { const result = await Promise.race([ @@ -148,14 +166,15 @@ export const useUpdateStore = create((set, get) => ({ }; if (result.status) { - set({ - status: result.status.status, - updateInfo: result.status.info || null, - progress: result.status.progress || null, - error: result.status.error || null, - }); + set(normalizeUpdateStatus(result.status)); + completedWithoutUpdaterEvent = true; } else if (!result.success) { - set({ status: 'error', error: result.error || 'Failed to check for updates' }); + if (isDevModeUpdateSkip(result.error)) { + set({ status: 'idle', error: null }); + completedWithoutUpdaterEvent = true; + } else { + set({ status: 'error', error: result.error || 'Failed to check for updates' }); + } } } catch (error) { set({ status: 'error', error: String(error) }); @@ -163,7 +182,7 @@ export const useUpdateStore = create((set, get) => ({ // In dev mode autoUpdater skips without emitting events, so the // status may still be 'checking' or even 'idle'. Catch both. const currentStatus = get().status; - if (currentStatus === 'checking' || currentStatus === 'idle') { + if (!completedWithoutUpdaterEvent && (currentStatus === 'checking' || currentStatus === 'idle')) { set({ status: 'error', error: 'Update check completed without a result. This usually means the app is running in dev mode.' }); } } diff --git a/src/stores/yinian-skills.ts b/src/stores/yinian-skills.ts new file mode 100644 index 0000000..8b5abcf --- /dev/null +++ b/src/stores/yinian-skills.ts @@ -0,0 +1,83 @@ +import { create } from 'zustand'; +import type { YinianLocalSkill, YinianSkillRegistry, YinianSkillRegistryByHotel, YinianSkillSyncResult } from '../../shared/yinian'; + +interface YinianSkillsState { + localSkills: YinianLocalSkill[]; + lastSync: YinianSkillSyncResult | null; + loading: boolean; + error: string | null; + listLocal: () => Promise; + getRegistry: (hotelId?: string) => Promise; + sync: () => Promise; + reset: () => void; +} + +function getErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : 'Skill 同步失败'; +} + +function isSingleRegistry( + registry: YinianSkillRegistry | YinianSkillRegistryByHotel | undefined, +): registry is YinianSkillRegistry { + return Boolean(registry) + && typeof registry === 'object' + && 'hotelId' in registry + && Array.isArray((registry as YinianSkillRegistry).skills); +} + +export const useYinianSkillsStore = create((set) => ({ + localSkills: [], + lastSync: null, + loading: false, + error: null, + + listLocal: async () => { + set({ loading: true, error: null }); + try { + const localSkills = await window.yinian.skills.listLocal(); + set({ localSkills, loading: false }); + } catch (error) { + set({ loading: false, error: getErrorMessage(error) }); + } + }, + + getRegistry: async (hotelId) => { + set({ loading: true, error: null }); + try { + const registry = await window.yinian.skills.getRegistry(hotelId); + if (isSingleRegistry(registry)) { + set({ localSkills: registry.skills, loading: false }); + } else { + set({ loading: false }); + } + return registry; + } catch (error) { + set({ loading: false, error: getErrorMessage(error) }); + return undefined; + } + }, + + sync: async () => { + set({ loading: true, error: null }); + try { + const result = await window.yinian.skills.sync(); + set({ + lastSync: result, + localSkills: result.skills, + loading: false, + }); + } catch (error) { + set({ loading: false, error: getErrorMessage(error) }); + throw error; + } + }, + + reset: () => { + set({ + localSkills: [], + lastSync: null, + loading: false, + error: null, + }); + }, +})); diff --git a/src/stores/yinian.ts b/src/stores/yinian.ts new file mode 100644 index 0000000..40e6d64 --- /dev/null +++ b/src/stores/yinian.ts @@ -0,0 +1,205 @@ +import { create } from 'zustand'; +import type { + YinianAuthSession, + YinianConfigSnapshot, + YinianLoginWithPasswordInput, + YinianLoginWithSmsInput, + YinianSessionState, +} from '../../shared/yinian'; +import { + getEffectiveUserName, + getEffectiveWorkspaceName, + setDesktopUserName, + setWorkspaceDisplayName, +} from '@/lib/yinian-local-prefs'; + +type YinianStatus = 'idle' | 'loading' | 'authenticated' | 'unauthenticated' | 'error'; + +interface YinianState { + status: YinianStatus; + session: YinianSessionState; + config: YinianConfigSnapshot | null; + error: string | null; + init: () => Promise; + loginWithSms: (input: YinianLoginWithSmsInput) => Promise; + loginWithPassword: (input: YinianLoginWithPasswordInput) => Promise; + logout: () => Promise; + refreshConfig: () => Promise; + switchHotel: (hotelId: string) => Promise; + switchWorkspace: (workspaceId: string) => Promise; + updateDesktopUserName: (name: string) => void; + updateWorkspaceDisplayName: (name: string) => void; +} + +const unauthenticatedSession: YinianSessionState = { authenticated: false }; + +function getErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : '操作失败'; +} + +async function loadConfigIfAuthenticated(session: YinianSessionState): Promise { + if (!session.authenticated) return null; + return applyLocalUserName(await window.yinian.app.getConfig()); +} + +async function refreshSkillsForSession(session: YinianSessionState): Promise { + if (!session.authenticated) return; + const { useYinianSkillsStore } = await import('./yinian-skills'); + await useYinianSkillsStore.getState().getRegistry(session.currentHotelId); +} + +function applyLocalUserName(config: YinianConfigSnapshot | null): YinianConfigSnapshot | null { + if (!config) return config; + return { + ...config, + hotel: { + ...config.hotel, + name: getEffectiveWorkspaceName(config.hotel.name), + }, + user: { + ...config.user, + name: getEffectiveUserName(config.user.name), + }, + }; +} + +function applyLocalSessionUserName(session: YinianSessionState): YinianSessionState { + if (!session.authenticated) return session; + return { + ...session, + user: { + ...session.user, + name: getEffectiveUserName(session.user.name), + }, + }; +} + +export const useYinianStore = create((set, get) => ({ + status: 'idle', + session: unauthenticatedSession, + config: null, + error: null, + + init: async () => { + set({ status: 'loading', error: null }); + try { + const session = await window.yinian.auth.restoreSession(); + const config = await loadConfigIfAuthenticated(session); + await refreshSkillsForSession(session); + set({ + session: applyLocalSessionUserName(session), + config, + status: session.authenticated ? 'authenticated' : 'unauthenticated', + }); + } catch (error) { + set({ + status: 'error', + session: unauthenticatedSession, + config: null, + error: getErrorMessage(error), + }); + } + }, + + loginWithSms: async (input) => { + set({ status: 'loading', error: null }); + try { + const session = await window.yinian.auth.loginWithSms(input); + const config = applyLocalUserName(await window.yinian.app.getConfig()); + await refreshSkillsForSession(session); + set({ session: applyLocalSessionUserName(session), config, status: 'authenticated' }); + } catch (error) { + set({ status: 'unauthenticated', error: getErrorMessage(error) }); + throw error; + } + }, + + loginWithPassword: async (input) => { + set({ status: 'loading', error: null }); + try { + const session = await window.yinian.auth.loginWithPassword(input); + const config = applyLocalUserName(await window.yinian.app.getConfig()); + await refreshSkillsForSession(session); + set({ session: applyLocalSessionUserName(session), config, status: 'authenticated' }); + } catch (error) { + set({ status: 'unauthenticated', error: getErrorMessage(error) }); + throw error; + } + }, + + logout: async () => { + await window.yinian.auth.logout(); + const { useYinianSkillsStore } = await import('./yinian-skills'); + useYinianSkillsStore.getState().reset(); + set({ + status: 'unauthenticated', + session: unauthenticatedSession, + config: null, + error: null, + }); + }, + + refreshConfig: async () => { + const { session } = get(); + if (!session.authenticated) return; + const config = applyLocalUserName(await window.yinian.app.getConfig()); + await refreshSkillsForSession(session); + set({ config }); + }, + + switchHotel: async (hotelId) => { + set({ status: 'loading', error: null }); + try { + const session: YinianAuthSession = await window.yinian.app.switchHotel(hotelId); + const config = applyLocalUserName(await window.yinian.app.getConfig()); + await refreshSkillsForSession(session); + set({ session: applyLocalSessionUserName(session), config, status: 'authenticated' }); + } catch (error) { + set({ status: 'error', error: getErrorMessage(error) }); + throw error; + } + }, + + switchWorkspace: async (workspaceId) => get().switchHotel(workspaceId), + + updateDesktopUserName: (name) => { + const nextName = setDesktopUserName(name); + if (!nextName) return; + set((state) => ({ + session: state.session.authenticated + ? { + ...state.session, + user: { + ...state.session.user, + name: nextName, + }, + } + : state.session, + config: state.config + ? { + ...state.config, + user: { + ...state.config.user, + name: nextName, + }, + } + : state.config, + })); + }, + + updateWorkspaceDisplayName: (name) => { + const nextName = setWorkspaceDisplayName(name); + if (!nextName) return; + set((state) => ({ + config: state.config + ? { + ...state.config, + hotel: { + ...state.config.hotel, + name: nextName, + }, + } + : state.config, + })); + }, +})); diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index ad287cc..daec984 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -17,9 +17,35 @@ export interface ElectronAPI { isDev: boolean; } +export interface YinianAPI { + auth: { + createImageCaptcha: (randomStr?: string) => Promise; + restoreSession: () => Promise; + getSessionState: () => Promise; + loginWithSms: (input: import('../../shared/yinian').YinianLoginWithSmsInput) => Promise; + loginWithPassword: (input: import('../../shared/yinian').YinianLoginWithPasswordInput) => Promise; + logout: () => Promise; + getSavedCredentials: () => Promise; + saveCredentials: (input: Pick) => Promise; + clearSavedCredentials: () => Promise; + }; + app: { + getServerStatus: () => Promise; + getConfig: () => Promise; + switchHotel: (hotelId: string) => Promise; + switchWorkspace: (workspaceId: string) => Promise; + }; + skills: { + sync: () => Promise; + listLocal: () => Promise; + getRegistry: (hotelId?: string) => Promise; + }; +} + declare global { interface Window { electron: ElectronAPI; + yinian: YinianAPI; } } diff --git a/tests/e2e/yinian-visual-smoke.spec.ts b/tests/e2e/yinian-visual-smoke.spec.ts new file mode 100644 index 0000000..b6e1048 --- /dev/null +++ b/tests/e2e/yinian-visual-smoke.spec.ts @@ -0,0 +1,105 @@ +import electronBinaryPath from 'electron'; +import { _electron as electron, test } from '@playwright/test'; +import { mkdir, mkdtemp, rm } from 'node:fs/promises'; +import { createServer } from 'node:net'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { closeElectronApp, expect, getStableWindow } from './fixtures/electron'; + +const repoRoot = resolve(process.cwd()); +const electronEntry = join(repoRoot, 'dist-electron/main/index.js'); +const rendererEntry = pathToFileURL(join(repoRoot, 'dist/index.html')).toString(); +const screenshotDir = join(repoRoot, 'test-results', 'yinian-visual'); + +async function allocatePort(): Promise { + return await new Promise((resolvePort, reject) => { + const server = createServer(); + server.once('error', reject); + server.listen(0, '127.0.0.1', () => { + const address = server.address(); + if (!address || typeof address === 'string') { + server.close(() => reject(new Error('Failed to allocate an ephemeral port'))); + return; + } + + server.close((error) => { + if (error) { + reject(error); + return; + } + resolvePort(address.port); + }); + }); + }); +} + +test('captures the core Zhinian production UI surfaces', async () => { + await mkdir(screenshotDir, { recursive: true }); + const homeDir = await mkdtemp(join(tmpdir(), 'zhinian-visual-home-')); + const userDataDir = await mkdtemp(join(tmpdir(), 'zhinian-visual-user-data-')); + const hostApiPort = await allocatePort(); + const electronEnv = process.platform === 'linux' + ? { ELECTRON_DISABLE_SANDBOX: '1' } + : {}; + + const app = await electron.launch({ + executablePath: electronBinaryPath, + args: [electronEntry], + env: { + ...process.env, + ...electronEnv, + HOME: homeDir, + USERPROFILE: homeDir, + APPDATA: join(homeDir, 'AppData', 'Roaming'), + LOCALAPPDATA: join(homeDir, 'AppData', 'Local'), + XDG_CONFIG_HOME: join(homeDir, '.config'), + CLAWX_E2E: '1', + CLAWX_USER_DATA_DIR: userDataDir, + CLAWX_PORT_CLAWX_HOST_API: String(hostApiPort), + VITE_DEV_SERVER_URL: '', + }, + timeout: 90_000, + }); + + try { + const page = await getStableWindow(app); + await page.setViewportSize({ width: 1440, height: 900 }); + + // The main process stays in E2E mode to avoid startup side effects, while + // the renderer is reloaded without the e2e query so the production YINIAN + // login and tenant flow are exercised. + await page.goto(rendererEntry); + await expect(page.getByTestId('yinian-login-page')).toBeVisible(); + await page.screenshot({ path: join(screenshotDir, '01-login.png'), fullPage: true }); + + await page.getByRole('textbox', { name: '账号' }).fill('admin'); + await page.locator('#password').fill('123456'); + const captchaInput = page.getByLabel('图形验证码'); + if (await captchaInput.count()) { + await captchaInput.fill('5678'); + } + await page.getByRole('button', { name: /^登录$/ }).click(); + await expect(page.getByTestId('today-page')).toBeVisible(); + await page.getByTestId('sidebar-chat-history').hover(); + await expect(page.getByTestId('sidebar-chat-history-popover')).toBeVisible(); + await page.screenshot({ path: join(screenshotDir, '02-today.png'), fullPage: true }); + + await page.getByTestId('sidebar-nav-skills').click(); + await expect(page.getByTestId('yinian-skills-page')).toBeVisible(); + await page.screenshot({ path: join(screenshotDir, '03-skills.png'), fullPage: true }); + + await page.getByTestId('sidebar-nav-knowledge').click(); + await expect(page.getByTestId('knowledge-page')).toBeVisible(); + await page.screenshot({ path: join(screenshotDir, '04-knowledge.png'), fullPage: true }); + + await page.getByTestId('sidebar-nav-settings').click(); + await expect(page.getByTestId('settings-page')).toBeVisible(); + await expect(page.getByTestId('settings-service-section')).toBeVisible(); + await page.screenshot({ path: join(screenshotDir, '05-settings.png'), fullPage: true }); + } finally { + await closeElectronApp(app); + await rm(homeDir, { recursive: true, force: true }); + await rm(userDataDir, { recursive: true, force: true }); + } +}); diff --git a/tests/fixtures/yinian-server-contract.ts b/tests/fixtures/yinian-server-contract.ts new file mode 100644 index 0000000..e6f228c --- /dev/null +++ b/tests/fixtures/yinian-server-contract.ts @@ -0,0 +1,147 @@ +export const yinianContractFixtures = { + login: { + accessToken: 'access_demo', + refreshToken: 'refresh_demo', + accessTokenExpiresAt: 1777188600000, + user: { + id: 'user_ops_001', + name: '王管理员', + phone: '13800000000', + email: 'ops@example.com', + avatar: 'https://cdn.example.com/avatar.png', + }, + hotels: [ + { + id: 'workspace_hangzhou_ops', + name: '智念企业组织空间', + brand: '智念', + }, + { + hotel_id: 'workspace_shanghai_growth', + name: '智念增长组织空间', + }, + ], + current_hotel_id: 'workspace_hangzhou_ops', + }, + refresh: { + access_token: 'access_refreshed', + refresh_token: 'refresh_rotated', + access_token_expires_at: 1777189500000, + user: { + id: 'user_ops_001', + name: '王管理员', + }, + hotels: [ + { + id: 'workspace_hangzhou_ops', + name: '智念企业组织空间', + }, + ], + current_hotel_id: 'workspace_hangzhou_ops', + }, + me: { + user: { + id: 'user_ops_001', + name: '王管理员', + phone: '13800000000', + email: 'ops@example.com', + avatar: 'https://cdn.example.com/avatar.png', + ignored: 'server-can-add-fields', + }, + hotels: [ + { + id: 'workspace_hangzhou_ops', + name: '智念企业组织空间', + brand: '智念', + }, + { + hotel_id: 'workspace_shanghai_growth', + name: '智念增长组织空间', + }, + ], + current_hotel_id: 'workspace_hangzhou_ops', + access_token_expires_at: 1777188600000, + }, + config: { + server_time: 1777188000000, + hotel: { + hotel_id: 'workspace_hangzhou_ops', + name: '智念企业组织空间', + brand: '智念', + }, + entitlements: [ + { + skill_id: 'daily-report', + name: '日报生成助手', + version: '1.0.0', + enabled: true, + category: 'reporting', + triggers: ['scheduled', 'manual', 'unknown-trigger'], + last_run_at: 1777184400000, + }, + { + skillId: 'unknown-category-skill', + name: '未知分类技能', + version: '0.1.0', + enabled: true, + category: 'future-category', + triggers: ['webhook'], + }, + ], + notification_channels: [ + { + id: 'wechat_ops', + kind: 'wecom', + label: '业务通知群', + recipient: 'room_001', + enabled: true, + source: 'nianxx', + }, + { + id: 'kernel_email', + kind: 'email', + label: '财务邮箱', + recipient: 'finance@example.com', + enabled: false, + source: 'kernel', + }, + ], + feature_flags: { + skillsSync: true, + advancedSettings: false, + ignoredString: 'yes', + }, + ui_policy: { + default_page: 'today', + show_advanced_settings: true, + }, + }, + manifest: { + serverTime: 1777188000000, + hotelId: 'workspace_hangzhou_ops', + manifestVersion: '2026.04.26.1', + skills: [ + { + skill_id: 'data-check', + name: '数据检查助手', + version: '1.2.0', + enabled: true, + bundle_sha256: 'sha256-demo', + bundleUrl: 'https://cdn.example.com/skills/data-check-1.2.0.tgz', + }, + { + skillId: 'customer-reply-helper', + name: '客户回复助手', + version: '0.9.0', + enabled: false, + bundleSha256: 'sha256-disabled', + }, + ], + }, + error: { + error: { + code: 'SESSION_EXPIRED', + message: 'Session expired', + }, + }, +} as const; diff --git a/tests/setup.ts b/tests/setup.ts index b03176d..6eeb6d6 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -42,11 +42,39 @@ const mockElectron = { isDev: true, }; +const mockYinian = { + auth: { + createImageCaptcha: vi.fn(), + restoreSession: vi.fn(), + getSessionState: vi.fn(), + loginWithSms: vi.fn(), + loginWithPassword: vi.fn(), + logout: vi.fn(), + getSavedCredentials: vi.fn(), + saveCredentials: vi.fn(), + clearSavedCredentials: vi.fn(), + }, + app: { + getServerStatus: vi.fn(), + getConfig: vi.fn(), + switchHotel: vi.fn(), + }, + skills: { + sync: vi.fn(), + listLocal: vi.fn(), + getRegistry: vi.fn(), + }, +}; + if (typeof window !== 'undefined') { Object.defineProperty(window, 'electron', { value: mockElectron, writable: true, }); + Object.defineProperty(window, 'yinian', { + value: mockYinian, + writable: true, + }); } // Mock matchMedia diff --git a/tests/unit/agent-config.test.ts b/tests/unit/agent-config.test.ts index 837ccba..e84c3e3 100644 --- a/tests/unit/agent-config.test.ts +++ b/tests/unit/agent-config.test.ts @@ -489,6 +489,101 @@ describe('agent config lifecycle', () => { expect(snapshot.channelAccountOwners['telegram:default']).toBe('main'); }); + it('creates and binds a dedicated channel agent for a default account', async () => { + await writeOpenClawJson({ + agents: { + list: [{ id: 'main', name: 'Main', default: true }], + }, + channels: { + feishu: { enabled: true }, + }, + }); + await mkdir(join(testHome, '.openclaw', 'workspace'), { recursive: true }); + await writeFile(join(testHome, '.openclaw', 'workspace', 'AGENTS.md'), 'main instructions', 'utf8'); + + const { ensureChannelAgentForAccount, listAgentsSnapshot } = await import('@electron/utils/agent-config'); + + await ensureChannelAgentForAccount('feishu', 'default'); + + const snapshot = await listAgentsSnapshot(); + expect(snapshot.agents).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 'channel-feishu', + name: '飞书助手', + channelTypes: ['feishu'], + }), + ]), + ); + expect(snapshot.channelAccountOwners['feishu:default']).toBe('channel-feishu'); + await expect(access(join(testHome, '.openclaw', 'workspace-channel-feishu', 'AGENTS.md'))).resolves.toBeUndefined(); + }); + + it('creates separate channel agents for multiple accounts on the same channel', async () => { + await writeOpenClawJson({ + agents: { + list: [{ id: 'main', name: 'Main', default: true }], + }, + channels: { + telegram: { + enabled: true, + accounts: { + 'telegram-a1b2c3d4': { enabled: true }, + }, + }, + }, + }); + + const { ensureChannelAgentForAccount, listAgentsSnapshot } = await import('@electron/utils/agent-config'); + + await ensureChannelAgentForAccount('telegram', 'telegram-a1b2c3d4'); + + const snapshot = await listAgentsSnapshot(); + expect(snapshot.agents).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 'channel-telegram-telegram-a1b2c3d4', + name: 'Telegram助手 telegram-a1b2c3d4', + channelTypes: ['telegram'], + }), + ]), + ); + expect(snapshot.channelAccountOwners['telegram:telegram-a1b2c3d4']).toBe('channel-telegram-telegram-a1b2c3d4'); + }); + + it('creates an AgentBus channel agent with a readable name', async () => { + await writeOpenClawJson({ + agents: { + list: [{ id: 'main', name: 'Main', default: true }], + }, + channels: { + agentbus: { + enabled: true, + accounts: { + 'acct-20260427-070043-65935ec0': { enabled: true }, + }, + }, + }, + }); + + const { ensureChannelAgentForAccount, listAgentsSnapshot } = await import('@electron/utils/agent-config'); + + await ensureChannelAgentForAccount('agentbus', 'acct-20260427-070043-65935ec0'); + + const snapshot = await listAgentsSnapshot(); + expect(snapshot.agents).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 'channel-agentbus-acct-20260427-070043-65935ec0', + name: 'AgentBus助手 acct-20260427-070043-65935ec0', + channelTypes: ['agentbus'], + }), + ]), + ); + expect(snapshot.channelAccountOwners['agentbus:acct-20260427-070043-65935ec0']) + .toBe('channel-agentbus-acct-20260427-070043-65935ec0'); + }); + it('avoids numeric-only ids when creating agents from CJK names', async () => { await writeOpenClawJson({ agents: { diff --git a/tests/unit/channel-config.test.ts b/tests/unit/channel-config.test.ts index b1e9862..e79650e 100644 --- a/tests/unit/channel-config.test.ts +++ b/tests/unit/channel-config.test.ts @@ -301,4 +301,38 @@ describe('configured channel account extraction', () => { accountIds: ['2'], }); }); + + it('hides AgentBus default compatibility mirrors when a real account is selected', async () => { + const { listConfiguredChannelAccountsFromConfig } = await import('@electron/utils/channel-config'); + + const result = listConfiguredChannelAccountsFromConfig({ + channels: { + agentbus: { + enabled: true, + defaultAccount: 'acct-20260427-070043-65935ec0', + accounts: { + 'acct-20260427-070043-65935ec0': { + id: 'acct-20260427-070043-65935ec0', + agentId: 'openclaw:public:acct-20260427-070043-65935ec0', + token: 'token', + wsUrl: 'wss://link.nianxx.com/ws', + enabled: true, + }, + default: { + id: 'default', + agentId: 'openclaw:public:acct-20260427-070043-65935ec0', + token: 'token', + wsUrl: 'wss://link.nianxx.com/ws', + enabled: false, + }, + }, + }, + }, + }); + + expect(result.agentbus).toEqual({ + defaultAccountId: 'acct-20260427-070043-65935ec0', + accountIds: ['acct-20260427-070043-65935ec0'], + }); + }); }); diff --git a/tests/unit/channel-routes.test.ts b/tests/unit/channel-routes.test.ts index 8492c0f..7506e32 100644 --- a/tests/unit/channel-routes.test.ts +++ b/tests/unit/channel-routes.test.ts @@ -13,14 +13,19 @@ const proxyAwareFetchMock = vi.fn(); const saveChannelConfigMock = vi.fn(); const setChannelDefaultAccountMock = vi.fn(); const assignChannelAccountToAgentMock = vi.fn(); +const clearAllBindingsForChannelMock = vi.fn(); +const deleteAgentConfigMock = vi.fn(); +const deleteChannelAccountConfigMock = vi.fn(); +const deleteChannelConfigMock = vi.fn(); +const ensureChannelAgentForAccountMock = vi.fn(); const clearChannelBindingMock = vi.fn(); const parseJsonBodyMock = vi.fn(); const testOpenClawConfigDir = join(tmpdir(), 'clawx-tests', 'channel-routes-openclaw'); vi.mock('@electron/utils/channel-config', () => ({ cleanupDanglingWeChatPluginState: vi.fn(), - deleteChannelAccountConfig: vi.fn(), - deleteChannelConfig: vi.fn(), + deleteChannelAccountConfig: (...args: unknown[]) => deleteChannelAccountConfigMock(...args), + deleteChannelConfig: (...args: unknown[]) => deleteChannelConfigMock(...args), getChannelFormValues: vi.fn(), listConfiguredChannelAccounts: (...args: unknown[]) => listConfiguredChannelAccountsMock(...args), listConfiguredChannelAccountsFromConfig: (...args: unknown[]) => listConfiguredChannelAccountsMock(...args), @@ -36,8 +41,10 @@ vi.mock('@electron/utils/channel-config', () => ({ vi.mock('@electron/utils/agent-config', () => ({ assignChannelAccountToAgent: (...args: unknown[]) => assignChannelAccountToAgentMock(...args), - clearAllBindingsForChannel: vi.fn(), + clearAllBindingsForChannel: (...args: unknown[]) => clearAllBindingsForChannelMock(...args), clearChannelBinding: (...args: unknown[]) => clearChannelBindingMock(...args), + deleteAgentConfig: (...args: unknown[]) => deleteAgentConfigMock(...args), + ensureChannelAgentForAccount: (...args: unknown[]) => ensureChannelAgentForAccountMock(...args), listAgentsSnapshot: (...args: unknown[]) => listAgentsSnapshotMock(...args), listAgentsSnapshotFromConfig: (...args: unknown[]) => listAgentsSnapshotMock(...args), })); @@ -203,6 +210,193 @@ describe('handleChannelRoutes', () => { ); }); + it('auto-creates missing AgentBus channel agent bindings while listing accounts', async () => { + listConfiguredChannelsMock.mockResolvedValue(['agentbus']); + listConfiguredChannelAccountsMock.mockResolvedValue({ + agentbus: { + defaultAccountId: 'acct-20260427-070043-65935ec0', + accountIds: ['acct-20260427-070043-65935ec0'], + }, + }); + readOpenClawConfigMock.mockResolvedValue({ + bindings: [], + channels: { + agentbus: { + defaultAccount: 'acct-20260427-070043-65935ec0', + accounts: { + 'acct-20260427-070043-65935ec0': { + enabled: true, + }, + }, + }, + }, + }); + listAgentsSnapshotMock.mockResolvedValue({ + agents: [], + channelOwners: {}, + channelAccountOwners: {}, + }); + ensureChannelAgentForAccountMock.mockResolvedValue({ + agents: [], + channelOwners: { agentbus: 'channel-agentbus-acct-20260427-070043-65935ec0' }, + channelAccountOwners: { + 'agentbus:acct-20260427-070043-65935ec0': 'channel-agentbus-acct-20260427-070043-65935ec0', + }, + }); + + const rpc = vi.fn().mockResolvedValue({ + channels: { + agentbus: { + configured: true, + }, + }, + channelAccounts: { + agentbus: [ + { + accountId: 'acct-20260427-070043-65935ec0', + configured: true, + connected: false, + running: false, + linked: false, + }, + ], + }, + channelDefaultAccountId: { + agentbus: 'acct-20260427-070043-65935ec0', + }, + }); + + const { handleChannelRoutes } = await import('@electron/api/routes/channels'); + await handleChannelRoutes( + { method: 'GET' } as IncomingMessage, + {} as ServerResponse, + new URL('http://127.0.0.1:13210/api/channels/accounts'), + { + gatewayManager: { + rpc, + getStatus: () => ({ state: 'running' }), + getDiagnostics: () => ({ consecutiveHeartbeatMisses: 0, consecutiveRpcFailures: 0 }), + debouncedReload: vi.fn(), + debouncedRestart: vi.fn(), + }, + } as never, + ); + + expect(ensureChannelAgentForAccountMock).toHaveBeenCalledWith( + 'agentbus', + 'acct-20260427-070043-65935ec0', + ); + expect(sendJsonMock).toHaveBeenCalledWith( + expect.anything(), + 200, + expect.objectContaining({ + success: true, + channels: [ + expect.objectContaining({ + channelType: 'agentbus', + accounts: [ + expect.objectContaining({ + accountId: 'acct-20260427-070043-65935ec0', + agentId: 'channel-agentbus-acct-20260427-070043-65935ec0', + status: 'connected', + }), + ], + }), + ], + }), + ); + }); + + it('deletes the managed channel agent when deleting a channel account', async () => { + readOpenClawConfigMock.mockResolvedValue({ + bindings: [ + { + agentId: 'channel-agentbus-acct-20260427-070043-65935ec0', + match: { + channel: 'agentbus', + accountId: 'acct-20260427-070043-65935ec0', + }, + }, + ], + channels: { + agentbus: { + enabled: true, + defaultAccount: 'acct-20260427-070043-65935ec0', + accounts: { + 'acct-20260427-070043-65935ec0': { enabled: true }, + }, + }, + }, + }); + + const { handleChannelRoutes } = await import('@electron/api/routes/channels'); + await handleChannelRoutes( + { method: 'DELETE' } as IncomingMessage, + {} as ServerResponse, + new URL('http://127.0.0.1:13210/api/channels/config/agentbus?accountId=acct-20260427-070043-65935ec0'), + { + gatewayManager: { + rpc: vi.fn(), + getStatus: () => ({ state: 'running' }), + debouncedReload: vi.fn(), + debouncedRestart: vi.fn(), + }, + } as never, + ); + + expect(deleteAgentConfigMock).toHaveBeenCalledWith('channel-agentbus-acct-20260427-070043-65935ec0'); + expect(deleteChannelAccountConfigMock).toHaveBeenCalledWith( + 'agentbus', + 'acct-20260427-070043-65935ec0', + ); + expect(clearChannelBindingMock).toHaveBeenCalledWith('agentbus', 'acct-20260427-070043-65935ec0'); + }); + + it('does not delete a manually selected non-managed agent when deleting a channel account', async () => { + readOpenClawConfigMock.mockResolvedValue({ + bindings: [ + { + agentId: 'sales-agent', + match: { + channel: 'agentbus', + accountId: 'acct-20260427-070043-65935ec0', + }, + }, + ], + channels: { + agentbus: { + enabled: true, + defaultAccount: 'acct-20260427-070043-65935ec0', + accounts: { + 'acct-20260427-070043-65935ec0': { enabled: true }, + }, + }, + }, + }); + + const { handleChannelRoutes } = await import('@electron/api/routes/channels'); + await handleChannelRoutes( + { method: 'DELETE' } as IncomingMessage, + {} as ServerResponse, + new URL('http://127.0.0.1:13210/api/channels/config/agentbus?accountId=acct-20260427-070043-65935ec0'), + { + gatewayManager: { + rpc: vi.fn(), + getStatus: () => ({ state: 'running' }), + debouncedReload: vi.fn(), + debouncedRestart: vi.fn(), + }, + } as never, + ); + + expect(deleteAgentConfigMock).not.toHaveBeenCalled(); + expect(deleteChannelAccountConfigMock).toHaveBeenCalledWith( + 'agentbus', + 'acct-20260427-070043-65935ec0', + ); + expect(clearChannelBindingMock).toHaveBeenCalledWith('agentbus', 'acct-20260427-070043-65935ec0'); + }); + it('rejects non-canonical account ID on channel config save', async () => { parseJsonBodyMock.mockResolvedValue({ channelType: 'feishu', @@ -271,6 +465,7 @@ describe('handleChannelRoutes', () => { { botToken: 'token', allowedUsers: '123456' }, 'Legacy_Account', ); + expect(ensureChannelAgentForAccountMock).toHaveBeenCalledWith('telegram', 'Legacy_Account'); expect(sendJsonMock).toHaveBeenCalledWith( expect.anything(), 200, @@ -760,6 +955,7 @@ describe('handleChannelRoutes', () => { ); expect(assignChannelAccountToAgentMock).toHaveBeenCalledWith('main', 'telegram', 'default'); expect(clearChannelBindingMock).toHaveBeenCalledWith('telegram'); + expect(ensureChannelAgentForAccountMock).toHaveBeenCalledWith('telegram', 'telegram-a1b2c3d4'); expect(assignChannelAccountToAgentMock).not.toHaveBeenCalledWith('main', 'telegram', 'telegram-a1b2c3d4'); }); diff --git a/tests/unit/chat-input.test.tsx b/tests/unit/chat-input.test.tsx index 2f596b8..37ebfe2 100644 --- a/tests/unit/chat-input.test.tsx +++ b/tests/unit/chat-input.test.tsx @@ -69,14 +69,14 @@ vi.mock('react-i18next', () => ({ }), })); -describe('ChatInput agent targeting', () => { +describe('ChatInput context controls', () => { beforeEach(() => { agentsState.agents = []; chatState.currentAgentId = 'main'; gatewayState.status = { state: 'running', port: 18789 }; }); - it('hides the @agent picker when only one agent is configured', () => { + it('does not expose the old @agent picker', () => { agentsState.agents = [ { id: 'main', @@ -96,43 +96,40 @@ describe('ChatInput agent targeting', () => { expect(screen.queryByTitle('Choose agent')).not.toBeInTheDocument(); }); - it('lets the user select an agent target and sends it with the message', () => { + it('passes selected knowledge documents as context option', () => { const onSend = vi.fn(); - agentsState.agents = [ - { - id: 'main', - name: 'Main', - isDefault: true, - modelDisplay: 'MiniMax', - inheritedModel: true, - workspace: '~/.openclaw/workspace', - agentDir: '~/.openclaw/agents/main/agent', - mainSessionKey: 'agent:main:main', - channelTypes: [], - }, - { - id: 'research', - name: 'Research', - isDefault: false, - modelDisplay: 'Claude', - inheritedModel: false, - workspace: '~/.openclaw/workspace-research', - agentDir: '~/.openclaw/agents/research/agent', - mainSessionKey: 'agent:research:desk', - channelTypes: [], - }, - ]; + render( + , + ); - render(); - - fireEvent.click(screen.getByTitle('Choose agent')); - fireEvent.click(screen.getByText('Research')); - - expect(screen.getByText('@Research')).toBeInTheDocument(); - - fireEvent.change(screen.getByRole('textbox'), { target: { value: 'Hello direct agent' } }); + fireEvent.click(screen.getByTestId('chat-composer-knowledge-button')); + fireEvent.click(screen.getByText('handbook.docx')); + fireEvent.click(screen.getByText('faq.md')); + fireEvent.change(screen.getByRole('textbox'), { target: { value: 'Use company docs' } }); fireEvent.click(screen.getByTitle('Send')); - expect(onSend).toHaveBeenCalledWith('Hello direct agent', undefined, 'research'); + expect(onSend).toHaveBeenCalledWith('Use company docs', undefined, null, { + useKnowledgeBase: true, + workspaceId: 'workspace-1', + selectedKnowledgeDocumentIds: ['doc-1', 'doc-2'], + }); }); }); diff --git a/tests/unit/chat-internal-message-filter.test.ts b/tests/unit/chat-internal-message-filter.test.ts index fc6dc82..072f724 100644 --- a/tests/unit/chat-internal-message-filter.test.ts +++ b/tests/unit/chat-internal-message-filter.test.ts @@ -18,6 +18,21 @@ describe('chat internal message filter', () => { expect(isInternalMessage({ role: 'assistant', content })).toBe(true); }); + it('filters pure channel ingress metadata from Feishu', () => { + const content = 'System: [2026-04-27 11:09:29 GMT+8] Feishu[default] DM | ou_256bec6880a8c77271bc610c5e42fe89 [msg:om_x100b51d9784e2908c144d6c6cde19a6]'; + + expect(isInternalMessage({ role: 'user', content })).toBe(true); + }); + + it('does not filter user text that merely mentions a channel metadata line', () => { + const content = [ + 'System: [2026-04-27 11:09:29 GMT+8] Feishu[default] DM | ou_256bec6880a8c77271bc610c5e42fe89 [msg:om_x100b51d9784e2908c144d6c6cde19a6]', + '这条消息为什么会这样?', + ].join('\n'); + + expect(isInternalMessage({ role: 'user', content })).toBe(false); + }); + it('does not filter normal user message that starts with current time', () => { const content = 'Current time: 北京现在几点?'; diff --git a/tests/unit/chat-message-utils.test.ts b/tests/unit/chat-message-utils.test.ts new file mode 100644 index 0000000..c9201a1 --- /dev/null +++ b/tests/unit/chat-message-utils.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest'; +import { extractText } from '@/pages/Chat/message-utils'; + +describe('chat message display cleanup', () => { + it('removes Feishu channel metadata prefix but keeps user text', () => { + const content = [ + 'System: [2026-04-27 11:20:03 GMT+8] Feishu[default] DM | ou_256bec6880a8c77271bc610c5e42fe89 [msg:om_x100b51d910e2e0f0c3b6494c6bf8a9c]', + '', + '喂', + ].join('\n'); + + expect(extractText({ role: 'user', content })).toBe('喂'); + }); + + it('removes pure Feishu channel metadata from user display text', () => { + const content = 'System: [2026-04-27 11:20:03 GMT+8] Feishu[default] DM | ou_256bec6880a8c77271bc610c5e42fe89 [msg:om_x100b51d910e2e0f0c3b6494c6bf8a9c]'; + + expect(extractText({ role: 'user', content })).toBe(''); + }); +}); diff --git a/tests/unit/chat-optimistic-match.test.ts b/tests/unit/chat-optimistic-match.test.ts index c7dff94..a635680 100644 --- a/tests/unit/chat-optimistic-match.test.ts +++ b/tests/unit/chat-optimistic-match.test.ts @@ -55,6 +55,28 @@ describe('matchesOptimisticUserMessage', () => { expect(matchesOptimisticUserMessage(candidate, optimistic, 1_700_000_000_000)).toBe(true); }); + it('matches a same-text Gateway echo whose timestamp drifts during processing', () => { + const optimistic = { role: 'user', content: '帮我整理今日事项', timestamp: 1_700_000_000 } as const; + const candidate = { + role: 'user', + content: '[Wed 2026-04-22 10:31 GMT+8] 帮我整理今日事项', + timestamp: 1_700_000_045, + } as const; + + expect(matchesOptimisticUserMessage(candidate, optimistic, 1_700_000_000_000)).toBe(true); + }); + + it('does not match an older repeated question from the same session', () => { + const optimistic = { role: 'user', content: '继续', timestamp: 1_700_000_000 } as const; + const candidate = { + role: 'user', + content: '继续', + timestamp: 1_699_999_700, + } as const; + + expect(matchesOptimisticUserMessage(candidate, optimistic, 1_700_000_000_000)).toBe(false); + }); + it('still rejects unrelated user messages', () => { const optimistic = { role: 'user', content: 'run github1', timestamp: 1_700_000_000 } as const; const candidate = { diff --git a/tests/unit/chat-target-routing.test.ts b/tests/unit/chat-target-routing.test.ts index 3d70a5c..65176e4 100644 --- a/tests/unit/chat-target-routing.test.ts +++ b/tests/unit/chat-target-routing.test.ts @@ -185,4 +185,71 @@ describe('chat target routing', () => { expect(payload.message).toBe('Process the attached file(s).'); expect(payload.media[0]?.filePath).toBe('/tmp/design.png'); }); + + it('injects knowledge-base file references when requested', async () => { + hostApiFetchMock.mockImplementation(async (url: string, options?: { body?: string }) => { + if (url === '/api/knowledge/context') { + const body = JSON.parse(String(options?.body ?? '{}')) as { workspaceId?: string; documentIds?: string[] }; + expect(body).toEqual({ + workspaceId: 'workspace-1', + documentIds: ['kb-1'], + }); + return { + success: true, + context: [ + '[知识库上下文]', + '## handbook.docx', + 'Refunds are available within 7 days.', + ].join('\n'), + documents: [ + { + id: 'kb-1', + name: 'handbook.docx', + mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + storedPath: '/kb/files/handbook.docx', + textPath: '/kb/texts/handbook.txt', + }, + ], + }; + } + return { success: true, result: { runId: 'run-media' } }; + }); + + const { useChatStore } = await import('@/stores/chat'); + useChatStore.setState({ + currentSessionKey: 'agent:main:main', + currentAgentId: 'main', + sessions: [{ key: 'agent:main:main' }], + messages: [], + sessionLabels: {}, + sessionLastActivity: {}, + sending: false, + activeRunId: null, + streamingText: '', + streamingMessage: null, + streamingTools: [], + pendingFinal: false, + lastUserMessageAt: null, + pendingToolImages: [], + error: null, + loading: false, + thinkingLevel: null, + }); + + await useChatStore.getState().sendMessage('What is our refund policy?', undefined, null, { + useKnowledgeBase: true, + workspaceId: 'workspace-1', + selectedKnowledgeDocumentIds: ['kb-1'], + }); + + const sendCall = gatewayRpcMock.mock.calls.find(([method]) => method === 'chat.send'); + expect(sendCall?.[1]).toMatchObject({ + sessionKey: 'agent:main:main', + deliver: false, + }); + expect((sendCall?.[1] as { message: string }).message).toContain('What is our refund policy?'); + expect((sendCall?.[1] as { message: string }).message).toContain('[知识库上下文]'); + expect((sendCall?.[1] as { message: string }).message).toContain('handbook.docx'); + expect((sendCall?.[1] as { message: string }).message).toContain('Refunds are available within 7 days.'); + }); }); diff --git a/tests/unit/config-sync.test.ts b/tests/unit/config-sync.test.ts index 143dbaa..c6125d8 100644 --- a/tests/unit/config-sync.test.ts +++ b/tests/unit/config-sync.test.ts @@ -1,6 +1,41 @@ -import { describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { stripSystemdSupervisorEnv } from '@electron/gateway/config-sync-env'; +const { + mockEnsureCloudSyncPluginInstalled, + mockReadOpenClawConfig, + mockWriteOpenClawConfig, + mockLoggerWarn, +} = vi.hoisted(() => ({ + mockEnsureCloudSyncPluginInstalled: vi.fn(), + mockReadOpenClawConfig: vi.fn(), + mockWriteOpenClawConfig: vi.fn(), + mockLoggerWarn: vi.fn(), +})); + +vi.mock('@electron/utils/plugin-install', () => ({ + copyPluginFromNodeModules: vi.fn(), + cpSyncSafe: vi.fn(), + ensureCloudSyncPluginInstalled: mockEnsureCloudSyncPluginInstalled, + fixupPluginManifest: vi.fn(), +})); + +vi.mock('@electron/utils/channel-config', () => ({ + cleanupDanglingWeChatPluginState: vi.fn(), + listConfiguredChannelsFromConfig: vi.fn(() => []), + readOpenClawConfig: mockReadOpenClawConfig, + writeOpenClawConfig: mockWriteOpenClawConfig, +})); + +vi.mock('@electron/utils/logger', () => ({ + logger: { + info: vi.fn(), + warn: mockLoggerWarn, + error: vi.fn(), + debug: vi.fn(), + }, +})); + describe('stripSystemdSupervisorEnv', () => { it('removes systemd supervisor marker env vars', () => { const env = { @@ -43,3 +78,82 @@ describe('stripSystemdSupervisorEnv', () => { expect(result).toEqual({ VALUE: '1' }); }); }); + +describe('Cloud Sync launch config', () => { + const originalEnv = process.env; + + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + process.env = { ...originalEnv }; + delete process.env.YINIAN_CLOUD_SYNC_ENABLED; + delete process.env.YINIAN_CLOUD_SYNC_SERVER_URL; + delete process.env.CLOUDCLAW_SERVER_URL; + delete process.env.YINIAN_API_BASE_URL; + mockEnsureCloudSyncPluginInstalled.mockReturnValue({ installed: true }); + mockReadOpenClawConfig.mockResolvedValue({}); + mockWriteOpenClawConfig.mockResolvedValue(undefined); + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it('derives the Cloud Sync server URL from explicit and API base env vars', async () => { + const { deriveCloudSyncServerUrl } = await import('@electron/gateway/config-sync'); + + expect(deriveCloudSyncServerUrl()).toBe('https://onefeel.brother7.cn'); + + process.env.YINIAN_API_BASE_URL = 'https://onefeel.brother7.cn/ingress/'; + expect(deriveCloudSyncServerUrl()).toBe('https://onefeel.brother7.cn'); + + process.env.YINIAN_CLOUD_SYNC_SERVER_URL = ' https://cloud.example.com/// '; + expect(deriveCloudSyncServerUrl()).toBe('https://cloud.example.com'); + }); + + it('writes the Cloud Sync plugin entry before Gateway launch', async () => { + process.env.YINIAN_API_BASE_URL = 'https://onefeel.brother7.cn/ingress'; + mockReadOpenClawConfig.mockResolvedValue({ + plugins: { + allow: ['existing-plugin'], + entries: { + 'cloud-sync': { + enabled: false, + config: { keep: 'value' }, + }, + }, + }, + }); + + const { ensureCloudSyncPluginConfigured } = await import('@electron/gateway/config-sync'); + await ensureCloudSyncPluginConfigured(); + + expect(mockEnsureCloudSyncPluginInstalled).toHaveBeenCalled(); + expect(mockWriteOpenClawConfig).toHaveBeenCalledWith({ + plugins: { + enabled: true, + allow: ['existing-plugin', 'cloud-sync'], + entries: { + 'cloud-sync': { + enabled: true, + config: { + keep: 'value', + serverUrl: 'https://onefeel.brother7.cn', + }, + }, + }, + }, + }); + }); + + it('does not touch plugin config when Cloud Sync is disabled', async () => { + process.env.YINIAN_CLOUD_SYNC_ENABLED = '0'; + + const { ensureCloudSyncPluginConfigured } = await import('@electron/gateway/config-sync'); + await ensureCloudSyncPluginConfigured(); + + expect(mockEnsureCloudSyncPluginInstalled).not.toHaveBeenCalled(); + expect(mockReadOpenClawConfig).not.toHaveBeenCalled(); + expect(mockWriteOpenClawConfig).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/unit/cron-store-fetch-dedupe.test.ts b/tests/unit/cron-store-fetch-dedupe.test.ts index 4f732cd..e653c1a 100644 --- a/tests/unit/cron-store-fetch-dedupe.test.ts +++ b/tests/unit/cron-store-fetch-dedupe.test.ts @@ -49,4 +49,28 @@ describe('cron store fetchJobs dedupe', () => { expect(useCronStore.getState().jobs.map((job) => job.id)).toEqual(['job-1']); }); + + it('normalizes object-wrapped job lists', async () => { + hostApiFetchMock.mockResolvedValueOnce({ jobs: [{ id: 'job-2' }] }); + + const { useCronStore } = await import('@/stores/cron'); + useCronStore.setState({ jobs: [], loading: false, error: null }); + + await useCronStore.getState().fetchJobs(); + + expect(useCronStore.getState().jobs.map((job) => job.id)).toEqual(['job-2']); + expect(useCronStore.getState().error).toBeNull(); + }); + + it('surfaces invalid job list responses without throwing map errors', async () => { + hostApiFetchMock.mockResolvedValueOnce({ success: false, error: 'Gateway unavailable' }); + + const { useCronStore } = await import('@/stores/cron'); + useCronStore.setState({ jobs: [], loading: false, error: null }); + + await useCronStore.getState().fetchJobs(); + + expect(useCronStore.getState().jobs).toEqual([]); + expect(useCronStore.getState().error).toContain('Gateway unavailable'); + }); }); diff --git a/tests/unit/gateway-ready-fallback.test.ts b/tests/unit/gateway-ready-fallback.test.ts index 8cd9596..8b939e9 100644 --- a/tests/unit/gateway-ready-fallback.test.ts +++ b/tests/unit/gateway-ready-fallback.test.ts @@ -41,12 +41,8 @@ describe('GatewayManager gatewayReady fallback', () => { statusUpdates.push({ gatewayReady: status.gatewayReady }); }); - // Simulate start attempt (will fail but we can check the initial status) - try { - await manager.start(); - } catch { - // expected to fail — no actual gateway process - } + const stateController = (manager as unknown as { stateController: { setStatus: (u: Record) => void } }).stateController; + stateController.setStatus({ state: 'starting', gatewayReady: false }); const startingUpdate = statusUpdates.find((u) => u.gatewayReady === false); expect(startingUpdate).toBeDefined(); diff --git a/tests/unit/knowledge-routes.test.ts b/tests/unit/knowledge-routes.test.ts new file mode 100644 index 0000000..23f6c75 --- /dev/null +++ b/tests/unit/knowledge-routes.test.ts @@ -0,0 +1,217 @@ +import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'; +import crypto from 'node:crypto'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +let userDataDir = ''; + +vi.mock('@electron/utils/paths', () => ({ + getDataDir: () => userDataDir, +})); + +describe('knowledge routes', () => { + beforeEach(async () => { + vi.resetModules(); + userDataDir = await mkdtemp(join(tmpdir(), 'zhinian-knowledge-test-')); + }); + + afterEach(async () => { + await rm(userDataDir, { recursive: true, force: true }); + }); + + it('copies supported text files into the app knowledge directory', async () => { + const sourceDir = await mkdtemp(join(tmpdir(), 'zhinian-knowledge-source-')); + const sourcePath = join(sourceDir, 'faq.md'); + await writeFile(sourcePath, '# FAQ\n\nhello', 'utf8'); + + const { importKnowledgeFiles } = await import('@electron/api/routes/knowledge'); + const result = await importKnowledgeFiles({ + workspaceId: 'workspace/test', + filePaths: [sourcePath], + }); + + expect(result.rejected).toEqual([]); + expect(result.documents).toHaveLength(1); + expect(result.documents[0].name).toBe('faq.md'); + expect(result.documents[0].mimeType).toBe('text/markdown'); + expect(result.documents[0].storedPath).not.toBe(sourcePath); + await expect(stat(result.documents[0].storedPath)).resolves.toBeTruthy(); + await expect(readFile(result.documents[0].storedPath, 'utf8')).resolves.toContain('hello'); + + const registryPath = join(userDataDir, 'yinian', 'knowledge', 'workspace_test', 'registry.json'); + await expect(readFile(registryPath, 'utf8')).resolves.toContain('faq.md'); + + await rm(sourceDir, { recursive: true, force: true }); + }); + + it('builds chat context from the local backup after the source file is deleted', async () => { + const sourceDir = await mkdtemp(join(tmpdir(), 'zhinian-knowledge-source-')); + const sourcePath = join(sourceDir, 'policy.md'); + await writeFile(sourcePath, '# Policy\n\nRefund within 7 days.', 'utf8'); + + const { importKnowledgeFiles, buildKnowledgeContext } = await import('@electron/api/routes/knowledge'); + const imported = await importKnowledgeFiles({ + workspaceId: 'workspace-context', + filePaths: [sourcePath], + }); + await rm(sourceDir, { recursive: true, force: true }); + + const context = await buildKnowledgeContext({ + workspaceId: 'workspace-context', + documentIds: [imported.documents[0].id], + }); + + expect(context.missing).toEqual([]); + expect(context.documents[0].name).toBe('policy.md'); + expect(context.context).toContain('[知识库上下文]'); + expect(context.context).toContain('Refund within 7 days.'); + }); + + it('builds chat context from extracted docx text', async () => { + const sourceDir = await mkdtemp(join(tmpdir(), 'zhinian-knowledge-source-')); + const sourcePath = join(sourceDir, 'handbook.docx'); + await writeMinimalDocx(sourcePath, 'Use the local backup for answers.'); + + const { importKnowledgeFiles, buildKnowledgeContext } = await import('@electron/api/routes/knowledge'); + const imported = await importKnowledgeFiles({ + workspaceId: 'workspace-docx-context', + filePaths: [sourcePath], + }); + + const context = await buildKnowledgeContext({ + workspaceId: 'workspace-docx-context', + documentIds: [imported.documents[0].id], + }); + + expect(context.context).toContain('handbook.docx'); + expect(context.context).toContain('Use the local backup for answers.'); + + await rm(sourceDir, { recursive: true, force: true }); + }); + + it('rejects unsupported binary-looking files', async () => { + const sourceDir = await mkdtemp(join(tmpdir(), 'zhinian-knowledge-source-')); + const sourcePath = join(sourceDir, 'image.png'); + await writeFile(sourcePath, 'not really an image', 'utf8'); + + const { importKnowledgeFiles } = await import('@electron/api/routes/knowledge'); + const result = await importKnowledgeFiles({ + workspaceId: 'workspace-test', + filePaths: [sourcePath], + }); + + expect(result.documents).toEqual([]); + expect(result.rejected).toEqual([{ filePath: sourcePath, reason: '仅支持文本类知识文件' }]); + + await rm(sourceDir, { recursive: true, force: true }); + }); + + it('rejects missing files without changing the registry', async () => { + const missingPath = join(tmpdir(), `missing-${crypto.randomUUID()}.md`); + + const { importKnowledgeFiles } = await import('@electron/api/routes/knowledge'); + const result = await importKnowledgeFiles({ + workspaceId: 'workspace-missing', + filePaths: [missingPath], + }); + + expect(result.documents).toEqual([]); + expect(result.rejected).toEqual([{ filePath: missingPath, reason: '文件不存在或不可读取' }]); + + const registryPath = join(userDataDir, 'yinian', 'knowledge', 'workspace-missing', 'registry.json'); + await expect(readFile(registryPath, 'utf8')).rejects.toThrow(); + }); + + it('keeps newer imports at the top of the local registry', async () => { + const sourceDir = await mkdtemp(join(tmpdir(), 'zhinian-knowledge-source-')); + const oldPath = join(sourceDir, 'old.md'); + const newPath = join(sourceDir, 'new.md'); + await writeFile(oldPath, 'old knowledge', 'utf8'); + await writeFile(newPath, 'new knowledge', 'utf8'); + + const { importKnowledgeFiles } = await import('@electron/api/routes/knowledge'); + await importKnowledgeFiles({ workspaceId: 'workspace-order', filePaths: [oldPath] }); + await importKnowledgeFiles({ workspaceId: 'workspace-order', filePaths: [newPath] }); + + const registryPath = join(userDataDir, 'yinian', 'knowledge', 'workspace-order', 'registry.json'); + const registry = JSON.parse(await readFile(registryPath, 'utf8')) as Array<{ name: string }>; + expect(registry.map((doc) => doc.name)).toEqual(['new.md', 'old.md']); + + await rm(sourceDir, { recursive: true, force: true }); + }); + + it('copies docx files and extracts searchable text', async () => { + const sourceDir = await mkdtemp(join(tmpdir(), 'zhinian-knowledge-source-')); + const sourcePath = join(sourceDir, 'handbook.docx'); + await writeMinimalDocx(sourcePath, 'Welcome to Zhinian Handbook'); + + const { importKnowledgeFiles } = await import('@electron/api/routes/knowledge'); + const result = await importKnowledgeFiles({ + workspaceId: 'workspace-docx', + filePaths: [sourcePath], + }); + + expect(result.rejected).toEqual([]); + expect(result.documents).toHaveLength(1); + expect(result.documents[0].name).toBe('handbook.docx'); + expect(result.documents[0].mimeType).toBe('application/vnd.openxmlformats-officedocument.wordprocessingml.document'); + expect(result.documents[0].storedPath).not.toBe(sourcePath); + expect(result.documents[0].textPath).toBeTruthy(); + await expect(readFile(result.documents[0].textPath!, 'utf8')).resolves.toContain('Welcome to Zhinian Handbook'); + + await rm(sourceDir, { recursive: true, force: true }); + }); + + it('rejects invalid docx files before adding them to the registry', async () => { + const sourceDir = await mkdtemp(join(tmpdir(), 'zhinian-knowledge-source-')); + const sourcePath = join(sourceDir, 'broken.docx'); + await writeFile(sourcePath, 'not a zip document', 'utf8'); + + const { importKnowledgeFiles } = await import('@electron/api/routes/knowledge'); + const result = await importKnowledgeFiles({ + workspaceId: 'workspace-docx-broken', + filePaths: [sourcePath], + }); + + expect(result.documents).toEqual([]); + expect(result.rejected).toEqual([{ filePath: sourcePath, reason: 'Word 文档解析失败,请确认文件为 .docx 格式' }]); + + const registryPath = join(userDataDir, 'yinian', 'knowledge', 'workspace-docx-broken', 'registry.json'); + await expect(readFile(registryPath, 'utf8')).rejects.toThrow(); + + await rm(sourceDir, { recursive: true, force: true }); + }); +}); + +async function writeMinimalDocx(filePath: string, text: string) { + const JSZip = (await import('jszip')).default; + const zip = new JSZip(); + zip.file('[Content_Types].xml', ` + + + + +`); + zip.folder('_rels')?.file('.rels', ` + + +`); + zip.folder('word')?.file('document.xml', ` + + + ${escapeXml(text)} + +`); + const buffer = await zip.generateAsync({ type: 'nodebuffer' }); + await writeFile(filePath, buffer); +} + +function escapeXml(value: string): string { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} diff --git a/tests/unit/launch-at-startup.test.ts b/tests/unit/launch-at-startup.test.ts index 432c7b4..3b43e46 100644 --- a/tests/unit/launch-at-startup.test.ts +++ b/tests/unit/launch-at-startup.test.ts @@ -70,12 +70,12 @@ describe('launch-at-startup integration', () => { setPlatform('linux'); const { applyLaunchAtStartupSetting } = await import('@electron/main/launch-at-startup'); - const autostartPath = join(testHome, '.config', 'autostart', 'clawx.desktop'); + const autostartPath = join(testHome, '.config', 'autostart', 'zhinian-assistant.desktop'); await applyLaunchAtStartupSetting(true); const content = await readFile(autostartPath, 'utf8'); expect(content).toContain('[Desktop Entry]'); - expect(content).toContain('Name=ClawX'); + expect(content).toContain('Name=智念助手'); expect(content).toContain('Exec='); await applyLaunchAtStartupSetting(false); diff --git a/tests/unit/today-page.test.tsx b/tests/unit/today-page.test.tsx new file mode 100644 index 0000000..1487e7d --- /dev/null +++ b/tests/unit/today-page.test.tsx @@ -0,0 +1,206 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { Today } from '@/pages/Today'; +import { useYinianStore } from '@/stores/yinian'; +import { useYinianSkillsStore } from '@/stores/yinian-skills'; +import { useSkillsStore } from '@/stores/skills'; +import type { YinianConfigSnapshot, YinianLocalSkill } from '../../shared/yinian'; + +const hotelHangzhou = { + id: 'workspace_hangzhou_ops', + name: '智念企业组织空间', + brand: '智念', + city: '杭州', + role: 'manager' as const, + permissions: ['dashboard.read', 'skills.sync'], + ota: [], +}; + +const hotelShanghai = { + id: 'workspace_shanghai_growth', + name: '智念增长组织空间', + city: '上海', + role: 'viewer' as const, + permissions: ['dashboard.read'], + ota: [], +}; + +function createConfig(overrides: Partial = {}): YinianConfigSnapshot { + return { + serverTime: 1, + user: { id: 'user_1', name: '王管理员' }, + hotel: hotelHangzhou, + hotels: [hotelHangzhou, hotelShanghai], + entitlements: [], + notificationChannels: [], + featureFlags: {}, + uiPolicy: { defaultPage: 'today', showAdvancedSettings: false }, + ...overrides, + }; +} + +function createLocalSkill(overrides: Partial): YinianLocalSkill { + return { + skillId: 'daily-report', + name: '日报生成助手', + version: '1.0.0', + enabled: true, + installedAt: 1, + lastSyncedAt: Date.now() - 5 * 60_000, + status: 'installed', + source: 'mock', + ...overrides, + }; +} + +describe('Today page', () => { + beforeEach(() => { + vi.clearAllMocks(); + useYinianStore.setState({ + status: 'authenticated', + session: { + authenticated: true, + user: { id: 'user_1', name: '王管理员' }, + hotels: [hotelHangzhou, hotelShanghai], + currentHotelId: hotelHangzhou.id, + accessTokenExpiresAt: 100, + }, + config: createConfig(), + error: null, + }); + useYinianSkillsStore.setState({ + localSkills: [], + lastSync: null, + loading: false, + error: null, + }); + useSkillsStore.setState({ + skills: [], + loading: false, + error: null, + }); + }); + + it('shows quick actions and plain status cards when workspace has no entitlements', () => { + render(); + + expect(screen.getAllByText('智念企业组织空间').length).toBeGreaterThan(0); + expect(screen.getByText('开始对话')).toBeInTheDocument(); + expect(screen.getByText('管理知识库')).toBeInTheDocument(); + expect(screen.getByText('管理应用')).toBeInTheDocument(); + expect(screen.queryByText('0/0')).not.toBeInTheDocument(); + expect(screen.getAllByText('0 个').length).toBeGreaterThan(0); + expect(screen.queryByText('需要关注')).not.toBeInTheDocument(); + expect(screen.queryByText('最近会话')).not.toBeInTheDocument(); + }); + + it('summarizes entitlements with local registry readiness', () => { + useYinianStore.setState({ + config: createConfig({ + entitlements: [ + { + skillId: 'daily-report', + name: '日报生成助手', + version: '1.0.0', + enabled: true, + category: 'reporting', + triggers: ['scheduled', 'manual'], + lastRunAt: Date.now() - 60 * 60_000, + }, + { + skillId: 'data-check', + name: '数据检查助手', + version: '1.2.0', + enabled: true, + category: 'ota-monitoring', + triggers: ['scheduled'], + }, + { + skillId: 'customer-reply-helper', + name: '客户回复助手', + version: '0.9.0', + enabled: false, + category: 'guest-comm', + triggers: ['manual'], + }, + ], + notificationChannels: [ + { + id: 'ops', + kind: 'wecom', + label: '业务通知群', + recipient: 'room_1', + enabled: true, + source: 'nianxx', + }, + ], + }), + }); + useYinianSkillsStore.setState({ + localSkills: [ + createLocalSkill({ skillId: 'daily-report', name: '日报生成助手', version: '1.0.0', status: 'skipped' }), + createLocalSkill({ skillId: 'data-check', name: '数据检查助手', version: '1.1.0', status: 'installed' }), + ], + }); + useSkillsStore.setState({ + skills: [ + { + id: 'daily-report', + name: '日报生成助手', + description: '', + enabled: true, + isCore: false, + source: 'openclaw-managed', + }, + { + id: 'data-check', + name: '数据检查助手', + description: '', + enabled: true, + isCore: false, + source: 'openclaw-managed', + }, + ], + }); + + render(); + + expect(screen.getByText('管理应用')).toBeInTheDocument(); + expect(screen.queryByText('2/2')).not.toBeInTheDocument(); + expect(screen.queryByText('下发 2 个 · 本地 2 个')).not.toBeInTheDocument(); + expect(screen.getAllByText('2 个').length).toBeGreaterThanOrEqual(2); + expect(screen.queryByText(/待处理 1 个/)).not.toBeInTheDocument(); + expect(screen.getAllByText(/日报生成助手/).length).toBeGreaterThan(0); + expect(screen.queryByText('数据检查助手 有新版本')).not.toBeInTheDocument(); + }); + + it('updates displayed workspace and counters after hotel switch refreshes config', () => { + const { rerender } = render(); + expect(screen.getAllByText('智念企业组织空间').length).toBeGreaterThan(0); + + useYinianStore.setState({ + config: createConfig({ + hotel: hotelShanghai, + entitlements: [ + { + skillId: 'customer-reply-helper', + name: '客户回复助手', + version: '0.9.0', + enabled: true, + category: 'guest-comm', + triggers: ['manual'], + }, + ], + }), + }); + useYinianSkillsStore.setState({ localSkills: [] }); + + rerender(); + + expect(screen.getAllByText('智念增长组织空间').length).toBeGreaterThan(0); + expect(screen.queryByText('1/0')).not.toBeInTheDocument(); + expect(screen.getByText('客户回复助手')).toBeInTheDocument(); + expect(screen.queryByText(/待处理 1 个/)).not.toBeInTheDocument(); + expect(screen.queryByText('智念企业组织空间')).not.toBeInTheDocument(); + }); +}); diff --git a/tests/unit/yinian-control-plane.test.ts b/tests/unit/yinian-control-plane.test.ts new file mode 100644 index 0000000..1998088 --- /dev/null +++ b/tests/unit/yinian-control-plane.test.ts @@ -0,0 +1,601 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { MockYinianControlPlane } from '@electron/yinian/mock-control-plane'; +import { createMemoryYinianStorage } from '@electron/yinian/storage'; +import { HttpYinianControlPlane } from '@electron/yinian/http-control-plane'; +import { getYinianControlPlane, resetYinianControlPlaneForTests } from '@electron/yinian/control-plane'; +import { yinianContractFixtures } from '../fixtures/yinian-server-contract'; + +type RouteKey = `${string} ${string}`; + +function jsonResponse(body: unknown, init: ResponseInit = {}): Response { + return new Response(JSON.stringify(body), { + status: init.status ?? 200, + headers: { + 'Content-Type': 'application/json', + ...(init.headers ?? {}), + }, + }); +} + +function installFetchRoutes(routes: Partial unknown)>>) { + const fetchMock = vi.fn(async (input: string | URL | Request, init?: RequestInit) => { + const requestUrl = typeof input === 'string' + ? input + : input instanceof URL + ? input.toString() + : input.url; + const url = new URL(requestUrl); + const method = init?.method ?? (input instanceof Request ? input.method : 'GET'); + const key = `${method} ${url.pathname}` as RouteKey; + const route = routes[key]; + if (!route) { + return jsonResponse({ + error: { + code: 'NOT_FOUND', + message: `Unexpected request: ${key}`, + }, + }, { status: 404 }); + } + return jsonResponse(typeof route === 'function' ? route() : route); + }); + vi.stubGlobal('fetch', fetchMock); + return fetchMock; +} + +afterEach(() => { + vi.unstubAllGlobals(); + vi.unstubAllEnvs(); + resetYinianControlPlaneForTests(); +}); + +describe('MockYinianControlPlane', () => { + it('persists saved login credentials in the YINIAN storage namespace', async () => { + const storage = createMemoryYinianStorage(); + await storage.setSavedCredentials({ + account: '15685275886-1', + password: '123456', + rememberPassword: true, + updatedAt: 1, + }); + + await expect(storage.getSavedCredentials()).resolves.toEqual({ + account: '15685275886-1', + password: '123456', + rememberPassword: true, + updatedAt: 1, + }); + + await storage.clearSavedCredentials(); + await expect(storage.getSavedCredentials()).resolves.toBeUndefined(); + }); + + it('starts unauthenticated and logs in with password', async () => { + const controlPlane = new MockYinianControlPlane({ storage: createMemoryYinianStorage() }); + + await expect(controlPlane.getSessionState()).resolves.toEqual({ authenticated: false }); + + const session = await controlPlane.loginWithPassword({ + account: 'admin', + password: 'demo-password', + }); + + expect(session.authenticated).toBe(true); + expect(session.user.name).toBe('NIANXX 实施'); + expect(session.hotels).toHaveLength(2); + }); + + it('switches workspace and returns matching config snapshot', async () => { + const controlPlane = new MockYinianControlPlane({ storage: createMemoryYinianStorage() }); + await controlPlane.loginWithPassword({ account: 'ops@example.com', password: 'secret' }); + + const switched = await controlPlane.switchHotel('workspace_shanghai_growth'); + expect(switched.currentHotelId).toBe('workspace_shanghai_growth'); + + const config = await controlPlane.getConfigSnapshot(); + expect(config.hotel.name).toBe('智念增长组织空间'); + expect(config.entitlements.map((skill) => skill.skillId)).toContain('daily-report'); + }); + + it('syncs entitled skills into local registry', async () => { + const controlPlane = new MockYinianControlPlane({ storage: createMemoryYinianStorage() }); + await controlPlane.loginWithPassword({ account: 'ops@example.com', password: 'secret' }); + + const firstSync = await controlPlane.syncSkills(); + expect(firstSync.skills).toHaveLength(4); + expect(firstSync.skills.every((skill) => skill.status === 'installed')).toBe(true); + + const secondSync = await controlPlane.syncSkills(); + expect(secondSync.skills.every((skill) => skill.status === 'skipped')).toBe(true); + + const localSkills = await controlPlane.listLocalSkills(); + expect(localSkills.map((skill) => skill.skillId)).toContain('data-check'); + }); + + it('restores mock session and registry from storage', async () => { + const storage = createMemoryYinianStorage(); + const firstControlPlane = new MockYinianControlPlane({ storage }); + await firstControlPlane.loginWithPassword({ account: 'admin', password: 'demo-password' }); + await firstControlPlane.syncSkills(); + + const restoredControlPlane = new MockYinianControlPlane({ storage }); + const session = await restoredControlPlane.restoreSession(); + expect(session.authenticated).toBe(true); + if (!session.authenticated) return; + expect(session.currentHotelId).toBe('workspace_hangzhou_ops'); + + const localSkills = await restoredControlPlane.listLocalSkills(); + expect(localSkills).toHaveLength(4); + }); + + it('clears session and current workspace skill registry on logout', async () => { + const storage = createMemoryYinianStorage(); + const controlPlane = new MockYinianControlPlane({ storage }); + await controlPlane.loginWithSms({ phone: '13800000000', code: '123456' }); + await controlPlane.syncSkills(); + + await controlPlane.logout(); + + await expect(controlPlane.restoreSession()).resolves.toEqual({ authenticated: false }); + expect(await controlPlane.getSkillRegistry('workspace_hangzhou_ops')).toBeUndefined(); + }); + + it('rejects config access before login', async () => { + const controlPlane = new MockYinianControlPlane({ storage: createMemoryYinianStorage() }); + + await expect(controlPlane.getConfigSnapshot()).rejects.toThrow('请先登录'); + }); +}); + +describe('HttpYinianControlPlane', () => { + it('logs in and normalizes contract v0 config responses', async () => { + const storage = createMemoryYinianStorage(); + const controlPlane = new HttpYinianControlPlane({ + apiBaseUrl: 'https://api.example.test/', + storage, + }); + const fetchMock = installFetchRoutes({ + 'POST /auth/oauth2/token': yinianContractFixtures.login, + 'GET /config/sync': yinianContractFixtures.config, + }); + + const session = await controlPlane.loginWithPassword({ + account: 'ops@example.com', + password: 'secret', + captchaCode: '5678nianxx', + randomStr: '333e6825-760c-4c1a-8b56-eb9539b43dbd', + }); + expect(session.authenticated).toBe(true); + expect(session.user.name).toBe('王管理员'); + expect(session.currentHotelId).toBe('workspace_hangzhou_ops'); + expect(session.accessTokenExpiresAt).toBe(1777188600000); + + const persisted = await storage.getSession(); + expect(persisted?.mode).toBe('http'); + expect(persisted?.refreshToken).toBe('refresh_demo'); + + const config = await controlPlane.getConfigSnapshot(); + expect(config.serverTime).toBe(1777188000000); + expect(config.hotel.id).toBe('workspace_hangzhou_ops'); + expect(config.featureFlags).toEqual({ + skillsSync: true, + advancedSettings: false, + }); + expect(config.uiPolicy).toEqual({ + defaultPage: 'today', + showAdvancedSettings: true, + }); + expect(config.entitlements[0]).toMatchObject({ + skillId: 'daily-report', + category: 'reporting', + triggers: ['scheduled', 'manual'], + lastRunAt: 1777184400000, + }); + expect(config.entitlements[1]).toMatchObject({ + skillId: 'unknown-category-skill', + category: 'ops-automation', + }); + expect(config.notificationChannels.map((channel) => channel.source)).toEqual(['nianxx', 'kernel']); + expect(fetchMock).not.toHaveBeenCalledWith('https://api.example.test/auth/me', expect.anything()); + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.example.test/auth/oauth2/token', + expect.objectContaining({ + method: 'POST', + headers: expect.objectContaining({ + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: 'Basic Y3VzdG9tUEM6Y3VzdG9tUEM=', + }), + body: expect.stringContaining('grant_type=password'), + }), + ); + const tokenCall = fetchMock.mock.calls.find(([url]) => url === 'https://api.example.test/auth/oauth2/token'); + const tokenBody = new URLSearchParams(String(tokenCall?.[1]?.body ?? '')); + expect(Object.fromEntries(tokenBody)).toMatchObject({ + grant_type: 'password', + scope: 'server', + username: 'ops@example.com', + password: 'secret', + randomStr: '333e6825-760c-4c1a-8b56-eb9539b43dbd', + code: '5678nianxx', + }); + expect(tokenBody.has('clientId')).toBe(false); + expect(tokenBody.has('client_id')).toBe(false); + }); + + it('syncs manifest into workspace registry and marks same-version sync as skipped', async () => { + const storage = createMemoryYinianStorage(); + const controlPlane = new HttpYinianControlPlane({ + apiBaseUrl: 'https://api.example.test', + storage, + }); + installFetchRoutes({ + 'POST /auth/oauth2/token': yinianContractFixtures.login, + 'GET /auth/me': yinianContractFixtures.me, + 'GET /skills/manifest': yinianContractFixtures.manifest, + }); + + await controlPlane.loginWithPassword({ account: 'ops@example.com', password: 'secret' }); + + const firstSync = await controlPlane.syncSkills(); + expect(firstSync.skills.map((skill) => skill.status)).toEqual(['installed', 'disabled']); + expect(firstSync.skills[0]).toMatchObject({ + skillId: 'data-check', + bundleSha256: 'sha256-demo', + source: 'nianxx', + }); + + const secondSync = await controlPlane.syncSkills(); + expect(secondSync.skills.map((skill) => skill.status)).toEqual(['skipped', 'disabled']); + + const registry = await controlPlane.getSkillRegistry('workspace_hangzhou_ops'); + expect(registry && 'skills' in registry ? registry.skills : []).toHaveLength(2); + }); + + it('supports configurable enterprise app manifest endpoints and app-shaped fields', async () => { + vi.stubEnv('YINIAN_SKILLS_MANIFEST_PATH', '/enterprise/spaces/{workspaceId}/apps?includeBundle=true'); + const storage = createMemoryYinianStorage(); + const controlPlane = new HttpYinianControlPlane({ + apiBaseUrl: 'https://api.example.test', + storage, + }); + const fetchMock = installFetchRoutes({ + 'POST /auth/oauth2/token': yinianContractFixtures.login, + 'GET /enterprise/spaces/workspace_hangzhou_ops/apps': { + data: { + records: [ + { + appId: 'daily-report', + appName: '日报生成助手', + currentVersion: '2.1.0', + status: 'enabled', + bundleHash: 'sha256-cloudclaw-demo', + }, + { + app_id: 'disabled-helper', + app_name: '停用应用', + app_version: '1.0.0', + status: 'disabled', + }, + ], + }, + }, + }); + + await controlPlane.loginWithPassword({ account: 'ops@example.com', password: 'secret' }); + const result = await controlPlane.syncSkills(); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.example.test/enterprise/spaces/workspace_hangzhou_ops/apps?includeBundle=true', + expect.anything(), + ); + expect(result.skills).toEqual([ + expect.objectContaining({ + skillId: 'daily-report', + name: '日报生成助手', + version: '2.1.0', + status: 'installed', + bundleSha256: 'sha256-cloudclaw-demo', + }), + expect.objectContaining({ + skillId: 'disabled-helper', + name: '停用应用', + status: 'disabled', + }), + ]); + }); + + it('stores an empty registry when the enterprise app manifest endpoint is not implemented yet', async () => { + const storage = createMemoryYinianStorage(); + const controlPlane = new HttpYinianControlPlane({ + apiBaseUrl: 'https://api.example.test', + storage, + }); + vi.stubGlobal('fetch', vi.fn(async (input: string | URL | Request, init?: RequestInit) => { + const requestUrl = typeof input === 'string' + ? input + : input instanceof URL + ? input.toString() + : input.url; + const url = new URL(requestUrl); + const method = init?.method ?? (input instanceof Request ? input.method : 'GET'); + if (method === 'POST' && url.pathname === '/auth/oauth2/token') { + return jsonResponse(yinianContractFixtures.login); + } + if (method === 'GET' && url.pathname === '/skills/manifest') { + return jsonResponse({ message: 'No static resource skills/manifest.' }, { status: 404 }); + } + return jsonResponse({ message: `Unexpected request: ${method} ${url.pathname}` }, { status: 404 }); + })); + + await controlPlane.loginWithPassword({ account: 'ops@example.com', password: 'secret' }); + const result = await controlPlane.syncSkills(); + + expect(result.skills).toEqual([]); + const registry = await controlPlane.getSkillRegistry('workspace_hangzhou_ops'); + expect(registry && 'skills' in registry ? registry.skills : []).toEqual([]); + }); + + it('restores an HTTP session through refresh token exchange', async () => { + const storage = createMemoryYinianStorage(); + await storage.setSession({ + mode: 'http', + user: { id: 'old_user', name: '旧用户' }, + hotels: [], + currentHotelId: 'workspace_hangzhou_ops', + accessTokenExpiresAt: 1, + refreshToken: 'refresh_demo', + updatedAt: 1, + }); + const controlPlane = new HttpYinianControlPlane({ + apiBaseUrl: 'https://api.example.test', + storage, + }); + installFetchRoutes({ + 'POST /auth/oauth2/token': yinianContractFixtures.refresh, + }); + + const session = await controlPlane.restoreSession(); + expect(session.authenticated).toBe(true); + if (!session.authenticated) return; + expect(session.user.id).toBe('user_ops_001'); + expect((await storage.getSession())?.refreshToken).toBe('refresh_rotated'); + }); + + it('clears persisted HTTP session when refresh fails', async () => { + const storage = createMemoryYinianStorage(); + await storage.setSession({ + mode: 'http', + user: { id: 'old_user', name: '旧用户' }, + hotels: [], + currentHotelId: 'workspace_hangzhou_ops', + accessTokenExpiresAt: 1, + refreshToken: 'expired_refresh', + updatedAt: 1, + }); + const controlPlane = new HttpYinianControlPlane({ + apiBaseUrl: 'https://api.example.test', + storage, + }); + vi.stubGlobal('fetch', vi.fn(async () => jsonResponse(yinianContractFixtures.error, { status: 401 }))); + + await expect(controlPlane.restoreSession()).resolves.toEqual({ authenticated: false }); + await expect(storage.getSession()).resolves.toBeUndefined(); + }); + + it('surfaces server error messages', async () => { + const controlPlane = new HttpYinianControlPlane({ + apiBaseUrl: 'https://api.example.test', + storage: createMemoryYinianStorage(), + }); + vi.stubGlobal('fetch', vi.fn(async () => jsonResponse(yinianContractFixtures.error, { status: 401 }))); + + await expect(controlPlane.loginWithPassword({ + account: 'ops@example.com', + password: 'bad-secret', + })).rejects.toThrow('Session expired'); + }); + + it('turns network failures into readable connection errors', async () => { + const controlPlane = new HttpYinianControlPlane({ + apiBaseUrl: 'https://onefeel.brother7.cn/ingress', + storage: createMemoryYinianStorage(), + }); + vi.stubGlobal('fetch', vi.fn(async () => { + throw new TypeError('fetch failed', { + cause: Object.assign(new Error('getaddrinfo ENOTFOUND onefeel.brother7.cn'), { + code: 'ENOTFOUND', + syscall: 'getaddrinfo', + hostname: 'onefeel.brother7.cn', + }), + }); + })); + + await expect(controlPlane.loginWithPassword({ + account: 'ops@example.com', + password: 'secret', + })).rejects.toThrow('域名 onefeel.brother7.cn 解析失败'); + }); + + it('does not send SMS login requests in HTTP mode', async () => { + const controlPlane = new HttpYinianControlPlane({ + apiBaseUrl: 'https://api.example.test', + storage: createMemoryYinianStorage(), + }); + const fetchMock = vi.fn(); + vi.stubGlobal('fetch', fetchMock); + + await expect(controlPlane.loginWithSms({ phone: '13800000000', code: '123456' })) + .rejects.toThrow('仅支持账号密码登录'); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it('accepts data-wrapped login responses with session payload', async () => { + const storage = createMemoryYinianStorage(); + const controlPlane = new HttpYinianControlPlane({ + apiBaseUrl: 'https://api.example.test', + storage, + }); + const fetchMock = installFetchRoutes({ + 'POST /auth/oauth2/token': { + success: true, + data: { + access_token: 'access_wrapped', + refresh_token: 'refresh_wrapped', + expires_in: 3600, + user: yinianContractFixtures.me.user, + workspaces: yinianContractFixtures.me.hotels, + current_workspace_id: 'workspace_shanghai_growth', + }, + }, + }); + + const session = await controlPlane.loginWithPassword({ + account: 'ops@example.com', + password: 'secret', + }); + + expect(session.authenticated).toBe(true); + expect(session.currentHotelId).toBe('workspace_shanghai_growth'); + expect(session.hotels).toHaveLength(2); + expect((await storage.getSession())?.refreshToken).toBe('refresh_wrapped'); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it('accepts oauth token responses with user_info and no workspace list', async () => { + const storage = createMemoryYinianStorage(); + const controlPlane = new HttpYinianControlPlane({ + apiBaseUrl: 'https://api.example.test', + storage, + }); + installFetchRoutes({ + 'POST /auth/oauth2/token': { + access_token: 'access_onefeel', + refresh_token: 'refresh_onefeel', + expires_in: '9999', + username: '15685275886-1', + user_info: { + id: '1965411380338970626', + username: '15685275886-1', + tenantId: '1', + name: '15685275886-1', + }, + }, + }); + + const session = await controlPlane.loginWithPassword({ + account: '15685275886-1', + password: '123456', + captchaCode: '5678nianxx', + randomStr: '333e6825-760c-4c1a-8b56-eb9539b43dbd', + }); + + expect(session.authenticated).toBe(true); + expect(session.user.id).toBe('1965411380338970626'); + expect(session.hotels).toEqual([ + expect.objectContaining({ + id: 'service_1', + name: '15685275886-1的组织空间', + }), + ]); + expect(session.currentHotelId).toBe('service_1'); + expect(session.accessTokenExpiresAt).toBeGreaterThan(Date.now()); + expect((await storage.getSession())?.refreshToken).toBe('refresh_onefeel'); + }); + + it('reports HTTP server status through health endpoint', async () => { + const controlPlane = new HttpYinianControlPlane({ + apiBaseUrl: 'https://api.example.test', + storage: createMemoryYinianStorage(), + }); + installFetchRoutes({ + 'GET /health': { + data: { + server_time: 1777188000000, + version: '2026.04.26', + message: 'ok', + }, + }, + }); + + await expect(controlPlane.getServerStatus()).resolves.toMatchObject({ + mode: 'http', + apiBaseUrl: 'https://api.example.test', + reachable: true, + serverTime: 1777188000000, + version: '2026.04.26', + message: 'ok', + }); + }); + + it('loads image captcha with randomStr', async () => { + const controlPlane = new HttpYinianControlPlane({ + apiBaseUrl: 'https://api.example.test', + storage: createMemoryYinianStorage(), + }); + installFetchRoutes({ + 'GET /auth/code/image': { + data: { + randomStr: 'captcha-key', + imageBase64: 'aW1hZ2U=', + mimeType: 'image/png', + }, + }, + }); + + await expect(controlPlane.createImageCaptcha('captcha-key')).resolves.toMatchObject({ + randomStr: 'captcha-key', + imageBase64: 'aW1hZ2U=', + mimeType: 'image/png', + }); + }); + + it('loads binary image captcha responses', async () => { + const controlPlane = new HttpYinianControlPlane({ + apiBaseUrl: 'https://api.example.test', + storage: createMemoryYinianStorage(), + }); + vi.stubGlobal('fetch', vi.fn(async () => new Response(new Uint8Array([137, 80, 78, 71]), { + status: 200, + headers: { + 'Content-Type': 'image/png', + }, + }))); + + await expect(controlPlane.createImageCaptcha('captcha-key')).resolves.toMatchObject({ + randomStr: 'captcha-key', + imageBase64: 'iVBORw==', + mimeType: 'image/png', + }); + }); +}); + +describe('getYinianControlPlane', () => { + it('uses the configured default server instead of mock mode when env URL is missing', async () => { + vi.stubEnv('YINIAN_API_BASE_URL', ''); + vi.stubEnv('YINIAN_CONTROL_PLANE_MODE', ''); + vi.stubEnv('CLAWX_E2E', ''); + installFetchRoutes({ + 'GET /ingress/health': { serverTime: 1700000000000, version: 'test' }, + }); + + const controlPlane = getYinianControlPlane(); + + await expect(controlPlane.getServerStatus()).resolves.toMatchObject({ + mode: 'http', + apiBaseUrl: 'https://onefeel.brother7.cn/ingress', + reachable: true, + }); + }); + + it('uses mock mode only when explicitly requested', async () => { + vi.stubEnv('YINIAN_API_BASE_URL', ''); + vi.stubEnv('YINIAN_CONTROL_PLANE_MODE', 'mock'); + vi.stubEnv('CLAWX_E2E', ''); + + const controlPlane = getYinianControlPlane(); + + await expect(controlPlane.getServerStatus()).resolves.toMatchObject({ + mode: 'mock', + reachable: true, + }); + }); +}); diff --git a/tests/unit/yinian-skills-page.test.tsx b/tests/unit/yinian-skills-page.test.tsx new file mode 100644 index 0000000..3aa9b82 --- /dev/null +++ b/tests/unit/yinian-skills-page.test.tsx @@ -0,0 +1,264 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { YinianSkills } from '@/pages/YinianSkills'; +import { useYinianStore } from '@/stores/yinian'; +import { useYinianSkillsStore } from '@/stores/yinian-skills'; +import { useSkillsStore } from '@/stores/skills'; +import type { YinianConfigSnapshot, YinianLocalSkill } from '../../shared/yinian'; + +const toastSuccessMock = vi.fn(); +const toastErrorMock = vi.fn(); + +vi.mock('sonner', () => ({ + toast: { + success: (...args: unknown[]) => toastSuccessMock(...args), + error: (...args: unknown[]) => toastErrorMock(...args), + }, +})); + +const hotelHangzhou = { + id: 'workspace_hangzhou_ops', + name: '智念企业组织空间', + city: '杭州', + role: 'manager' as const, + permissions: ['skills.sync'], + ota: [], +}; + +const hotelShanghai = { + id: 'workspace_shanghai_growth', + name: '智念增长组织空间', + city: '上海', + role: 'viewer' as const, + permissions: ['skills.read'], + ota: [], +}; + +function createConfig(overrides: Partial = {}): YinianConfigSnapshot { + return { + serverTime: 1, + user: { id: 'user_1', name: '王管理员' }, + hotel: hotelHangzhou, + hotels: [hotelHangzhou, hotelShanghai], + entitlements: [], + notificationChannels: [], + featureFlags: {}, + uiPolicy: { defaultPage: 'today', showAdvancedSettings: false }, + ...overrides, + }; +} + +function createLocalSkill(overrides: Partial): YinianLocalSkill { + return { + skillId: 'daily-report', + name: '日报生成助手', + version: '1.0.0', + enabled: true, + installedAt: 1, + lastSyncedAt: 2, + status: 'installed', + source: 'mock', + ...overrides, + }; +} + +describe('YinianSkills page', () => { + beforeEach(() => { + vi.clearAllMocks(); + useYinianStore.setState({ + status: 'authenticated', + session: { + authenticated: true, + user: { id: 'user_1', name: '王管理员' }, + hotels: [hotelHangzhou, hotelShanghai], + currentHotelId: hotelHangzhou.id, + accessTokenExpiresAt: 100, + }, + config: createConfig(), + error: null, + }); + useYinianSkillsStore.setState({ + localSkills: [], + lastSync: null, + loading: false, + error: null, + }); + useSkillsStore.setState({ + skills: [], + loading: false, + error: null, + fetchSkills: vi.fn().mockResolvedValue(undefined), + }); + vi.mocked(window.yinian.skills.getRegistry).mockReset(); + vi.mocked(window.yinian.skills.getRegistry).mockResolvedValue(undefined); + vi.mocked(window.yinian.skills.sync).mockReset(); + }); + + it('shows empty entitlement state', () => { + render(); + + expect(screen.getByText('智念企业组织空间')).toBeInTheDocument(); + expect(screen.getByTestId('yinian-skills-empty-entitlements')).toHaveTextContent('当前组织空间尚未开通应用'); + }); + + it('maps entitlement and registry states for installed, update, disabled, and failed skills', () => { + useYinianStore.setState({ + config: createConfig({ + entitlements: [ + { + skillId: 'daily-report', + name: '日报生成助手', + version: '1.0.0', + enabled: true, + category: 'reporting', + triggers: ['scheduled'], + }, + { + skillId: 'data-check', + name: '数据检查助手', + version: '1.2.0', + enabled: true, + category: 'ota-monitoring', + triggers: ['manual'], + }, + { + skillId: 'customer-reply-helper', + name: '客户回复助手', + version: '0.9.0', + enabled: false, + category: 'guest-comm', + triggers: ['manual'], + }, + { + skillId: 'ops-check', + name: '流程检查', + version: '0.1.0', + enabled: true, + category: 'ops-automation', + triggers: [], + }, + ], + }), + }); + useYinianSkillsStore.setState({ + localSkills: [ + createLocalSkill({ skillId: 'daily-report', name: '日报生成助手', version: '1.0.0', status: 'skipped' }), + createLocalSkill({ skillId: 'data-check', name: '数据检查助手', version: '1.1.0', status: 'installed', source: 'nianxx', bundleSha256: 'sha256-demo' }), + createLocalSkill({ skillId: 'ops-check', name: '流程检查', version: '0.1.0', status: 'failed', error: 'download failed' }), + ], + }); + + render(); + + expect(screen.getByTestId('yinian-skill-state-daily-report')).toHaveTextContent('已是最新'); + expect(screen.getByTestId('yinian-skill-state-data-check')).toHaveTextContent('有更新'); + expect(screen.getByTestId('yinian-skill-state-customer-reply-helper')).toHaveTextContent('已禁用'); + expect(screen.getByTestId('yinian-skill-state-ops-check')).toHaveTextContent('同步失败'); + fireEvent.click(screen.getByRole('button', { name: /流程检查/ })); + expect(screen.getByText('download failed')).toBeInTheDocument(); + expect(screen.getAllByText('版本校验').length).toBeGreaterThan(0); + }); + + it('shows locally installed OpenClaw skills in a separate tab', () => { + useSkillsStore.setState({ + skills: [ + { + id: 'local-summary', + slug: 'local-summary', + name: '本地总结助手', + description: '整理本地会话内容', + enabled: true, + version: '0.2.0', + source: 'openclaw-managed', + baseDir: '/tmp/openclaw/skills/local-summary', + }, + ], + loading: false, + error: null, + fetchSkills: vi.fn().mockResolvedValue(undefined), + }); + + render(); + fireEvent.click(screen.getByRole('button', { name: /本地安装/ })); + + expect(screen.getByText('本地总结助手')).toBeInTheDocument(); + expect(screen.getByText('整理本地会话内容')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: /本地总结助手/ })); + expect(screen.getByText('/tmp/openclaw/skills/local-summary')).toBeInTheDocument(); + }); + + it('shows empty registry warning when entitlements exist but no local skills are installed', () => { + useYinianStore.setState({ + config: createConfig({ + entitlements: [ + { + skillId: 'daily-report', + name: '日报生成助手', + version: '1.0.0', + enabled: true, + category: 'reporting', + triggers: ['scheduled'], + }, + ], + }), + }); + + render(); + + expect(screen.getByTestId('yinian-skill-state-daily-report')).toHaveTextContent('已开通未同步'); + expect(screen.getByTestId('yinian-skills-empty-registry')).toHaveTextContent('这台电脑还没有准备好应用'); + }); + + it('sync button stores result and shows success toast', async () => { + useYinianStore.setState({ + config: createConfig({ + entitlements: [ + { + skillId: 'daily-report', + name: '日报生成助手', + version: '1.0.0', + enabled: true, + category: 'reporting', + triggers: ['scheduled'], + }, + ], + }), + }); + vi.mocked(window.yinian.skills.sync).mockResolvedValueOnce({ + hotelId: hotelHangzhou.id, + syncedAt: 10, + skills: [createLocalSkill({ skillId: 'daily-report', name: '日报生成助手', version: '1.0.0', status: 'installed' })], + }); + + render(); + const syncButton = screen.getByRole('button', { name: '同步应用' }); + await waitFor(() => { + expect(syncButton).not.toBeDisabled(); + }); + fireEvent.click(syncButton); + + await waitFor(() => { + expect(window.yinian.skills.sync).toHaveBeenCalled(); + expect(toastSuccessMock).toHaveBeenCalledWith('应用已同步'); + }); + expect(useYinianSkillsStore.getState().localSkills).toHaveLength(1); + }); + + it('loads registry for the current workspace when workspace changes', async () => { + const { rerender } = render(); + await waitFor(() => { + expect(window.yinian.skills.getRegistry).toHaveBeenCalledWith(hotelHangzhou.id); + }); + + useYinianStore.setState({ + config: createConfig({ + hotel: hotelShanghai, + }), + }); + rerender(); + + await waitFor(() => { + expect(window.yinian.skills.getRegistry).toHaveBeenCalledWith(hotelShanghai.id); + }); + }); +}); diff --git a/tests/unit/yinian-skills-store.test.ts b/tests/unit/yinian-skills-store.test.ts new file mode 100644 index 0000000..c3fbc59 --- /dev/null +++ b/tests/unit/yinian-skills-store.test.ts @@ -0,0 +1,93 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useYinianSkillsStore } from '@/stores/yinian-skills'; + +describe('useYinianSkillsStore', () => { + beforeEach(() => { + useYinianSkillsStore.setState({ + localSkills: [], + lastSync: null, + loading: false, + error: null, + }); + vi.mocked(window.yinian.skills.listLocal).mockReset(); + vi.mocked(window.yinian.skills.sync).mockReset(); + vi.mocked(window.yinian.skills.getRegistry).mockReset(); + }); + + it('loads local skills through preload API', async () => { + vi.mocked(window.yinian.skills.listLocal).mockResolvedValueOnce([ + { + skillId: 'daily-report', + name: '日报生成助手', + version: '0.1.0', + enabled: true, + installedAt: 1, + lastSyncedAt: 2, + status: 'installed', + source: 'mock', + }, + ]); + + await useYinianSkillsStore.getState().listLocal(); + + expect(useYinianSkillsStore.getState().localSkills).toHaveLength(1); + expect(useYinianSkillsStore.getState().localSkills[0].skillId).toBe('daily-report'); + }); + + it('syncs skills and stores last sync result', async () => { + vi.mocked(window.yinian.skills.sync).mockResolvedValueOnce({ + hotelId: 'workspace_hangzhou_ops', + syncedAt: 10, + skills: [ + { + skillId: 'data-check', + name: '数据检查助手', + version: '1.0.0', + enabled: true, + installedAt: 1, + lastSyncedAt: 10, + status: 'installed', + source: 'mock', + }, + ], + }); + + await useYinianSkillsStore.getState().sync(); + + expect(useYinianSkillsStore.getState().lastSync?.hotelId).toBe('workspace_hangzhou_ops'); + expect(useYinianSkillsStore.getState().localSkills[0].skillId).toBe('data-check'); + }); + + it('loads a specific workspace registry', async () => { + vi.mocked(window.yinian.skills.getRegistry).mockResolvedValueOnce({ + hotelId: 'workspace_shanghai_growth', + updatedAt: 20, + skills: [ + { + skillId: 'customer-reply-helper', + name: '客户回复助手', + version: '0.1.0', + enabled: true, + installedAt: 1, + lastSyncedAt: 20, + status: 'installed', + source: 'mock', + }, + ], + }); + + const registry = await useYinianSkillsStore.getState().getRegistry('workspace_shanghai_growth'); + + expect(registry && 'hotelId' in registry ? registry.hotelId : undefined).toBe('workspace_shanghai_growth'); + expect(useYinianSkillsStore.getState().localSkills[0].skillId).toBe('customer-reply-helper'); + }); + + it('records an error when registry loading fails', async () => { + vi.mocked(window.yinian.skills.getRegistry).mockRejectedValueOnce(new Error('registry unavailable')); + + const registry = await useYinianSkillsStore.getState().getRegistry('workspace_hangzhou_ops'); + + expect(registry).toBeUndefined(); + expect(useYinianSkillsStore.getState().error).toBe('registry unavailable'); + }); +}); diff --git a/tests/unit/yinian-store.test.ts b/tests/unit/yinian-store.test.ts new file mode 100644 index 0000000..ada32bd --- /dev/null +++ b/tests/unit/yinian-store.test.ts @@ -0,0 +1,120 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useYinianStore } from '@/stores/yinian'; +import { useYinianSkillsStore } from '@/stores/yinian-skills'; + +const session = { + authenticated: true as const, + user: { id: 'user_1', name: '王管理员' }, + hotels: [ + { + id: 'workspace_hangzhou_ops', + name: '智念企业组织空间', + city: '杭州', + role: 'manager' as const, + permissions: ['hotel:view'], + ota: [], + }, + { + id: 'workspace_shanghai_growth', + name: '智念增长组织空间', + city: '上海', + role: 'viewer' as const, + permissions: ['hotel:view'], + ota: [], + }, + ], + currentHotelId: 'workspace_hangzhou_ops', + accessTokenExpiresAt: 100, +}; + +const config = { + serverTime: 1, + user: session.user, + hotel: session.hotels[0], + hotels: session.hotels, + entitlements: [], + notificationChannels: [], + featureFlags: {}, + uiPolicy: { defaultPage: 'today' as const, showAdvancedSettings: false }, +}; + +describe('useYinianStore', () => { + beforeEach(() => { + useYinianStore.setState({ + status: 'idle', + session: { authenticated: false }, + config: null, + error: null, + }); + useYinianSkillsStore.setState({ + localSkills: [], + lastSync: null, + loading: false, + error: null, + }); + vi.mocked(window.yinian.auth.restoreSession).mockReset(); + vi.mocked(window.yinian.auth.loginWithPassword).mockReset(); + vi.mocked(window.yinian.auth.logout).mockReset(); + vi.mocked(window.yinian.app.getConfig).mockReset(); + vi.mocked(window.yinian.app.switchHotel).mockReset(); + vi.mocked(window.yinian.skills.getRegistry).mockReset(); + }); + + it('restores session and loads matching registry on init', async () => { + vi.mocked(window.yinian.auth.restoreSession).mockResolvedValueOnce(session); + vi.mocked(window.yinian.app.getConfig).mockResolvedValueOnce(config); + vi.mocked(window.yinian.skills.getRegistry).mockResolvedValueOnce({ + hotelId: 'workspace_hangzhou_ops', + updatedAt: 10, + skills: [], + }); + + await useYinianStore.getState().init(); + + expect(useYinianStore.getState().status).toBe('authenticated'); + expect(window.yinian.skills.getRegistry).toHaveBeenCalledWith('workspace_hangzhou_ops'); + }); + + it('switches workspace and refreshes registry for the new hotel', async () => { + const switched = { ...session, currentHotelId: 'workspace_shanghai_growth' }; + vi.mocked(window.yinian.app.switchHotel).mockResolvedValueOnce(switched); + vi.mocked(window.yinian.app.getConfig).mockResolvedValueOnce({ + ...config, + hotel: session.hotels[1], + }); + vi.mocked(window.yinian.skills.getRegistry).mockResolvedValueOnce({ + hotelId: 'workspace_shanghai_growth', + updatedAt: 10, + skills: [], + }); + + useYinianStore.setState({ session, status: 'authenticated', config }); + await useYinianStore.getState().switchHotel('workspace_shanghai_growth'); + + expect(useYinianStore.getState().session.authenticated).toBe(true); + expect(window.yinian.skills.getRegistry).toHaveBeenCalledWith('workspace_shanghai_growth'); + }); + + it('resets yinian skill state on logout', async () => { + vi.mocked(window.yinian.auth.logout).mockResolvedValueOnce({ authenticated: false }); + useYinianSkillsStore.setState({ + localSkills: [ + { + skillId: 'daily-report', + name: '日报生成助手', + version: '0.1.0', + enabled: true, + installedAt: 1, + lastSyncedAt: 2, + status: 'installed', + source: 'mock', + }, + ], + }); + + await useYinianStore.getState().logout(); + + expect(useYinianStore.getState().session.authenticated).toBe(false); + expect(useYinianSkillsStore.getState().localSkills).toEqual([]); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 5b04ba5..c74c8a0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,7 +27,7 @@ "@electron/*": ["electron/*"] } }, - "include": ["src"], + "include": ["src", "packages/*/src"], "exclude": ["node_modules"], "references": [{ "path": "./tsconfig.node.json" }] }