mirror of
https://github.com/moeru-ai/airi.git
synced 2026-07-10 00:08:33 +00:00
Compare commits
3 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0642ede8d1 | ||
|
|
5aa44aedd3 | ||
|
|
de0ff177ad |
60 changed files with 1910 additions and 197 deletions
|
|
@ -130,7 +130,8 @@
|
|||
| Conversation controls | `chat_message_deleted` / `chat_messages_cleared` / `chat_message_retried` | 前端 | `packages/stage-layouts/src/components/Layouts/*InteractiveArea.vue` / `packages/stage-layouts/src/components/Widgets/ChatActionButtons.vue` / `apps/stage-tamagotchi/src/renderer/components/InteractiveArea.vue` | PostHog |
|
||||
| Conversation controls | `tts_stop_clicked` | 前端 | `packages/stage-layouts/src/composables/useStopSpeakingButton.ts` | PostHog |
|
||||
| Churn | `subscription_cancelled`(带 cancellation_reason) | 外部 Stripe 数据源 | PostHog Stripe source connector | Stripe/Postgres |
|
||||
| 老事件 | `provider_card_clicked` / `first_message_sent` | 前端 | `packages/stage-ui/src/composables/use-analytics.ts` | PostHog |
|
||||
| 老事件 | `provider_card_clicked` | 前端 | `packages/stage-ui/src/composables/use-analytics.ts` | PostHog |
|
||||
| 已退役 | `first_message_sent` / `user_signed_up`(前端版) | 无生产者 | wrapper 已删除;激活口径用 `chat_activation_succeeded`,注册事实用服务端转发的 `signup_completed` | PostHog 历史数据仍在 |
|
||||
|
||||
待埋点(API 已在 `use-analytics.ts` 暴露但调用点未接入):
|
||||
|
||||
|
|
@ -150,22 +151,21 @@
|
|||
|
||||
## PostHog 接入路线图
|
||||
|
||||
落地分两步,**不要一次性埋全部事件**,否则 schema 漂移会很快出现。PostHog 采集只发生在前端或外部 source connector;server 请求路径只写 Postgres/Grafana。
|
||||
落地分两步,**不要一次性埋全部事件**,否则 schema 漂移会很快出现。PostHog 采集以前端为主;服务端只经 product-events 白名单转发业务事实(注册、支付、订阅),per-request 路径仍只写 Postgres/Grafana。
|
||||
|
||||
### 阶段 1(P0 — 付费漏斗 + activation)
|
||||
|
||||
`apps/stage-web`:
|
||||
所有 surface 共用根目录 `posthog.config.ts`(单一 project key,`surface` super property 区分端)。初始化实况:
|
||||
|
||||
```ts
|
||||
import posthog from 'posthog-js'
|
||||
import { DEFAULT_POSTHOG_CONFIG, POSTHOG_PROJECT_KEY } from '../posthog.config'
|
||||
|
||||
posthog.init(import.meta.env.VITE_POSTHOG_KEY, {
|
||||
api_host: 'https://us.i.posthog.com',
|
||||
capture_pageview: false, // 手动 capture 控制语义
|
||||
})
|
||||
// 登录后
|
||||
// DEFAULT_POSTHOG_CONFIG 内含 defaults: '2025-05-24':
|
||||
// SPA 路由切换自动发 $pageview / $pageleave,页面浏览不再手动埋。
|
||||
posthog.init(POSTHOG_PROJECT_KEY, { ...DEFAULT_POSTHOG_CONFIG })
|
||||
// 登录后(stage 端在 analytics store,auth 端在 profile.vue)
|
||||
posthog.identify(user.id)
|
||||
// 在 pricing.vue
|
||||
// 在 flux.vue
|
||||
posthog.capture('pricing_page_viewed', { plan_period, source })
|
||||
```
|
||||
|
||||
|
|
@ -184,13 +184,13 @@ posthog.init(import.meta.env.VITE_POSTHOG_KEY, {
|
|||
|
||||
埋点事件清单(P0):
|
||||
|
||||
- 前端:`pricing_page_viewed`、`plan_selected`、`checkout_started`、`user_signed_up`、`first_message_sent`、`first_model_selected`
|
||||
- 后端:不直接发 PostHog;`payment_completed` 写入 `product_events`,PostHog 侧通过 Stripe source connector 或离线导入展示
|
||||
- 前端:`pricing_page_viewed`、`plan_selected`、`checkout_started`、`signup_completed`(ui-server-auth 邮箱注册路径)、`first_model_selected`
|
||||
- 后端:`product_events` 是事实账本;其中业务事实白名单(`signup_completed`、`payment_completed`、`subscription_started/renewed/cancelled`)由 product-events 服务经 posthog-node 转发一份到 PostHog(`apps/server/src/services/domain/product-events.ts`,distinctId = Better Auth user id)。LLM / TTS 等 per-request 事件不转发
|
||||
|
||||
PostHog UI 配两个 funnel:
|
||||
|
||||
- **付费漏斗** (7d 窗口):`pricing_page_viewed → plan_selected → checkout_started → payment_completed`(最后一步来自 Stripe source connector / 离线导入)
|
||||
- **激活漏斗** (14d 窗口):`user_signed_up → first_message_sent → first_model_selected → payment_completed`(最后一步同上)
|
||||
- **付费漏斗** (7d 窗口):`pricing_page_viewed → plan_selected → checkout_started → payment_completed`(最后一步来自服务端转发)
|
||||
- **激活漏斗** (14d 窗口):`signup_completed → onboarding_started → chat_activation_succeeded → payment_completed`
|
||||
|
||||
### 阶段 2(P1 — retention / feature adoption / churn)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
# Verification: 服务端 PostHog 转发 + SPA 路由 pageview
|
||||
|
||||
Status: **transport 与 pageview 已真实验证;线上 Stripe webhook 端到端待部署后确认**
|
||||
Owner: Product Analytics
|
||||
Last updated: 2026-07-08
|
||||
Environment: commit 689f02ac4 + 本次工作区改动;posthog-node 5.39.4;posthog-js 1.306.1;Node 26.3.0
|
||||
|
||||
## 用户路径
|
||||
|
||||
- **场景 1**:用户完成 Stripe 支付 → webhook 写 `product_events` → 服务端把 `payment_completed` 转发到 PostHog(distinctId = Better Auth user id)→ PostHog 付费漏斗在 `checkout_started` 后闭环。
|
||||
- **场景 2**:用户在 stage-web 内切换路由(如进入 `/settings/flux`)→ PostHog 收到 `$pageview`,带 `$pathname`、上一页路径与停留时长。
|
||||
|
||||
## 已验证证据
|
||||
|
||||
| 验证项 | 命令 / 方式 | 实际输出 |
|
||||
|---|---|---|
|
||||
| 转发白名单与映射 | `pnpm exec vitest run src/services/domain/product-events.test.ts`(apps/server) | 6 passed:`payment_completed` 原名转发、`user_signed_up→signup_completed` 映射、per-request 动作不转发、sink 抛错时 DB 行仍落库且 track 不抛 |
|
||||
| posthog-node 真实传输 | `node posthog-smoke.mjs`(captureImmediate → us.i.posthog.com,生产 project key,事件名 `server_forwarding_smoke_test`) | `captureImmediate resolved in 1380ms` + `shutdown clean` |
|
||||
| SPA 路由 pageview | `VITE_ENABLE_POSTHOG=true pnpm -F @proj-airi/stage-web dev` + agent-browser 两次 `history.pushState` | 两条 `$pageview`,`$pathname` 分别为 `/settings/flux`、`/settings/airi-card`,`navigation_type: pushState`,携带 `$prev_pageview_duration` 与 `surface: web` super property;批量 POST `us.i.posthog.com/e/` 返回 200 |
|
||||
| 服务端 typecheck / lint | `pnpm -F @proj-airi/server typecheck`、eslint 改动文件 | 均通过 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 自动化浏览器(`navigator.webdriver = true`)会被 posthog-js 默认 bot 过滤静默丢弃事件;本次浏览器验证通过会话内 `set_config({ opt_out_useragent_filter: true })` 绕过,仅影响该验证会话,生产配置未改。人工复测时用普通浏览器即可,无需任何绕过。
|
||||
- 服务端转发默认开启:`POSTHOG_PROJECT_KEY` 的默认值就是前端共用的 phc_* project key,置空字符串可关闭。部署后在 PostHog 里确认 `payment_completed` 事件出现在真实支付后,即完成端到端收尾。
|
||||
- PostHog 项目里会留有一条 `server_forwarding_smoke_test`(distinctId `verification-smoke`)测试事件,分析时按事件名过滤。
|
||||
|
|
@ -61,6 +61,7 @@
|
|||
"nanoid": "catalog:",
|
||||
"ofetch": "catalog:",
|
||||
"pg": "catalog:",
|
||||
"posthog-node": "catalog:",
|
||||
"resend": "catalog:",
|
||||
"stripe": "catalog:",
|
||||
"unspeech": "catalog:xsai",
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ import { createStripeRoutes } from './routes/stripe'
|
|||
import { createVoicePackRoutes } from './routes/voice-packs'
|
||||
import { createConfigKVService } from './services/adapters/config-kv'
|
||||
import { createEmailService } from './services/adapters/email'
|
||||
import { createPosthogSink } from './services/adapters/posthog'
|
||||
import { createAdminFluxGrantsService } from './services/domain/admin/flux-grants'
|
||||
import { createAdminRouterConfigService } from './services/domain/admin/router-config'
|
||||
import { createAdminUsersService } from './services/domain/admin/users'
|
||||
|
|
@ -593,9 +594,27 @@ export async function createApp() {
|
|||
}, undefined, dependsOn.otel?.email),
|
||||
})
|
||||
|
||||
const posthogSink = injeca.provide('services:posthogSink', {
|
||||
dependsOn: { env: parsedEnv, lifecycle },
|
||||
// POSTHOG_PROJECT_KEY defaults to the shared project key, so the falsy
|
||||
// branch is only reachable via the documented off-switch: setting the
|
||||
// env var to an empty string (valibot defaults don't apply to '').
|
||||
build: ({ dependsOn }) => {
|
||||
if (!dependsOn.env.POSTHOG_PROJECT_KEY)
|
||||
return null
|
||||
|
||||
const sink = createPosthogSink({
|
||||
projectKey: dependsOn.env.POSTHOG_PROJECT_KEY,
|
||||
host: dependsOn.env.POSTHOG_API_HOST,
|
||||
})
|
||||
dependsOn.lifecycle.appHooks.onStop(() => sink.shutdown())
|
||||
return sink
|
||||
},
|
||||
})
|
||||
|
||||
const productEventService = injeca.provide('services:productEvents', {
|
||||
dependsOn: { db, otel },
|
||||
build: ({ dependsOn }) => createProductEventService(dependsOn.db, dependsOn.otel?.product),
|
||||
dependsOn: { db, otel, posthogSink },
|
||||
build: ({ dependsOn }) => createProductEventService(dependsOn.db, dependsOn.otel?.product, dependsOn.posthogSink),
|
||||
})
|
||||
|
||||
const characterService = injeca.provide('services:characters', {
|
||||
|
|
|
|||
|
|
@ -175,6 +175,14 @@ const EnvSchema = object({
|
|||
DB_POOL_CONNECTION_TIMEOUT_MS: optionalIntegerFromString(5000, 'DB_POOL_CONNECTION_TIMEOUT_MS', 1),
|
||||
DB_POOL_KEEPALIVE_INITIAL_DELAY_MS: optionalIntegerFromString(10000, 'DB_POOL_KEEPALIVE_INITIAL_DELAY_MS', 1),
|
||||
|
||||
// PostHog product-event forwarding (signup / payment / subscription facts).
|
||||
// Defaults to the shared AIRI project key (same browser-safe phc_* key the
|
||||
// client surfaces embed in posthog.config.ts), so forwarding is on out of
|
||||
// the box. Set to an empty string to disable; Postgres `product_events`
|
||||
// stays the source of truth either way.
|
||||
POSTHOG_PROJECT_KEY: optional(string(), 'phc_pzjziJjrVZpa9SqnQqq0QEKvkmuCPH7GDTA6TbRTEf9'), // cspell:disable-line
|
||||
POSTHOG_API_HOST: optional(string(), 'https://us.i.posthog.com'),
|
||||
|
||||
// OpenTelemetry
|
||||
OTEL_SERVICE_NAMESPACE: optional(string(), 'airi'),
|
||||
OTEL_SERVICE_NAME: optional(string(), 'server'),
|
||||
|
|
|
|||
59
apps/server/src/services/adapters/posthog.ts
Normal file
59
apps/server/src/services/adapters/posthog.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { useLogger } from '@guiiai/logg'
|
||||
import { PostHog } from 'posthog-node'
|
||||
|
||||
const logger = useLogger('posthog')
|
||||
|
||||
/**
|
||||
* One product event forwarded to PostHog, keyed by the Better Auth user id
|
||||
* so it merges with the browser person identified via `posthog.identify()`.
|
||||
*/
|
||||
export interface PosthogCaptureInput {
|
||||
distinctId: string
|
||||
event: string
|
||||
properties: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal capture boundary the product-events service depends on. Kept as
|
||||
* an interface so tests inject a fake instead of mocking the SDK.
|
||||
*/
|
||||
export interface PosthogSink {
|
||||
capture: (input: PosthogCaptureInput) => Promise<void>
|
||||
/** Flush and close the underlying client. Call on server shutdown. */
|
||||
shutdown: () => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* PostHog sink for server-side product events.
|
||||
*
|
||||
* Uses `captureImmediate` (one HTTP roundtrip per event, no background
|
||||
* queue) on purpose: the forwarded events are low-frequency business facts
|
||||
* (signup, payment, subscription lifecycle) fired from webhook/auth-hook
|
||||
* paths where a queued batch could be lost on process exit.
|
||||
*
|
||||
* Capture failures are logged and swallowed — analytics forwarding must
|
||||
* never fail the Stripe webhook or auth flow that triggered it. The
|
||||
* Postgres `product_events` row is the source of truth either way.
|
||||
*/
|
||||
export function createPosthogSink(options: { projectKey: string, host: string }): PosthogSink {
|
||||
const client = new PostHog(options.projectKey, { host: options.host })
|
||||
|
||||
return {
|
||||
async capture(input: PosthogCaptureInput): Promise<void> {
|
||||
try {
|
||||
await client.captureImmediate({
|
||||
distinctId: input.distinctId,
|
||||
event: input.event,
|
||||
properties: input.properties,
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
logger.withError(err).withFields({ event: input.event }).warn('Failed to forward product event to PostHog')
|
||||
}
|
||||
},
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
await client.shutdown()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import type { Database } from '../../libs/db'
|
||||
import type { ProductMetrics } from '../../otel'
|
||||
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { mockDB } from '../../libs/mock-db'
|
||||
|
|
@ -136,4 +137,137 @@ describe('productEventService', () => {
|
|||
flux_balance_bucket: 'zero',
|
||||
})
|
||||
})
|
||||
|
||||
it('forwards allowlisted business facts to PostHog keyed by user id, mapping user_signed_up to signup_completed', async () => {
|
||||
const capture = vi.fn(async () => {})
|
||||
const sink = { capture, shutdown: vi.fn(async () => {}) }
|
||||
const service = createProductEventService(db, null, sink)
|
||||
|
||||
await service.track({
|
||||
userId: 'user-1',
|
||||
feature: 'billing',
|
||||
action: 'payment_completed',
|
||||
status: 'succeeded',
|
||||
source: 'stripe.webhook',
|
||||
metadata: { amount_minor_unit: 990, currency: 'usd' },
|
||||
})
|
||||
await service.track({
|
||||
userId: 'user-2',
|
||||
feature: 'auth',
|
||||
action: 'user_signed_up',
|
||||
status: 'succeeded',
|
||||
})
|
||||
|
||||
expect(capture).toHaveBeenNthCalledWith(1, {
|
||||
distinctId: 'user-1',
|
||||
event: 'payment_completed',
|
||||
properties: {
|
||||
surface: 'server',
|
||||
feature: 'billing',
|
||||
status: 'succeeded',
|
||||
source: 'stripe.webhook',
|
||||
amount_minor_unit: 990,
|
||||
currency: 'usd',
|
||||
},
|
||||
})
|
||||
expect(capture).toHaveBeenNthCalledWith(2, {
|
||||
distinctId: 'user-2',
|
||||
event: 'signup_completed',
|
||||
properties: {
|
||||
surface: 'server',
|
||||
feature: 'auth',
|
||||
status: 'succeeded',
|
||||
},
|
||||
})
|
||||
|
||||
const rows = await db.select().from(schema.productEvents)
|
||||
expect(rows).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('does not forward high-volume per-request actions to PostHog', async () => {
|
||||
const capture = vi.fn(async () => {})
|
||||
const sink = { capture, shutdown: vi.fn(async () => {}) }
|
||||
const service = createProductEventService(db, null, sink)
|
||||
|
||||
await service.track({
|
||||
userId: 'user-1',
|
||||
feature: 'gen_ai_chat',
|
||||
action: 'completion_succeeded',
|
||||
status: 'succeeded',
|
||||
})
|
||||
await service.track({
|
||||
userId: 'user-1',
|
||||
feature: 'billing',
|
||||
action: 'checkout_started',
|
||||
status: 'started',
|
||||
})
|
||||
|
||||
expect(capture).not.toHaveBeenCalled()
|
||||
const rows = await db.select().from(schema.productEvents)
|
||||
expect(rows).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('does not forward to PostHog when the DB write fails', async () => {
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// track() swallows DB insert errors to protect the caller, but the
|
||||
// PostHog forwarding block ran unconditionally afterwards — a Postgres
|
||||
// outage during a Stripe webhook would mint `payment_completed` in
|
||||
// PostHog with no `product_events` row backing it, breaking the
|
||||
// "Postgres is the fact of record" invariant and later reconciliation.
|
||||
// Found by PR #2038 review.
|
||||
//
|
||||
// Fixed by gating forwarding on a `persisted` flag set only after the
|
||||
// insert resolves.
|
||||
const capture = vi.fn(async () => {})
|
||||
const sink = { capture, shutdown: vi.fn(async () => {}) }
|
||||
const service = createProductEventService(db, null, sink)
|
||||
|
||||
// Simulate a DB outage by renaming the table out from under the insert.
|
||||
await db.execute(sql`ALTER TABLE product_events RENAME TO product_events_outage`)
|
||||
try {
|
||||
await expect(service.track({
|
||||
userId: 'user-1',
|
||||
feature: 'billing',
|
||||
action: 'payment_completed',
|
||||
status: 'succeeded',
|
||||
})).resolves.toBeUndefined()
|
||||
}
|
||||
finally {
|
||||
await db.execute(sql`ALTER TABLE product_events_outage RENAME TO product_events`)
|
||||
}
|
||||
|
||||
expect(capture).not.toHaveBeenCalled()
|
||||
const rows = await db.select().from(schema.productEvents)
|
||||
expect(rows).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('persists the product event even when a misbehaving sink throws', async () => {
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// The PosthogSink contract says implementations swallow transport
|
||||
// errors, but the forwarding call sits on the Stripe webhook path —
|
||||
// if a sink ever throws, an unguarded `await` would fail the webhook
|
||||
// after the fact was already persisted, causing Stripe to retry and
|
||||
// (before the idempotency guard) double-process the payment.
|
||||
//
|
||||
// track() therefore wraps forwarding in its own try/catch: the DB row
|
||||
// must survive and track() must resolve regardless of sink behavior.
|
||||
const capture = vi.fn(async () => {
|
||||
throw new Error('posthog exploded')
|
||||
})
|
||||
const sink = { capture, shutdown: vi.fn(async () => {}) }
|
||||
const service = createProductEventService(db, null, sink)
|
||||
|
||||
await expect(service.track({
|
||||
userId: 'user-1',
|
||||
feature: 'billing',
|
||||
action: 'payment_completed',
|
||||
status: 'succeeded',
|
||||
})).resolves.toBeUndefined()
|
||||
|
||||
const rows = await db.select().from(schema.productEvents)
|
||||
expect(rows).toHaveLength(1)
|
||||
expect(rows[0]).toMatchObject({ action: 'payment_completed', status: 'succeeded' })
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { Database } from '../../libs/db'
|
||||
import type { ProductMetrics } from '../../otel'
|
||||
import type { ProductEventMetadata } from '../../schemas/product-events'
|
||||
import type { PosthogSink } from '../adapters/posthog'
|
||||
|
||||
import { useLogger } from '@guiiai/logg'
|
||||
import { and, asc, count, gte, lt, sql } from 'drizzle-orm'
|
||||
|
|
@ -75,6 +76,24 @@ export interface ProductEventAggregateRow {
|
|||
distinctUsers: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-side actions worth a PostHog copy, mapped to the event name the
|
||||
* client-side funnels expect. Only business facts that terminate or anchor
|
||||
* a funnel are forwarded — per-request LLM/TTS volume stays in Postgres and
|
||||
* Grafana where it belongs (see `docs/ai-context/metrics-ownership.md`).
|
||||
*
|
||||
* `user_signed_up` maps to `signup_completed` because that is the canonical
|
||||
* activation-funnel step name the browser surfaces emit; the server copy
|
||||
* covers OAuth signups the auth UI cannot classify client-side.
|
||||
*/
|
||||
const POSTHOG_FORWARDED_ACTIONS: Partial<Record<ProductAction, string>> = {
|
||||
user_signed_up: 'signup_completed',
|
||||
payment_completed: 'payment_completed',
|
||||
subscription_started: 'subscription_started',
|
||||
subscription_renewed: 'subscription_renewed',
|
||||
subscription_cancelled: 'subscription_cancelled',
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds bounded Prometheus labels from product event inputs.
|
||||
*/
|
||||
|
|
@ -113,9 +132,14 @@ function metricLabels(input: ProductEventInput): Record<string, string> {
|
|||
* Returns:
|
||||
* - Best-effort event writer plus a DB aggregation helper for analytics jobs.
|
||||
*/
|
||||
export function createProductEventService(db: Database, metrics?: ProductMetrics | null) {
|
||||
export function createProductEventService(db: Database, metrics?: ProductMetrics | null, posthog?: PosthogSink | null) {
|
||||
return {
|
||||
async track(input: ProductEventInput): Promise<void> {
|
||||
// Postgres is the fact of record, so forwarding is gated both ways:
|
||||
// the DB write comes first (a PostHog outage can't lose the row) and
|
||||
// forwarding only runs when the row actually landed (a DB outage
|
||||
// can't mint PostHog events with no DB backing).
|
||||
let persisted = false
|
||||
try {
|
||||
await db.insert(schema.productEvents).values({
|
||||
userId: input.userId,
|
||||
|
|
@ -129,6 +153,7 @@ export function createProductEventService(db: Database, metrics?: ProductMetrics
|
|||
metadata: input.metadata,
|
||||
createdAt: input.createdAt,
|
||||
})
|
||||
persisted = true
|
||||
|
||||
metrics?.events.add(1, metricLabels(input))
|
||||
}
|
||||
|
|
@ -140,6 +165,32 @@ export function createProductEventService(db: Database, metrics?: ProductMetrics
|
|||
status: input.status,
|
||||
}).warn('Failed to write product event; swallowing to protect caller')
|
||||
}
|
||||
|
||||
// PostHog copy so browser funnels (identified by the same Better Auth
|
||||
// user id) get their server-side terminator events.
|
||||
const forwardedEvent = POSTHOG_FORWARDED_ACTIONS[input.action]
|
||||
if (persisted && posthog && forwardedEvent) {
|
||||
try {
|
||||
await posthog.capture({
|
||||
distinctId: input.userId,
|
||||
event: forwardedEvent,
|
||||
properties: {
|
||||
surface: 'server',
|
||||
feature: input.feature,
|
||||
status: input.status,
|
||||
...(input.source && { source: input.source }),
|
||||
...(input.reason && { reason: input.reason }),
|
||||
...input.metadata,
|
||||
},
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
// The sink contract already swallows transport errors; this guard
|
||||
// is the last line so a misbehaving sink can never fail the
|
||||
// webhook/auth flow that produced the business fact.
|
||||
logger.withError(err).withFields({ action: input.action }).warn('PostHog forwarding threw; product event already persisted')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async countDistinctUsersByFeature(input: ProductEventAggregateInput): Promise<ProductEventAggregateRow[]> {
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@ import semver from 'semver'
|
|||
|
||||
import { useElectronAutoUpdater, useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
|
||||
import { AboutContent, BugReportDialog, createBugReportPageContext, MarkdownRenderer } from '@proj-airi/stage-ui/components'
|
||||
import { useBreakpoints } from '@proj-airi/stage-ui/composables'
|
||||
import { useAnalytics, useBreakpoints } from '@proj-airi/stage-ui/composables'
|
||||
import { useSharedAnalyticsStore } from '@proj-airi/stage-ui/stores/analytics'
|
||||
import { Button, ContainerError, DoubleCheckButton, FieldSelect, Progress } from '@proj-airi/ui'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { DialogContent, DialogDescription, DialogOverlay, DialogPortal, DialogRoot, DialogTitle } from 'reka-ui'
|
||||
import { DrawerContent, DrawerDescription, DrawerHandle, DrawerOverlay, DrawerPortal, DrawerRoot, DrawerTitle } from 'vaul-vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { electronGetUpdaterPreferences, electronSetUpdaterPreferences } from '../../shared/eventa'
|
||||
|
|
@ -32,6 +32,13 @@ const {
|
|||
quitAndInstall,
|
||||
} = useElectronAutoUpdater()
|
||||
|
||||
const {
|
||||
trackUpdateCheckClicked,
|
||||
trackUpdateDownloaded,
|
||||
trackUpdateInstallClicked,
|
||||
trackSettingsChanged,
|
||||
} = useAnalytics()
|
||||
|
||||
const isDisabled = computed(() => updateState.value.status === 'disabled')
|
||||
const isLatestVersion = computed(() => {
|
||||
return updateState.value.status === 'not-available' && !isDisabled.value
|
||||
|
|
@ -119,6 +126,23 @@ const isDowngradeUpdate = computed(() => {
|
|||
const getUpdaterPreferences = useElectronEventaInvoke(electronGetUpdaterPreferences)
|
||||
const setUpdaterPreferences = useElectronEventaInvoke(electronSetUpdaterPreferences)
|
||||
|
||||
function handleCheckForUpdates() {
|
||||
trackUpdateCheckClicked({ channel: selectedUpdateChannel.value })
|
||||
return checkForUpdates()
|
||||
}
|
||||
|
||||
function handleQuitAndInstall() {
|
||||
trackUpdateInstallClicked({ channel: selectedUpdateChannel.value, version: updateState.value.info?.version })
|
||||
quitAndInstall()
|
||||
}
|
||||
|
||||
// Fires on the state transition (not the download click) so the event means
|
||||
// "an update binary actually landed on this machine".
|
||||
watch(() => updateState.value.status, (status) => {
|
||||
if (status === 'downloaded')
|
||||
trackUpdateDownloaded({ channel: selectedUpdateChannel.value, version: updateState.value.info?.version })
|
||||
})
|
||||
|
||||
function handleDownloadClick() {
|
||||
if (updateState.value.info?.releaseNotes)
|
||||
showChangelog.value = true
|
||||
|
|
@ -181,9 +205,16 @@ async function setUpdateChannelPreference(channel: UpdateChannelOption) {
|
|||
|
||||
isUpdateChannelUpdating.value = true
|
||||
try {
|
||||
const previousChannel = selectedUpdateChannel.value
|
||||
const nextChannel = channel === 'auto' ? undefined : channel as ElectronUpdaterChannel
|
||||
const preferences = await setUpdaterPreferences({ channel: nextChannel })
|
||||
selectedUpdateChannel.value = preferences?.channel ?? 'auto'
|
||||
trackSettingsChanged({
|
||||
setting_name: 'update_channel',
|
||||
previous_value: previousChannel,
|
||||
new_value: selectedUpdateChannel.value,
|
||||
source: 'settings',
|
||||
})
|
||||
await checkForUpdates()
|
||||
}
|
||||
finally {
|
||||
|
|
@ -325,7 +356,7 @@ onMounted(() => {
|
|||
<div>
|
||||
<DoubleCheckButton
|
||||
variant="primary"
|
||||
@confirm="quitAndInstall()"
|
||||
@confirm="handleQuitAndInstall()"
|
||||
>
|
||||
{{ restartButtonLabel }}
|
||||
<template #confirm>
|
||||
|
|
@ -366,7 +397,7 @@ onMounted(() => {
|
|||
: isError
|
||||
? t('tamagotchi.stage.about.update.actions.retry-check')
|
||||
: t('tamagotchi.stage.about.update.actions.check-for-updates')"
|
||||
@click="checkForUpdates()"
|
||||
@click="handleCheckForUpdates()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { ServerChannelQrPayload } from '@proj-airi/stage-shared/server-chan
|
|||
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables'
|
||||
import { Button, Callout, Collapsible } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { renderSVG } from 'uqr'
|
||||
|
|
@ -13,7 +14,14 @@ import { electronGetServerChannelQrPayload } from '../../../../shared/eventa'
|
|||
import { useServerChannelSettingsStore } from '../../../stores/settings/server-channel'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { trackDevicePairingQrShown } = useAnalytics()
|
||||
const getServerChannelQrPayload = useElectronEventaInvoke(electronGetServerChannelQrPayload)
|
||||
|
||||
function handleToggleExpanded(visible: boolean, setVisible: (value: boolean) => void) {
|
||||
setVisible(visible)
|
||||
if (visible)
|
||||
trackDevicePairingQrShown()
|
||||
}
|
||||
const { authToken, hostname, tlsConfig } = storeToRefs(useServerChannelSettingsStore())
|
||||
|
||||
const loading = shallowRef(false)
|
||||
|
|
@ -82,7 +90,7 @@ watch([hostname, tlsConfig, authToken], () => {
|
|||
:class="[
|
||||
'w-full flex items-center justify-between gap-3 rounded-xl text-left outline-none transition-all duration-250 ease-in-out',
|
||||
]"
|
||||
@click="slotProps.setVisible(!slotProps.visible)"
|
||||
@click="handleToggleExpanded(!slotProps.visible, slotProps.setVisible)"
|
||||
>
|
||||
<div :class="['min-w-0 flex flex-col gap-1']">
|
||||
<div :class="['text-sm font-medium text-neutral-900 dark:text-neutral-100']">
|
||||
|
|
|
|||
|
|
@ -2,18 +2,21 @@
|
|||
import type { DataSettingsStatusEmits } from '@proj-airi/stage-pages/pages/settings/data/status'
|
||||
|
||||
import { createDataSettingsStatusHelpers } from '@proj-airi/stage-pages/pages/settings/data/status'
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables'
|
||||
import { useDataMaintenance } from '@proj-airi/stage-ui/composables/use-data-maintenance'
|
||||
import { DoubleCheckButton } from '@proj-airi/ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const emit = defineEmits<DataSettingsStatusEmits>()
|
||||
const { t } = useI18n()
|
||||
const { trackDataAction } = useAnalytics()
|
||||
const { resetDesktopApplicationState } = useDataMaintenance()
|
||||
const { emitStatus, handleActionError } = createDataSettingsStatusHelpers(emit)
|
||||
|
||||
async function resetDesktopState() {
|
||||
try {
|
||||
await resetDesktopApplicationState()
|
||||
trackDataAction({ action: 'desktop_state_reset' })
|
||||
emitStatus(t('settings.pages.data.status.desktop_reset'))
|
||||
}
|
||||
catch (error) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import type {
|
|||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { useElectronEventaContext, useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
|
||||
import { ModelSettingsPanel } from '@proj-airi/stage-ui/components/scenarios/settings/model-settings'
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { Button, Callout } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
|
@ -272,9 +273,12 @@ async function syncGodotSceneInput(model: DisplayModel) {
|
|||
}
|
||||
}
|
||||
|
||||
const { trackFeatureUsed } = useAnalytics()
|
||||
|
||||
async function handleGodotStageToggle() {
|
||||
switchingGodotStage.value = true
|
||||
godotStageError.value = undefined
|
||||
const enablingGodot = !usesGodotStage.value
|
||||
|
||||
try {
|
||||
if (usesGodotStage.value) {
|
||||
|
|
@ -291,6 +295,12 @@ async function handleGodotStageToggle() {
|
|||
await refreshGodotStageStatus()
|
||||
}
|
||||
finally {
|
||||
trackFeatureUsed({
|
||||
feature_name: enablingGodot ? 'godot_stage_enabled' : 'godot_stage_disabled',
|
||||
business_domain: 'stage_rendering',
|
||||
entry: 'settings',
|
||||
success: godotStageError.value === undefined,
|
||||
})
|
||||
switchingGodotStage.value = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type { ServerForm } from './mcp-config'
|
|||
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables'
|
||||
import {
|
||||
Button,
|
||||
Callout,
|
||||
|
|
@ -41,6 +42,7 @@ import {
|
|||
} from './mcp-config'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { trackMcpServerAdded, trackMcpServerRemoved, trackMcpConnectionTestRun } = useAnalytics()
|
||||
const tn = (key: string, params?: Record<string, unknown>) => t(`settings.pages.modules.mcp-server.${key}`, params ?? {})
|
||||
|
||||
const invokeOpenConfigFile = useElectronEventaInvoke(electronMcpOpenConfigFile)
|
||||
|
|
@ -213,14 +215,17 @@ function formatJsonDraft() {
|
|||
function addServer() {
|
||||
const server = createServerForm()
|
||||
servers.value.push(server)
|
||||
trackMcpServerAdded()
|
||||
if (!testRowId.value)
|
||||
testRowId.value = server.rowId
|
||||
}
|
||||
|
||||
function removeServer(rowId: string) {
|
||||
const i = servers.value.findIndex(s => s.rowId === rowId)
|
||||
if (i >= 0)
|
||||
if (i >= 0) {
|
||||
servers.value.splice(i, 1)
|
||||
trackMcpServerRemoved()
|
||||
}
|
||||
savedIds.value.delete(rowId)
|
||||
expandedIds.value.delete(rowId)
|
||||
if (testRowId.value === rowId)
|
||||
|
|
@ -320,6 +325,7 @@ async function runConnectionTest() {
|
|||
testResult.value = { ok: false, error: errorMessageFrom(e) ?? 'Unknown error', durationMs: 0 }
|
||||
}
|
||||
finally {
|
||||
trackMcpConnectionTestRun({ success: testResult.value?.ok === true })
|
||||
testRunning.value = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { ShortcutAccelerator, ShortcutFailureReason } from '@proj-airi/stag
|
|||
|
||||
import { useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
|
||||
import { formatAccelerator, ShortcutFailureReasons } from '@proj-airi/stage-shared/global-shortcut'
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables'
|
||||
import { Button } from '@proj-airi/ui'
|
||||
import { isMacOS } from 'std-env'
|
||||
import { computed, onMounted, shallowRef } from 'vue'
|
||||
|
|
@ -17,6 +18,7 @@ import { isSafeSpotlightAccelerator } from '../../../../shared/spotlight-shortcu
|
|||
|
||||
const getShortcut = useElectronEventaInvoke(electronSpotlightShortcutGet)
|
||||
const setShortcut = useElectronEventaInvoke(electronSpotlightShortcutSet)
|
||||
const { trackSettingsChanged } = useAnalytics()
|
||||
const { t } = useI18n()
|
||||
const tt = (key: string) => t(`tamagotchi.settings.pages.system.window-shortcuts.${key}`)
|
||||
|
||||
|
|
@ -64,6 +66,13 @@ async function saveShortcut(next: ShortcutAccelerator | null) {
|
|||
return
|
||||
}
|
||||
accelerator.value = result.actualAccelerator ?? next ?? accelerator.value
|
||||
// Value is intentionally coarse (customized / reset) — the exact key
|
||||
// combo is a fingerprinting-adjacent detail no dashboard needs.
|
||||
trackSettingsChanged({
|
||||
setting_name: 'spotlight_shortcut',
|
||||
new_value: next ? 'customized' : 'reset_to_default',
|
||||
source: 'settings',
|
||||
})
|
||||
}
|
||||
catch {
|
||||
toast.error(tt('errors.failed'))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables'
|
||||
import { useWindowFocus } from '@vueuse/core'
|
||||
import { shallowRef, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
|
@ -17,6 +18,7 @@ const sending = shallowRef(false)
|
|||
const inputRef = useTemplateRef<HTMLInputElement>('inputRef')
|
||||
|
||||
const chatSyncStore = useChatSyncStore()
|
||||
const { trackSpotlightUsed } = useAnalytics()
|
||||
const hideSpotlightWindow = useElectronEventaInvoke(electronSpotlightHide)
|
||||
const showResultNotification = useElectronEventaInvoke(electronSpotlightShowResultNotification)
|
||||
const { t } = useI18n()
|
||||
|
|
@ -39,6 +41,7 @@ async function handleSend() {
|
|||
|
||||
messageInput.value = ''
|
||||
sending.value = true
|
||||
trackSpotlightUsed()
|
||||
|
||||
try {
|
||||
await hideSpotlightWindow()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import type { WidgetsIframeRequestPayload, WidgetsIframeRequestResultPayload, WidgetSnapshot, WidgetWindowSize } from '../../shared/eventa'
|
||||
|
||||
import { useElectronEventaContext, useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables'
|
||||
import { computed, defineAsyncComponent, defineComponent, h, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
|
@ -100,6 +101,8 @@ async function requestSnapshot(id: string) {
|
|||
}
|
||||
}
|
||||
|
||||
const { trackWidgetOpened } = useAnalytics()
|
||||
|
||||
watch(widgetId, (id) => {
|
||||
clearTtl()
|
||||
widget.value = null
|
||||
|
|
@ -107,6 +110,7 @@ watch(widgetId, (id) => {
|
|||
loading.value = false
|
||||
if (!id)
|
||||
return
|
||||
trackWidgetOpened({ widget_id: id })
|
||||
requestSnapshot(id)
|
||||
}, { immediate: true })
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables'
|
||||
import { applyOIDCTokens, fetchSession, triggerSignIn } from '@proj-airi/stage-ui/libs/auth'
|
||||
import { consumeFlowState, exchangeCodeForTokens } from '@proj-airi/stage-ui/libs/auth-oidc'
|
||||
import { Button } from '@proj-airi/ui'
|
||||
|
|
@ -9,6 +10,7 @@ import { useRouter } from 'vue-router'
|
|||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const { trackOauthCallbackFailed } = useAnalytics()
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
onMounted(async () => {
|
||||
|
|
@ -18,17 +20,20 @@ onMounted(async () => {
|
|||
const errorParam = url.searchParams.get('error')
|
||||
|
||||
if (errorParam) {
|
||||
trackOauthCallbackFailed({ stage: 'provider_error' })
|
||||
error.value = url.searchParams.get('error_description') ?? errorParam
|
||||
return
|
||||
}
|
||||
|
||||
if (!code || !state) {
|
||||
trackOauthCallbackFailed({ stage: 'missing_code_or_state' })
|
||||
error.value = t('server.auth.webCallback.message.missingCodeOrState')
|
||||
return
|
||||
}
|
||||
|
||||
const persisted = consumeFlowState()
|
||||
if (!persisted) {
|
||||
trackOauthCallbackFailed({ stage: 'missing_flow_state' })
|
||||
error.value = t('server.auth.webCallback.message.missingFlowState')
|
||||
return
|
||||
}
|
||||
|
|
@ -40,6 +45,7 @@ onMounted(async () => {
|
|||
router.replace('/')
|
||||
}
|
||||
catch (err) {
|
||||
trackOauthCallbackFailed({ stage: 'token_exchange_failed' })
|
||||
error.value = errorMessageFrom(err) ?? t('server.auth.webCallback.message.tokenExchangeFailed')
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const emit = defineEmits<{
|
|||
}>()
|
||||
|
||||
const characterStore = useCharacterStore()
|
||||
const { trackCharacterCreated } = useAnalytics()
|
||||
const { trackCharacterCreated, trackCharacterUpdated } = useAnalytics()
|
||||
|
||||
// Form State
|
||||
const form = reactive({
|
||||
|
|
@ -154,6 +154,7 @@ async function handleSubmit() {
|
|||
version: form.version,
|
||||
coverUrl: form.coverUrl,
|
||||
})
|
||||
trackCharacterUpdated({ character_id: props.character.id })
|
||||
// Capabilities/I18n update not supported in simple UpdateCharacterSchema yet?
|
||||
// Checking types/character.ts: UpdateCharacterSchema only has version, coverUrl, characterId.
|
||||
// So deep update is not supported by the simple endpoint yet?
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import type { Character } from '@proj-airi/stage-ui/types/character'
|
||||
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables'
|
||||
import { useCharacterStore } from '@proj-airi/stage-ui/stores/characters'
|
||||
import { Button, FieldInput } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
|
@ -9,6 +10,7 @@ import { computed, onMounted, ref } from 'vue'
|
|||
import CharacterDialog from './components/CharacterDialog.vue'
|
||||
import CharacterItem from './components/CharacterItem.vue'
|
||||
|
||||
const { trackCharacterDeleted } = useAnalytics()
|
||||
const characterStore = useCharacterStore()
|
||||
const { characters } = storeToRefs(characterStore)
|
||||
|
||||
|
|
@ -45,7 +47,9 @@ function handleDelete(id: string) {
|
|||
// TODO: Remove this
|
||||
// eslint-disable-next-line no-alert
|
||||
if (confirm('Are you sure you want to delete this character?')) {
|
||||
characterStore.remove(id).catch(console.error)
|
||||
characterStore.remove(id)
|
||||
.then(() => trackCharacterDeleted({ character_id: id }))
|
||||
.catch(console.error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { routes } from 'vue-router/auto-routes'
|
|||
|
||||
import App from './App.vue'
|
||||
|
||||
import { initAuthAnalytics } from './modules/analytics'
|
||||
import { AUTH_UI_ROUTER_BASE_PATH } from './modules/auth-ui-base'
|
||||
import { i18n } from './modules/i18n'
|
||||
|
||||
|
|
@ -23,6 +24,8 @@ import 'vue-sonner/style.css'
|
|||
import './styles/main.css'
|
||||
import 'uno.css'
|
||||
|
||||
initAuthAnalytics()
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
// TODO: vite-plugin-vue-layouts is long deprecated, replace with another layout solution
|
||||
|
|
|
|||
167
apps/ui-server-auth/src/modules/analytics.ts
Normal file
167
apps/ui-server-auth/src/modules/analytics.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
/**
|
||||
* PostHog product analytics for the auth-only SPA (`apps/ui-server-auth`).
|
||||
*
|
||||
* This surface is the top of the activation funnel: sign-up, sign-in, email
|
||||
* verification, password recovery. Events captured here are the funnel
|
||||
* entry that the in-app surfaces (`signup_completed → onboarding_started →
|
||||
* first message`) join against, keyed by the Better Auth user id passed to
|
||||
* {@link identifyAuthUser} — the same id the server uses as `distinctId`
|
||||
* for its own events, so the person profiles merge.
|
||||
*
|
||||
* Unlike the stage apps there is no in-app analytics consent toggle here
|
||||
* (the user isn't signed in yet, so there's no settings store to read).
|
||||
* Capture posture matches the docs site: enabled in analytics-enabled
|
||||
* builds (`VITE_ENABLE_POSTHOG`), disclosed via the privacy policy linked
|
||||
* on the sign-in page.
|
||||
*/
|
||||
|
||||
import type { OauthCallbackFailureStage } from '@proj-airi/stage-ui/composables'
|
||||
|
||||
import posthog from 'posthog-js'
|
||||
|
||||
import {
|
||||
DEFAULT_POSTHOG_CONFIG,
|
||||
POSTHOG_ENABLED,
|
||||
POSTHOG_PROJECT_KEY,
|
||||
} from '../../../../posthog.config'
|
||||
|
||||
/** Login/signup credential kinds shown on the sign-in page. */
|
||||
export type AuthMethod = 'email' | 'github' | 'google'
|
||||
|
||||
let initialized = false
|
||||
|
||||
/**
|
||||
* Initialize PostHog for the auth surface. Call once from `main.ts` before
|
||||
* mount; later calls are no-ops. Returns whether capture is active so
|
||||
* callers can skip building event payloads in analytics-disabled builds.
|
||||
*/
|
||||
export function initAuthAnalytics(): boolean {
|
||||
if (!POSTHOG_ENABLED)
|
||||
return false
|
||||
|
||||
if (initialized)
|
||||
return true
|
||||
|
||||
posthog.init(POSTHOG_PROJECT_KEY, { ...DEFAULT_POSTHOG_CONFIG })
|
||||
// Same single-project setup as the stage apps: the `surface` super
|
||||
// property is how auth traffic is told apart in shared dashboards.
|
||||
posthog.register({ surface: 'auth' })
|
||||
initialized = true
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge this browser's anonymous events with the Better Auth user person.
|
||||
* `userId` must be the Better Auth `user.id` — the same value the server
|
||||
* uses as `distinctId` (see `apps/server` product events forwarding).
|
||||
*/
|
||||
export function identifyAuthUser(userId: string): void {
|
||||
if (!initialized)
|
||||
return
|
||||
posthog.identify(userId)
|
||||
}
|
||||
|
||||
interface CaptureOptions {
|
||||
/**
|
||||
* Set when navigation immediately follows the capture call
|
||||
* (`window.location.href = ...`). The batched queue would race the
|
||||
* unload and drop the event; sendBeacon survives it.
|
||||
*/
|
||||
beforeNavigation?: boolean
|
||||
}
|
||||
|
||||
function capture(event: string, properties: Record<string, unknown>, options?: CaptureOptions): void {
|
||||
if (!initialized)
|
||||
return
|
||||
|
||||
posthog.capture(
|
||||
event,
|
||||
properties,
|
||||
options?.beforeNavigation ? { send_instantly: true, transport: 'sendBeacon' } : undefined,
|
||||
)
|
||||
}
|
||||
|
||||
/** Activation funnel step 1 — the account now exists (email flow). */
|
||||
export function trackSignupCompleted(properties: { source: AuthMethod, requires_verification: boolean }): void {
|
||||
capture('signup_completed', properties, { beforeNavigation: !properties.requires_verification })
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth flows leave the page before their outcome is knowable, so the
|
||||
* client can only record the attempt; completion shows up as the
|
||||
* identified session on the callback landing.
|
||||
*/
|
||||
export function trackLoginStarted(properties: { method: AuthMethod }): void {
|
||||
capture('login_started', properties, { beforeNavigation: true })
|
||||
}
|
||||
|
||||
/** Credential sign-in succeeded; OIDC continuation navigation follows. */
|
||||
export function trackLoginSucceeded(properties: { method: AuthMethod }): void {
|
||||
capture('login_succeeded', properties, { beforeNavigation: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign-in attempt failed. No error detail on purpose — auth error messages
|
||||
* can embed the email address, and the count per method is what the funnel
|
||||
* needs.
|
||||
*/
|
||||
export function trackLoginFailed(properties: { method: AuthMethod }): void {
|
||||
capture('login_failed', properties)
|
||||
}
|
||||
|
||||
/** Verification link landing with `?verified=true`. */
|
||||
export function trackEmailVerificationCompleted(): void {
|
||||
capture('email_verification_completed', {})
|
||||
}
|
||||
|
||||
/** Verification link landing with `?error=...`. */
|
||||
export function trackEmailVerificationFailed(): void {
|
||||
capture('email_verification_failed', {})
|
||||
}
|
||||
|
||||
export function trackPasswordResetRequested(): void {
|
||||
capture('password_reset_requested', {})
|
||||
}
|
||||
|
||||
export function trackPasswordResetCompleted(): void {
|
||||
capture('password_reset_completed', {})
|
||||
}
|
||||
|
||||
export function trackPasswordChanged(): void {
|
||||
capture('password_changed', {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Link handed off to the provider's consent page. Completion is not
|
||||
* client-observable (it lands back via a full-page OAuth redirect), so the
|
||||
* funnel pairs this with the refreshed linked-accounts state server-side.
|
||||
*/
|
||||
export function trackOauthProviderLinkStarted(properties: { provider: string }): void {
|
||||
capture('oauth_provider_link_started', properties, { beforeNavigation: true })
|
||||
}
|
||||
|
||||
export function trackOauthProviderUnlinked(properties: { provider: string }): void {
|
||||
capture('oauth_provider_unlinked', properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletion-confirmed landing page reached (`delete-account.vue`). The
|
||||
* deletion request itself is raised from the stage apps' account settings.
|
||||
*/
|
||||
export function trackAccountDeletionCompleted(): void {
|
||||
capture('account_deletion_completed', {})
|
||||
}
|
||||
|
||||
export function trackSignedOut(): void {
|
||||
capture('signed_out', {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Electron OIDC relay handoff failed. `stage` distinguishes a malformed
|
||||
* callback (`parse`) from an unreachable local app (`relay_unreachable`);
|
||||
* the full cross-surface vocabulary lives in stage-ui's
|
||||
* `OauthCallbackFailureStage` so the two emitters share one schema.
|
||||
*/
|
||||
export function trackOauthCallbackFailed(properties: { stage: Extract<OauthCallbackFailureStage, 'parse' | 'relay_unreachable'> }): void {
|
||||
capture('oauth_callback_failed', properties)
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { trackAccountDeletionCompleted } from '../modules/analytics'
|
||||
|
||||
// NOTICE:
|
||||
// This page is a SUCCESS landing — better-auth's `/api/auth/delete-user/callback`
|
||||
// performs the actual deletion server-side, then redirects here via the
|
||||
|
|
@ -28,6 +30,14 @@ const errorMessage = computed(() => {
|
|||
const value = route.query.error
|
||||
return typeof value === 'string' ? value : null
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// Strongest churn fact the client can observe: better-auth only
|
||||
// redirects here after the deletion callback already ran server-side,
|
||||
// so rendering the success state means the account is gone.
|
||||
if (!errorMessage.value)
|
||||
trackAccountDeletionCompleted()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { onMounted, shallowRef } from 'vue'
|
|||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { parseElectronCallbackQuery } from '../composables/electron-callback.shared'
|
||||
import { trackOauthCallbackFailed } from '../modules/analytics'
|
||||
import { getServerAuthBootstrapContext } from '../modules/server-auth-context'
|
||||
|
||||
type CallbackStatus = 'loading' | 'success' | 'fallback' | 'error'
|
||||
|
|
@ -55,6 +56,7 @@ async function runRelayFlow() {
|
|||
const parsed = parseElectronCallbackQuery(query)
|
||||
|
||||
if (parsed.status === 'error') {
|
||||
trackOauthCallbackFailed({ stage: 'parse' })
|
||||
setViewModel({
|
||||
description: t('server.auth.electronCallback.message.invalidResponse'),
|
||||
detail: parsed.message,
|
||||
|
|
@ -98,6 +100,7 @@ async function runRelayFlow() {
|
|||
}, 1200)
|
||||
}
|
||||
catch {
|
||||
trackOauthCallbackFailed({ stage: 'relay_unreachable' })
|
||||
setViewModel({
|
||||
description: t('server.auth.electronCallback.message.loopbackUnreachable'),
|
||||
detail: t('server.auth.electronCallback.message.tryOpenDirectly'),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { Button, FieldInput } from '@proj-airi/ui'
|
|||
import { reactive, shallowRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { trackPasswordResetRequested } from '../modules/analytics'
|
||||
import { buildCurrentOriginAuthUiUrl } from '../modules/auth-ui-base'
|
||||
import { describeAuthError, requestPasswordReset } from '../modules/email-password'
|
||||
import { getServerAuthBootstrapContext } from '../modules/server-auth-context'
|
||||
|
|
@ -36,6 +37,7 @@ async function handleSubmit(event: Event) {
|
|||
email: form.email.trim(),
|
||||
redirectTo: resetRedirect,
|
||||
})
|
||||
trackPasswordResetRequested()
|
||||
submitted.value = true
|
||||
}
|
||||
catch (error) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,14 @@ import { computed, onMounted, reactive, shallowRef } from 'vue'
|
|||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import {
|
||||
identifyAuthUser,
|
||||
trackOauthProviderLinkStarted,
|
||||
trackOauthProviderUnlinked,
|
||||
trackPasswordChanged,
|
||||
trackPasswordResetRequested,
|
||||
trackSignedOut,
|
||||
} from '../modules/analytics'
|
||||
import { getAuthClient } from '../modules/auth-client'
|
||||
import { requestPasswordReset } from '../modules/email-password'
|
||||
import {
|
||||
|
|
@ -98,6 +106,8 @@ const {
|
|||
unlinked: provider => t('server.auth.profile.linkedAccounts.message.unlinked', { provider }),
|
||||
linkStarted: provider => t('server.auth.profile.linkedAccounts.message.linkStarted', { provider }),
|
||||
},
|
||||
onUnlinked: providerId => trackOauthProviderUnlinked({ provider: providerId }),
|
||||
onLinkStarted: providerId => trackOauthProviderLinkStarted({ provider: providerId }),
|
||||
})
|
||||
|
||||
const nameDirty = computed(() => {
|
||||
|
|
@ -138,6 +148,9 @@ onMounted(async () => {
|
|||
// call needed here.
|
||||
user.value = result.user
|
||||
profileForm.name = result.user.name
|
||||
// Merge this browser's anonymous funnel events (sign-in page views,
|
||||
// login_started, …) into the Better Auth user person.
|
||||
identifyAuthUser(result.user.id)
|
||||
}
|
||||
catch (error) {
|
||||
profileError.value = describeProfileError(error) || t('server.auth.profile.error.loadFailed')
|
||||
|
|
@ -200,6 +213,7 @@ async function handleChangePassword(event: Event) {
|
|||
passwordForm.next = ''
|
||||
passwordForm.confirm = ''
|
||||
passwordSuccess.value = t('server.auth.profile.message.passwordChanged')
|
||||
trackPasswordChanged()
|
||||
}
|
||||
catch (error) {
|
||||
passwordError.value = describeProfileError(error) || t('server.auth.profile.error.changePasswordFailed')
|
||||
|
|
@ -237,6 +251,7 @@ async function handleSendSetPasswordLink() {
|
|||
redirectTo: new URL('/auth/reset-password', apiServerUrl).toString(),
|
||||
})
|
||||
setPasswordSuccess.value = t('server.auth.profile.password.setLinkSent', { email: user.value.email })
|
||||
trackPasswordResetRequested()
|
||||
}
|
||||
catch (error) {
|
||||
setPasswordError.value = describeProfileError(error)
|
||||
|
|
@ -256,6 +271,7 @@ async function handleSignOut() {
|
|||
|
||||
try {
|
||||
await signOut({ apiServerUrl })
|
||||
trackSignedOut()
|
||||
await router.replace('/sign-in')
|
||||
}
|
||||
catch (error) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { computed, reactive, shallowRef } from 'vue'
|
|||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { trackPasswordResetCompleted } from '../modules/analytics'
|
||||
import { describeAuthError, resetPasswordWithToken } from '../modules/email-password'
|
||||
import { getServerAuthBootstrapContext } from '../modules/server-auth-context'
|
||||
|
||||
|
|
@ -52,6 +53,7 @@ async function handleSubmit(event: Event) {
|
|||
newPassword: form.password,
|
||||
token: token.value,
|
||||
})
|
||||
trackPasswordResetCompleted()
|
||||
completed.value = true
|
||||
}
|
||||
catch (error) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@ import { computed, reactive, shallowRef, watch } from 'vue'
|
|||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import {
|
||||
trackLoginFailed,
|
||||
trackLoginStarted,
|
||||
trackLoginSucceeded,
|
||||
trackSignupCompleted,
|
||||
} from '../modules/analytics'
|
||||
import { buildCurrentOriginAuthUiUrl } from '../modules/auth-ui-base'
|
||||
import {
|
||||
checkEmail,
|
||||
|
|
@ -127,9 +133,11 @@ async function handleProviderSelect(provider: OAuthProvider) {
|
|||
callbackURL: effectiveCallbackURL.value,
|
||||
})
|
||||
|
||||
trackLoginStarted({ method: provider })
|
||||
window.location.href = redirectUrl
|
||||
}
|
||||
catch (error) {
|
||||
trackLoginFailed({ method: provider })
|
||||
errorMessage.value = describeAuthError(error) || t('server.auth.signIn.error.fallback')
|
||||
pendingProvider.value = null
|
||||
}
|
||||
|
|
@ -202,9 +210,11 @@ async function handleEmailSignIn(event: Event) {
|
|||
// After a successful credential sign-in better-auth has set the session
|
||||
// cookie. Bounce into the OIDC `/oauth2/authorize` flow (or wherever the
|
||||
// OIDC client originally pointed) so the upstream stage app gets its tokens.
|
||||
trackLoginSucceeded({ method: 'email' })
|
||||
window.location.href = result.redirectURL ?? effectiveCallbackURL.value
|
||||
}
|
||||
catch (error) {
|
||||
trackLoginFailed({ method: 'email' })
|
||||
errorMessage.value = describeAuthError(error) || t('server.auth.signIn.error.fallback')
|
||||
}
|
||||
finally {
|
||||
|
|
@ -237,6 +247,7 @@ async function handleEmailSignUp(event: Event) {
|
|||
})
|
||||
|
||||
if (result.requiresVerification) {
|
||||
trackSignupCompleted({ source: 'email', requires_verification: true })
|
||||
await router.push({
|
||||
path: '/verify-email',
|
||||
query: {
|
||||
|
|
@ -249,6 +260,7 @@ async function handleEmailSignUp(event: Event) {
|
|||
|
||||
// Verification disabled at server config: session is live, fall through
|
||||
// to the OIDC continuation just like sign-in.
|
||||
trackSignupCompleted({ source: 'email', requires_verification: false })
|
||||
window.location.href = effectiveCallbackURL.value
|
||||
}
|
||||
catch (error) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { computed, onMounted, watch } from 'vue'
|
|||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { trackEmailVerificationCompleted, trackEmailVerificationFailed } from '../modules/analytics'
|
||||
import { buildCurrentOriginAuthUiUrl } from '../modules/auth-ui-base'
|
||||
import { getServerAuthBootstrapContext } from '../modules/server-auth-context'
|
||||
|
||||
|
|
@ -87,13 +88,16 @@ onMounted(async () => {
|
|||
// session cookie has been written, then stay put so the user sees the
|
||||
// success message. The pending tab does the OIDC continuation.
|
||||
if (verified.value) {
|
||||
trackEmailVerificationCompleted()
|
||||
if (isSupported.value)
|
||||
post('verified')
|
||||
return
|
||||
}
|
||||
|
||||
if (error.value)
|
||||
if (error.value) {
|
||||
trackEmailVerificationFailed()
|
||||
return
|
||||
}
|
||||
|
||||
// Pending tab: cover the case where verification already happened before
|
||||
// this tab subscribed (back-button navigation, page reload, etc.). One
|
||||
|
|
|
|||
|
|
@ -19640,7 +19640,7 @@
|
|||
}
|
||||
},
|
||||
"isOneTime": false,
|
||||
"monthlyDollars": 8,
|
||||
"monthlyDollars": -1,
|
||||
"privacyLevel": "PUBLIC",
|
||||
"tierName": "Patreon",
|
||||
"createdAt": "2026-04-03T08:34:28.264+00:00",
|
||||
|
|
|
|||
|
|
@ -409,8 +409,14 @@ pages:
|
|||
systemprompt: System Prompt
|
||||
title: AIRI Card
|
||||
try_different_search: Try a different search term
|
||||
export: Export
|
||||
exported: Card package exported
|
||||
export_failed: Failed to export card package
|
||||
imported: Card package imported
|
||||
import_missing_file: Card package is missing required files
|
||||
import_invalid_file: Card package file is invalid
|
||||
upload: Upload
|
||||
upload_desc: Click or drag file to upload
|
||||
upload_desc: Click or drag an AIRI card package to import
|
||||
memory:
|
||||
description: Where memories got stored, and organized
|
||||
title: Memory
|
||||
|
|
|
|||
|
|
@ -390,8 +390,14 @@ pages:
|
|||
systemprompt: Prompt del Sistema
|
||||
title: Tarjeta AIRI
|
||||
try_different_search: Prueba un término de búsqueda diferente
|
||||
export: Exportar
|
||||
exported: Paquete de tarjeta exportado
|
||||
export_failed: No se pudo exportar el paquete de tarjeta
|
||||
imported: Paquete de tarjeta importado
|
||||
import_missing_file: Al paquete de tarjeta le faltan archivos necesarios
|
||||
import_invalid_file: El archivo del paquete de tarjeta no es válido
|
||||
upload: Subir
|
||||
upload_desc: Haz clic o arrastra el archivo para subir
|
||||
upload_desc: Haz clic o arrastra un paquete de tarjeta AIRI para importar
|
||||
memory:
|
||||
description: Donde se almacenan y organizan las memorias
|
||||
title: Memoria
|
||||
|
|
|
|||
|
|
@ -390,8 +390,14 @@ pages:
|
|||
systemprompt: Prompt système
|
||||
title: Carte AIRI
|
||||
try_different_search: Essayez un autre terme de recherche
|
||||
export: Exporter
|
||||
exported: Paquet de carte exporté
|
||||
export_failed: Impossible d’exporter le paquet de carte
|
||||
imported: Paquet de carte importé
|
||||
import_missing_file: Le paquet de carte ne contient pas les fichiers requis
|
||||
import_invalid_file: Le fichier du paquet de carte est invalide
|
||||
upload: Téléverser
|
||||
upload_desc: Cliquez ou glissez un fichier pour téléverser
|
||||
upload_desc: Cliquez ou glissez un paquet de carte AIRI pour l’importer
|
||||
memory:
|
||||
description: Endroit où les souvenirs sont stockés et organisés
|
||||
title: Mémoire
|
||||
|
|
|
|||
|
|
@ -390,8 +390,14 @@ pages:
|
|||
systemprompt: システムプロンプト
|
||||
title: AIRIカード
|
||||
try_different_search: 別の検索ワードを試してください
|
||||
export: エクスポート
|
||||
exported: カードパッケージをエクスポートしました
|
||||
export_failed: カードパッケージのエクスポートに失敗しました
|
||||
imported: カードパッケージをインポートしました
|
||||
import_missing_file: カードパッケージに必要なファイルがありません
|
||||
import_invalid_file: カードパッケージファイルが正しくありません
|
||||
upload: アップロード
|
||||
upload_desc: ファイルをアップロードするにはクリックまたはドラッグ
|
||||
upload_desc: AIRI カードパッケージをクリックまたはドラッグしてインポート
|
||||
memory:
|
||||
description: 記憶が保存され、整理される場所
|
||||
title: 記憶
|
||||
|
|
|
|||
|
|
@ -390,8 +390,14 @@ pages:
|
|||
systemprompt: 시스템 프롬프트
|
||||
title: AIRI 카드
|
||||
try_different_search: 다른 검색어를 시도해보세요
|
||||
export: 내보내기
|
||||
exported: 카드 패키지를 내보냈습니다
|
||||
export_failed: 카드 패키지를 내보내지 못했습니다
|
||||
imported: 카드 패키지를 가져왔습니다
|
||||
import_missing_file: 카드 패키지에 필요한 파일이 없습니다
|
||||
import_invalid_file: 카드 패키지 파일이 올바르지 않습니다
|
||||
upload: 업로드
|
||||
upload_desc: 클릭 또는 파일을 드래그하여 업로드
|
||||
upload_desc: AIRI 카드 패키지를 클릭하거나 드래그하여 가져오기
|
||||
memory:
|
||||
description: 기억이 저장되고 정리된 곳
|
||||
title: 메모리
|
||||
|
|
|
|||
|
|
@ -390,8 +390,14 @@ pages:
|
|||
systemprompt: Системный промпт
|
||||
title: Карта AIRI
|
||||
try_different_search: Попробуйте другой поисковый запрос
|
||||
export: Экспорт
|
||||
exported: Пакет карточки экспортирован
|
||||
export_failed: Не удалось экспортировать пакет карточки
|
||||
imported: Пакет карточки импортирован
|
||||
import_missing_file: В пакете карточки отсутствуют необходимые файлы
|
||||
import_invalid_file: Файл пакета карточки недействителен
|
||||
upload: Загрузить
|
||||
upload_desc: Нажмите или перетащите файл для загрузки
|
||||
upload_desc: Нажмите или перетащите пакет карточки AIRI для импорта
|
||||
memory:
|
||||
description: Хранилище и организация воспоминаний
|
||||
title: Память
|
||||
|
|
|
|||
|
|
@ -390,8 +390,14 @@ pages:
|
|||
systemprompt: Lời nhắc hệ thống
|
||||
title: Thẻ AIRI
|
||||
try_different_search: Hãy thử từ khóa khác
|
||||
export: Xuất
|
||||
exported: Đã xuất gói thẻ
|
||||
export_failed: Không thể xuất gói thẻ
|
||||
imported: Đã nhập gói thẻ
|
||||
import_missing_file: Gói thẻ thiếu tệp bắt buộc
|
||||
import_invalid_file: Tệp gói thẻ không hợp lệ
|
||||
upload: Tải lên
|
||||
upload_desc: Bấm hoặc kéo thả tệp để tải lên
|
||||
upload_desc: Bấm hoặc kéo thả gói thẻ AIRI để nhập
|
||||
memory:
|
||||
description: Nơi lưu trữ và tổ chức ký ức
|
||||
title: Bộ nhớ
|
||||
|
|
|
|||
|
|
@ -390,8 +390,14 @@ pages:
|
|||
systemprompt: 系统提示词
|
||||
title: AIRI 角色卡
|
||||
try_different_search: 尝试使用其他关键词搜索
|
||||
export: 导出
|
||||
exported: 角色卡包已导出
|
||||
export_failed: 角色卡包导出失败
|
||||
imported: 角色卡包已导入
|
||||
import_missing_file: 角色卡包缺少必要文件
|
||||
import_invalid_file: 角色卡包文件错误
|
||||
upload: 上传
|
||||
upload_desc: 点击或拖拽文件到此处上传
|
||||
upload_desc: 点击或拖拽 AIRI 角色卡包到此处导入
|
||||
memory:
|
||||
description: 存放记忆的地方,以及策略
|
||||
title: 记忆体
|
||||
|
|
|
|||
|
|
@ -390,8 +390,14 @@ pages:
|
|||
systemprompt: 系統提示詞
|
||||
title: AIRI 角色卡
|
||||
try_different_search: 嘗試使用其他關鍵字搜尋
|
||||
export: 匯出
|
||||
exported: 角色卡包已匯出
|
||||
export_failed: 角色卡包匯出失敗
|
||||
imported: 角色卡包已匯入
|
||||
import_missing_file: 角色卡包缺少必要檔案
|
||||
import_invalid_file: 角色卡包檔案錯誤
|
||||
upload: 上傳
|
||||
upload_desc: 點擊或拖曳檔案到此處上傳
|
||||
upload_desc: 點擊或拖曳 AIRI 角色卡包到此處匯入
|
||||
memory:
|
||||
description: 存放記憶的地方,以及策略
|
||||
title: 記憶體
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { defaultSignInProviders } from '@proj-airi/stage-ui/components/auth'
|
||||
import { resolveLinkedAccountOAuthErrorMessageKey, useLinkedAccounts } from '@proj-airi/stage-ui/composables'
|
||||
import { resolveLinkedAccountOAuthErrorMessageKey, useAnalytics, useLinkedAccounts } from '@proj-airi/stage-ui/composables'
|
||||
import { authClient } from '@proj-airi/stage-ui/libs/auth'
|
||||
import { SERVER_URL } from '@proj-airi/stage-ui/libs/server'
|
||||
import { useAuthStore } from '@proj-airi/stage-ui/stores/auth'
|
||||
|
|
@ -22,6 +22,13 @@ const emit = defineEmits<{
|
|||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const {
|
||||
trackAccountDeletionRequested,
|
||||
trackOauthProviderLinkStarted,
|
||||
trackOauthProviderUnlinked,
|
||||
trackPasswordChanged,
|
||||
trackPasswordResetRequested,
|
||||
} = useAnalytics()
|
||||
const authStore = useAuthStore()
|
||||
const { isAuthenticated, user, credits } = storeToRefs(authStore)
|
||||
|
||||
|
|
@ -157,6 +164,8 @@ const {
|
|||
unlinked: provider => t('settings.pages.account.connections.message.unlinked', { provider }),
|
||||
linkStarted: provider => t('settings.pages.account.connections.message.linkStarted', { provider }),
|
||||
},
|
||||
onUnlinked: providerId => trackOauthProviderUnlinked({ provider: providerId }),
|
||||
onLinkStarted: providerId => trackOauthProviderLinkStarted({ provider: providerId }),
|
||||
})
|
||||
|
||||
watch(
|
||||
|
|
@ -279,6 +288,7 @@ async function handleChangePassword(event: Event) {
|
|||
passwordForm.next = ''
|
||||
passwordForm.confirm = ''
|
||||
passwordSuccess.value = t('settings.pages.account.security.message.changed')
|
||||
trackPasswordChanged()
|
||||
}
|
||||
catch (error) {
|
||||
passwordError.value = errorMessageFrom(error) ?? t('settings.pages.account.security.error.fallback')
|
||||
|
|
@ -316,6 +326,7 @@ async function handleSendSetPasswordLink() {
|
|||
if (error)
|
||||
throw new Error(error.message ?? 'requestPasswordReset failed')
|
||||
setPasswordSuccess.value = t('settings.pages.account.security.message.setLinkSent', { email })
|
||||
trackPasswordResetRequested()
|
||||
}
|
||||
catch (error) {
|
||||
setPasswordError.value = errorMessageFrom(error) ?? t('settings.pages.account.security.error.setLinkFailed')
|
||||
|
|
@ -390,6 +401,7 @@ async function handleConfirmDelete(event: Event) {
|
|||
|
||||
deleteSent.value = true
|
||||
deleteDialogOpen.value = false
|
||||
trackAccountDeletionRequested()
|
||||
}
|
||||
catch (error) {
|
||||
deleteError.value = errorMessageFrom(error) ?? t('settings.pages.account.danger.deleteAccount.error.fallback')
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type { AiriExtension } from '@proj-airi/stage-ui/stores/modules/airi-card
|
|||
import kebabcase from '@stdlib/string-base-kebabcase'
|
||||
|
||||
import { isCustomProvidersDisabled } from '@proj-airi/stage-shared'
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables'
|
||||
import { DEFAULT_ARTISTRY_WIDGET_INSTRUCTION } from '@proj-airi/stage-ui/constants/prompts/artistry-instruction'
|
||||
import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models'
|
||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||
|
|
@ -58,6 +59,7 @@ const emit = defineEmits<{
|
|||
const modelValue = defineModel<boolean>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { trackCardEdited } = useAnalytics()
|
||||
const cardStore = useAiriCardStore()
|
||||
const consciousnessStore = useConsciousnessStore()
|
||||
const visionStore = useVisionStore()
|
||||
|
|
@ -417,10 +419,11 @@ function saveCard(card: Card): boolean {
|
|||
if (isEditMode.value && props.cardId) {
|
||||
// Edit mode: update existing card
|
||||
cardStore.updateCard(props.cardId, cardWithModules)
|
||||
trackCardEdited({ card_id: props.cardId })
|
||||
}
|
||||
else {
|
||||
// Create mode: add new card
|
||||
cardStore.addCard(cardWithModules)
|
||||
cardStore.addCard(cardWithModules, 'scratch')
|
||||
}
|
||||
|
||||
modelValue.value = false // Close this
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ import type { AiriCard } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
|||
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables'
|
||||
import { useDownload } from '@proj-airi/stage-ui/composables/download'
|
||||
import { exportAiriCardPackage } from '@proj-airi/stage-ui/services/airi-card-import-export'
|
||||
import { useBackgroundStore } from '@proj-airi/stage-ui/stores/background'
|
||||
import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models'
|
||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||
|
|
@ -17,8 +21,9 @@ import {
|
|||
DialogRoot,
|
||||
DialogTitle,
|
||||
} from 'reka-ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, ref, shallowRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import DeleteCardDialog from './DeleteCardDialog.vue'
|
||||
|
||||
|
|
@ -34,11 +39,13 @@ const emit = defineEmits<{
|
|||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { trackSceneBackgroundSet } = useAnalytics()
|
||||
const cardStore = useAiriCardStore()
|
||||
const consciousnessStore = useConsciousnessStore()
|
||||
const speechStore = useSpeechStore()
|
||||
const visionStore = useVisionStore()
|
||||
const backgroundStore = useBackgroundStore()
|
||||
const displayModelsStore = useDisplayModelsStore()
|
||||
|
||||
const { removeCard } = cardStore
|
||||
const { activeCardId } = storeToRefs(cardStore)
|
||||
|
|
@ -47,6 +54,7 @@ const { activeSpeechProvider: speechProvider, activeSpeechModel: defaultSpeechMo
|
|||
const { activeProvider: visionProvider, activeModel: defaultVisionModel } = storeToRefs(visionStore)
|
||||
|
||||
const isRefreshingGallery = ref(false)
|
||||
const isExportingCard = shallowRef(false)
|
||||
|
||||
// Get selected card data
|
||||
const selectedCard = computed<AiriCard | undefined>(() => {
|
||||
|
|
@ -113,6 +121,27 @@ function handleActivate() {
|
|||
}, 300)
|
||||
}
|
||||
|
||||
async function handleExportCard() {
|
||||
if (!selectedCard.value)
|
||||
return
|
||||
|
||||
isExportingCard.value = true
|
||||
try {
|
||||
useDownload(
|
||||
await exportAiriCardPackage({ card: selectedCard.value, displayModelsStore }),
|
||||
`${selectedCard.value.name.trim()}.zip`,
|
||||
).download()
|
||||
toast(t('settings.pages.card.exported'))
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error exporting card package:', error)
|
||||
toast(t('settings.pages.card.export_failed'))
|
||||
}
|
||||
finally {
|
||||
isExportingCard.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function highlightTagToHtml(text: string) {
|
||||
return DOMPurify.sanitize(text?.replace(/\{\{(.*?)\}\}/g, '<span class="bg-primary-500/20 inline-block">{{ $1 }}</span>').trim())
|
||||
}
|
||||
|
|
@ -155,6 +184,7 @@ const activeBackgroundId = computed({
|
|||
...selectedCard.value,
|
||||
extensions: extension,
|
||||
})
|
||||
trackSceneBackgroundSet({ source: 'card_gallery', cleared: val === 'none' })
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -328,6 +358,13 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
|
|||
|
||||
<!-- Action buttons -->
|
||||
<div flex="~ row" gap-2>
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="i-solar:download-minimalistic-bold-duotone"
|
||||
:label="t('settings.pages.card.export')"
|
||||
:disabled="isExportingCard"
|
||||
@click="handleExportCard"
|
||||
/>
|
||||
<!-- Activation button -->
|
||||
<Button
|
||||
variant="primary"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import type { ccv3 } from '@proj-airi/ccc'
|
||||
|
||||
import { Alert } from '@proj-airi/stage-ui/components'
|
||||
import { AiriCardPackageError, importAiriCardPackage } from '@proj-airi/stage-ui/services/airi-card-import-export'
|
||||
import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models'
|
||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||
import { InputFileCard } from '@proj-airi/ui'
|
||||
import { ComboboxSelect } from '@proj-airi/ui/components/form'
|
||||
|
|
@ -9,6 +9,7 @@ import { storeToRefs } from 'pinia'
|
|||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import CardCreate from './components/CardCreate.vue'
|
||||
import CardCreationDialog from './components/CardCreationDialog.vue'
|
||||
|
|
@ -18,6 +19,7 @@ import DeleteCardDialog from './components/DeleteCardDialog.vue'
|
|||
|
||||
const { t } = useI18n()
|
||||
const cardStore = useAiriCardStore()
|
||||
const displayModelsStore = useDisplayModelsStore()
|
||||
const { addCard, removeCard } = cardStore
|
||||
const { cards, activeCardId } = storeToRefs(cardStore)
|
||||
|
||||
|
|
@ -57,15 +59,17 @@ watch(inputFiles, async (newFiles) => {
|
|||
return
|
||||
|
||||
try {
|
||||
const content = await file.text()
|
||||
const cardJSON = JSON.parse(content) as ccv3.CharacterCardV3
|
||||
|
||||
// Add card and select it
|
||||
selectedCardId.value = addCard(cardJSON)
|
||||
isCardDialogOpen.value = true
|
||||
addCard(await importAiriCardPackage({ file, displayModelsStore }), 'import')
|
||||
toast(t('settings.pages.card.imported'))
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error processing card file:', error)
|
||||
console.error('Error processing card package:', error)
|
||||
toast(t(error instanceof AiriCardPackageError && error.code === 'missing-file'
|
||||
? 'settings.pages.card.import_missing_file'
|
||||
: 'settings.pages.card.import_invalid_file'))
|
||||
}
|
||||
finally {
|
||||
inputFiles.value = []
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -267,7 +271,7 @@ function getModuleShortName(id: string, module: 'consciousness' | 'voice') {
|
|||
:class="{ 'grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-4 grid-auto-rows-[minmax(min-content,max-content)] grid-auto-flow-dense sm:grid-cols-[repeat(auto-fill,minmax(240px,1fr))] sm:gap-5 md:grid-cols-[repeat(auto-fill,minmax(220px,1fr))] lg:grid-cols-[repeat(auto-fill,minmax(250px,1fr))]': cards.size > 0 }"
|
||||
>
|
||||
<!-- Upload card -->
|
||||
<InputFileCard v-model="inputFiles" accept="*.json">
|
||||
<InputFileCard v-model="inputFiles" accept=".zip">
|
||||
<template #default="{ isDragging }">
|
||||
<template v-if="!isDragging">
|
||||
<div flex flex-col items-center>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import type { DataSettingsStatusEmits } from '../status'
|
||||
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables'
|
||||
import { useDataMaintenance } from '@proj-airi/stage-ui/composables/use-data-maintenance'
|
||||
import { Button, DoubleCheckButton } from '@proj-airi/ui'
|
||||
import { shallowRef, useTemplateRef } from 'vue'
|
||||
|
|
@ -10,6 +11,7 @@ import { createDataSettingsStatusHelpers } from '../status'
|
|||
|
||||
const emit = defineEmits<DataSettingsStatusEmits>()
|
||||
const { t } = useI18n()
|
||||
const { trackDataAction } = useAnalytics()
|
||||
const importFileInput = useTemplateRef<HTMLInputElement>('importFileInput')
|
||||
const importError = shallowRef('')
|
||||
const {
|
||||
|
|
@ -32,6 +34,7 @@ async function triggerExport() {
|
|||
anchor.download = `airi-chat-sessions-${new Date().toISOString()}.json`
|
||||
anchor.click()
|
||||
URL.revokeObjectURL(url)
|
||||
trackDataAction({ action: 'chats_exported' })
|
||||
emitStatus(t('settings.pages.data.status.exported'))
|
||||
}
|
||||
catch (error) {
|
||||
|
|
@ -42,6 +45,7 @@ async function triggerExport() {
|
|||
function deleteChats() {
|
||||
try {
|
||||
deleteAllChatSessions()
|
||||
trackDataAction({ action: 'chats_cleared' })
|
||||
emitStatus(t('settings.pages.data.status.chats_deleted'))
|
||||
}
|
||||
catch (error) {
|
||||
|
|
@ -60,6 +64,7 @@ async function handleImport(event: Event) {
|
|||
const parsed = JSON.parse(raw) as Record<string, unknown>
|
||||
await importChatSessions(parsed)
|
||||
importError.value = ''
|
||||
trackDataAction({ action: 'chats_imported' })
|
||||
emitStatus(t('settings.pages.data.status.imported'))
|
||||
}
|
||||
catch (error) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import type { DataSettingsStatusEmits } from '../status'
|
||||
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables'
|
||||
import { useDataMaintenance } from '@proj-airi/stage-ui/composables/use-data-maintenance'
|
||||
import { DoubleCheckButton } from '@proj-airi/ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
|
@ -9,12 +10,14 @@ import { createDataSettingsStatusHelpers } from '../status'
|
|||
|
||||
const emit = defineEmits<DataSettingsStatusEmits>()
|
||||
const { t } = useI18n()
|
||||
const { trackDataAction } = useAnalytics()
|
||||
const { deleteAllData, resetProvidersSettings } = useDataMaintenance()
|
||||
const { emitStatus, handleActionError } = createDataSettingsStatusHelpers(emit)
|
||||
|
||||
async function resetProviders() {
|
||||
try {
|
||||
await resetProvidersSettings()
|
||||
trackDataAction({ action: 'provider_settings_reset' })
|
||||
emitStatus(t('settings.pages.data.status.providers_reset'))
|
||||
}
|
||||
catch (error) {
|
||||
|
|
@ -25,6 +28,7 @@ async function resetProviders() {
|
|||
async function deleteAll() {
|
||||
try {
|
||||
await deleteAllData()
|
||||
trackDataAction({ action: 'app_data_cleared' })
|
||||
emitStatus(t('settings.pages.data.status.all_deleted'))
|
||||
}
|
||||
catch (error) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import type { DataSettingsStatusEmits } from '../status'
|
||||
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables'
|
||||
import { useDataMaintenance } from '@proj-airi/stage-ui/composables/use-data-maintenance'
|
||||
import { DoubleCheckButton } from '@proj-airi/ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
|
@ -9,12 +10,14 @@ import { createDataSettingsStatusHelpers } from '../status'
|
|||
|
||||
const emit = defineEmits<DataSettingsStatusEmits>()
|
||||
const { t } = useI18n()
|
||||
const { trackDataAction } = useAnalytics()
|
||||
const { deleteAllModels, resetModulesSettings } = useDataMaintenance()
|
||||
const { emitStatus, handleActionError } = createDataSettingsStatusHelpers(emit)
|
||||
|
||||
async function deleteModels() {
|
||||
try {
|
||||
await deleteAllModels()
|
||||
trackDataAction({ action: 'models_cache_cleared' })
|
||||
emitStatus(t('settings.pages.data.status.models_deleted'))
|
||||
}
|
||||
catch (error) {
|
||||
|
|
@ -25,6 +28,7 @@ async function deleteModels() {
|
|||
function resetModules() {
|
||||
try {
|
||||
resetModulesSettings()
|
||||
trackDataAction({ action: 'modules_settings_reset' })
|
||||
emitStatus(t('settings.pages.data.status.modules_reset'))
|
||||
}
|
||||
catch (error) {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import { Section } from '@proj-airi/stage-ui/components'
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables'
|
||||
import { useAiriCardStore, useBackgroundStore } from '@proj-airi/stage-ui/stores'
|
||||
import { Button, Callout } from '@proj-airi/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { trackSceneBackgroundSet } = useAnalytics()
|
||||
const backgroundStore = useBackgroundStore()
|
||||
const cardStore = useAiriCardStore()
|
||||
|
||||
|
|
@ -31,6 +33,7 @@ const activeBackgroundId = computed({
|
|||
...cardStore.activeCard,
|
||||
extensions: extension,
|
||||
})
|
||||
trackSceneBackgroundSet({ source: 'scene_settings', cleared: val === 'none' })
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
"./libs/inference": "./src/libs/inference/index.ts",
|
||||
"./libs/*": "./src/libs/*.ts",
|
||||
"./libs": "./src/libs/index.ts",
|
||||
"./services/*": "./src/services/*.ts",
|
||||
"./tools/mcp": "./src/tools/mcp.ts",
|
||||
"./stores/providers/aliyun": "./src/stores/providers/aliyun/index.ts",
|
||||
"./stores/analytics": "./src/stores/analytics/index.ts",
|
||||
|
|
@ -124,6 +125,7 @@
|
|||
"hono": "catalog:",
|
||||
"html2canvas": "catalog:",
|
||||
"idb-keyval": "catalog:",
|
||||
"jszip": "catalog:",
|
||||
"kokoro-js": "catalog:",
|
||||
"localforage": "catalog:",
|
||||
"mediabunny": "catalog:",
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ function confirmCreate() {
|
|||
const newId = cardStore.addCard({
|
||||
...structuredClone(toRaw(current)),
|
||||
name,
|
||||
})
|
||||
}, 'duplicate')
|
||||
|
||||
activeCardId.value = newId
|
||||
cancelCreate()
|
||||
|
|
|
|||
|
|
@ -716,12 +716,6 @@ describe('useAnalytics conversation product events', () => {
|
|||
preset_type: 'stage_model',
|
||||
source: 'settings',
|
||||
})
|
||||
analytics.trackModelChanged({
|
||||
from_model: 'gpt-old',
|
||||
to_model: 'gpt-new',
|
||||
provider: 'official-provider',
|
||||
reason: 'manual',
|
||||
})
|
||||
analytics.trackProviderSwitched({
|
||||
from_provider: 'openai-compatible',
|
||||
to_provider: 'official-provider',
|
||||
|
|
@ -777,14 +771,7 @@ describe('useAnalytics conversation product events', () => {
|
|||
preset_type: 'stage_model',
|
||||
source: 'settings',
|
||||
})
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(7, 'model_changed', {
|
||||
surface: 'web',
|
||||
from_model: 'gpt-old',
|
||||
to_model: 'gpt-new',
|
||||
provider: 'official-provider',
|
||||
reason: 'manual',
|
||||
})
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(8, 'provider_switched', {
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(7, 'provider_switched', {
|
||||
surface: 'web',
|
||||
from_provider: 'openai-compatible',
|
||||
to_provider: 'official-provider',
|
||||
|
|
@ -792,14 +779,14 @@ describe('useAnalytics conversation product events', () => {
|
|||
to_provider_type: 'official',
|
||||
reason: 'manual',
|
||||
})
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(9, 'settings_changed', {
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(8, 'settings_changed', {
|
||||
surface: 'web',
|
||||
setting_name: 'analytics_enabled',
|
||||
previous_value: false,
|
||||
new_value: true,
|
||||
source: 'settings',
|
||||
})
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(10, 'support_contacted', {
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(9, 'support_contacted', {
|
||||
surface: 'web',
|
||||
channel: 'discord',
|
||||
source: 'settings',
|
||||
|
|
@ -853,4 +840,106 @@ describe('useAnalytics conversation product events', () => {
|
|||
entrypoint: 'community_manual_tag',
|
||||
})
|
||||
})
|
||||
|
||||
it('emits account lifecycle events shared with the ui-server-auth surface', () => {
|
||||
const analytics = useAnalytics()
|
||||
|
||||
analytics.trackPasswordChanged()
|
||||
analytics.trackPasswordResetRequested()
|
||||
analytics.trackOauthProviderLinkStarted({ provider: 'github' })
|
||||
analytics.trackOauthProviderUnlinked({ provider: 'google' })
|
||||
analytics.trackAccountDeletionRequested()
|
||||
analytics.trackOauthCallbackFailed({ stage: 'missing_flow_state' })
|
||||
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(1, 'password_changed', { surface: 'web' })
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(2, 'password_reset_requested', { surface: 'web' })
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'oauth_provider_link_started',
|
||||
{ surface: 'web', provider: 'github' },
|
||||
{ send_instantly: true, transport: 'sendBeacon' },
|
||||
)
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(4, 'oauth_provider_unlinked', {
|
||||
surface: 'web',
|
||||
provider: 'google',
|
||||
})
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(5, 'account_deletion_requested', { surface: 'web' })
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(6, 'oauth_callback_failed', {
|
||||
surface: 'web',
|
||||
stage: 'missing_flow_state',
|
||||
})
|
||||
})
|
||||
|
||||
it('emits AIRI card edit and scene background events', () => {
|
||||
const analytics = useAnalytics()
|
||||
|
||||
analytics.trackCardEdited({ card_id: 'card-1' })
|
||||
analytics.trackSceneBackgroundSet({ source: 'card_gallery', cleared: false })
|
||||
analytics.trackSceneBackgroundSet({ source: 'scene_settings', cleared: true })
|
||||
analytics.trackCharacterUpdated({ character_id: 'character-1' })
|
||||
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(1, 'card_edited', {
|
||||
surface: 'web',
|
||||
card_id: 'card-1',
|
||||
})
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(2, 'scene_background_set', {
|
||||
surface: 'web',
|
||||
source: 'card_gallery',
|
||||
cleared: false,
|
||||
})
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(3, 'scene_background_set', {
|
||||
surface: 'web',
|
||||
source: 'scene_settings',
|
||||
cleared: true,
|
||||
})
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(4, 'character_updated', {
|
||||
character_id: 'character-1',
|
||||
})
|
||||
})
|
||||
|
||||
it('emits data maintenance actions as one event with an action discriminator', () => {
|
||||
const analytics = useAnalytics()
|
||||
|
||||
analytics.trackDataAction({ action: 'chats_exported' })
|
||||
analytics.trackDataAction({ action: 'app_data_cleared' })
|
||||
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(1, 'data_action', {
|
||||
surface: 'web',
|
||||
action: 'chats_exported',
|
||||
})
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(2, 'data_action', {
|
||||
surface: 'web',
|
||||
action: 'app_data_cleared',
|
||||
})
|
||||
})
|
||||
|
||||
it('emits desktop differentiator events for spotlight, widgets, updater, MCP, and pairing', () => {
|
||||
analyticsMocks.isStageTamagotchiMock.mockReturnValue(true)
|
||||
const analytics = useAnalytics()
|
||||
|
||||
analytics.trackSpotlightUsed()
|
||||
analytics.trackWidgetOpened({ widget_id: 'weather' })
|
||||
analytics.trackUpdateCheckClicked({ channel: 'auto' })
|
||||
analytics.trackUpdateDownloaded({ channel: 'stable', version: '0.11.0' })
|
||||
analytics.trackUpdateInstallClicked({ channel: 'stable', version: '0.11.0' })
|
||||
analytics.trackMcpServerAdded()
|
||||
analytics.trackMcpServerRemoved()
|
||||
analytics.trackMcpConnectionTestRun({ success: false })
|
||||
analytics.trackDevicePairingQrShown()
|
||||
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(1, 'spotlight_used')
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(2, 'widget_opened', { widget_id: 'weather' })
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(3, 'update_check_clicked', { channel: 'auto' })
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(4, 'update_downloaded', { channel: 'stable', version: '0.11.0' })
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(
|
||||
5,
|
||||
'update_install_clicked',
|
||||
{ channel: 'stable', version: '0.11.0' },
|
||||
{ send_instantly: true, transport: 'sendBeacon' },
|
||||
)
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(6, 'mcp_server_added')
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(7, 'mcp_server_removed')
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(8, 'mcp_connection_test_run', { success: false })
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(9, 'device_pairing_qr_shown')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -37,6 +37,20 @@ export type ProductAnalyticsEntry = 'app_start' | 'onboarding' | 'settings' | 'c
|
|||
export type MessageInputMode = 'text' | 'voice'
|
||||
export type ConversationEventSource = 'new_session' | 'fork' | 'history' | 'share_button' | 'unknown'
|
||||
|
||||
/**
|
||||
* Full stage vocabulary of the cross-surface `oauth_callback_failed` event.
|
||||
* The web/PKCE stages fire from `pages/auth/callback.vue`; the electron
|
||||
* relay stages fire from ui-server-auth's `electron-callback.vue`, which
|
||||
* imports this type so the two emitters can't drift apart silently.
|
||||
*/
|
||||
export type OauthCallbackFailureStage
|
||||
= | 'provider_error'
|
||||
| 'missing_code_or_state'
|
||||
| 'missing_flow_state'
|
||||
| 'token_exchange_failed'
|
||||
| 'parse'
|
||||
| 'relay_unreachable'
|
||||
|
||||
interface ChatActivationBaseProperties {
|
||||
provider_mode: ProviderMode
|
||||
provider_id: string
|
||||
|
|
@ -187,8 +201,10 @@ export function useAnalytics() {
|
|||
* the regular batched queue would race the redirect and drop the
|
||||
* event, which breaks the funnel.
|
||||
*
|
||||
* The funnel terminator `payment_completed` is emitted server-side from
|
||||
* the Stripe webhook — see `apps/server/src/routes/stripe/index.ts`.
|
||||
* The funnel terminator `payment_completed` is forwarded to PostHog
|
||||
* server-side by the product-events service (allowlist in
|
||||
* `apps/server/src/services/domain/product-events.ts`), keyed by the
|
||||
* Better Auth user id.
|
||||
*/
|
||||
function trackCheckoutStarted(planId: string, properties: { checkout_session_id?: string, price_minor_unit?: number, currency?: string }) {
|
||||
if (!canCapture())
|
||||
|
|
@ -215,12 +231,71 @@ export function useAnalytics() {
|
|||
})
|
||||
}
|
||||
|
||||
/** Activation funnel — step 1. */
|
||||
function trackSignup(method: 'email' | 'google' | 'github' | string) {
|
||||
/**
|
||||
* OAuth/OIDC callback landing failed before a session existed. Stage
|
||||
* values map 1:1 to the guard branches in `pages/auth/callback.vue` so
|
||||
* the funnel can tell a provider-side denial from a lost PKCE state.
|
||||
*/
|
||||
function trackOauthCallbackFailed(properties: {
|
||||
stage: Extract<OauthCallbackFailureStage, 'provider_error' | 'missing_code_or_state' | 'missing_flow_state' | 'token_exchange_failed'>
|
||||
}) {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('user_signed_up', { method })
|
||||
posthog.capture('signup_completed', { source: method })
|
||||
posthog.capture('oauth_callback_failed', {
|
||||
...properties,
|
||||
surface: getConversationAnalyticsSurface(),
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Account lifecycle (same event names as apps/ui-server-auth's
|
||||
// analytics module — both surfaces feed one PostHog series) ───────────
|
||||
|
||||
function trackPasswordChanged() {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('password_changed', { surface: getConversationAnalyticsSurface() })
|
||||
}
|
||||
|
||||
function trackPasswordResetRequested() {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('password_reset_requested', { surface: getConversationAnalyticsSurface() })
|
||||
}
|
||||
|
||||
function trackOauthProviderLinkStarted(properties: { provider: string }) {
|
||||
if (!canCapture())
|
||||
return
|
||||
// The only caller (`useLinkedAccounts.link`) navigates to the OAuth
|
||||
// consent page right after this hook — the batched queue would race
|
||||
// the unload and drop the event, same as `trackCheckoutStarted`.
|
||||
posthog.capture(
|
||||
'oauth_provider_link_started',
|
||||
{
|
||||
...properties,
|
||||
surface: getConversationAnalyticsSurface(),
|
||||
},
|
||||
{ send_instantly: true, transport: 'sendBeacon' },
|
||||
)
|
||||
}
|
||||
|
||||
function trackOauthProviderUnlinked(properties: { provider: string }) {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('oauth_provider_unlinked', {
|
||||
...properties,
|
||||
surface: getConversationAnalyticsSurface(),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletion email sent (user confirmed in the dialog). The completion
|
||||
* event lands on ui-server-auth's success page; this one is the churn
|
||||
* intent signal even when the user never clicks the email link.
|
||||
*/
|
||||
function trackAccountDeletionRequested() {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('account_deletion_requested', { surface: getConversationAnalyticsSurface() })
|
||||
}
|
||||
|
||||
function trackSignupCompleted(properties: {
|
||||
|
|
@ -255,19 +330,6 @@ export function useAnalytics() {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Activation funnel — fires the first time a user picks a model in any
|
||||
* provider settings. De-dup is intentional caller-side (we don't have a
|
||||
* persistent "first model selected" flag yet); a small number of repeats
|
||||
* is OK in PostHog funnels because step matching is per-distinctId, not
|
||||
* per-event.
|
||||
*/
|
||||
function trackFirstModelSelected(modelId: string, provider: string) {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('first_model_selected', { model_id: modelId, provider })
|
||||
}
|
||||
|
||||
/** Retention driver — character creation is a strong D7 retention predictor. */
|
||||
function trackCharacterCreated(properties: { character_type: 'built_in' | 'custom', voice_enabled: boolean }) {
|
||||
if (!canCapture())
|
||||
|
|
@ -312,10 +374,10 @@ export function useAnalytics() {
|
|||
|
||||
// ─── LLM round events (client-known fields only) ──────────────────────
|
||||
// Source-of-truth for HTTP status / token usage / billing stage is the
|
||||
// server (apps/server/src/routes/openai/v1) — emitting those server-side
|
||||
// via captureSafe so PostHog has both perspectives merged on the same
|
||||
// distinctId. These client emits supply the user-facing latency picture
|
||||
// (TTFT, render time) that the server cannot see.
|
||||
// server (apps/server/src/routes/openai/v1), which records them as
|
||||
// Postgres `product_events` rows — deliberately NOT forwarded to PostHog
|
||||
// (per-request volume stays in DB/Grafana). These client emits supply the
|
||||
// user-facing latency picture (TTFT, render time) the server cannot see.
|
||||
|
||||
function trackMessageSendStarted(properties: { source: 'text' | 'voice', model?: string }) {
|
||||
if (!canCapture())
|
||||
|
|
@ -878,20 +940,6 @@ export function useAnalytics() {
|
|||
})
|
||||
}
|
||||
|
||||
function trackModelChanged(properties: {
|
||||
from_model?: string
|
||||
to_model: string
|
||||
provider: string
|
||||
reason: 'manual' | 'auto'
|
||||
}) {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('model_changed', {
|
||||
...properties,
|
||||
surface: getConversationAnalyticsSurface(),
|
||||
})
|
||||
}
|
||||
|
||||
function trackOfficialTtsPreviewSucceeded(properties: Omit<TtsVoiceBaseProperties, 'source'> & {
|
||||
voice_id: string
|
||||
voice_type: VoiceType
|
||||
|
|
@ -969,6 +1017,38 @@ export function useAnalytics() {
|
|||
posthog.capture('autonomous_generate_text', properties)
|
||||
}
|
||||
|
||||
// ─── AIRI card (ccv3 character card) events ──────────────────────────
|
||||
// `card_created` is emitted store-side (`stores/modules/airi-card.ts`)
|
||||
// because creation has three entry points; edit has exactly one
|
||||
// user-driven entry (the creation dialog in edit mode), so it lives
|
||||
// here. Background card writes (autonomous artistry, image journal,
|
||||
// scene background) intentionally do NOT count as edits.
|
||||
|
||||
function trackCardEdited(properties: { card_id: string }) {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('card_edited', {
|
||||
...properties,
|
||||
surface: getConversationAnalyticsSurface(),
|
||||
})
|
||||
}
|
||||
|
||||
/** Stage background switched on the active card. `cleared` = set to none. */
|
||||
function trackSceneBackgroundSet(properties: { source: 'scene_settings' | 'card_gallery', cleared: boolean }) {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('scene_background_set', {
|
||||
...properties,
|
||||
surface: getConversationAnalyticsSurface(),
|
||||
})
|
||||
}
|
||||
|
||||
function trackCharacterUpdated(properties: { character_id: string }) {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('character_updated', properties)
|
||||
}
|
||||
|
||||
// ─── App lifecycle ───────────────────────────────────────────────────
|
||||
|
||||
function trackAppLoaded(properties: { platform: 'web' | 'desktop' | 'mobile', version: string, cold_start_ms?: number }) {
|
||||
|
|
@ -1058,6 +1138,87 @@ export function useAnalytics() {
|
|||
})
|
||||
}
|
||||
|
||||
// ─── Data maintenance (churn-precursor signals) ──────────────────────
|
||||
|
||||
/**
|
||||
* One event for every destructive/exporting action on the data settings
|
||||
* page. Wipes and exports often precede churn, so cohorts built on this
|
||||
* event feed the at-risk-user list. Fires only after the action
|
||||
* succeeded — a failed wipe is not a churn signal.
|
||||
*/
|
||||
function trackDataAction(properties: {
|
||||
action: 'chats_exported' | 'chats_imported' | 'chats_cleared' | 'app_data_cleared' | 'models_cache_cleared' | 'modules_settings_reset' | 'provider_settings_reset' | 'desktop_state_reset'
|
||||
}) {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('data_action', {
|
||||
...properties,
|
||||
surface: getConversationAnalyticsSurface(),
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Desktop (Electron / Tamagotchi) differentiators ─────────────────
|
||||
// These measure whether the desktop-only surfaces earn their upkeep:
|
||||
// spotlight quick-input, floating widgets, the in-app updater, MCP
|
||||
// server management. Input text never leaves the device — events carry
|
||||
// counts and low-cardinality ids only.
|
||||
|
||||
function trackSpotlightUsed() {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('spotlight_used')
|
||||
}
|
||||
|
||||
function trackWidgetOpened(properties: { widget_id: string }) {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('widget_opened', properties)
|
||||
}
|
||||
|
||||
function trackUpdateCheckClicked(properties: { channel: string }) {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('update_check_clicked', properties)
|
||||
}
|
||||
|
||||
function trackUpdateDownloaded(properties: { channel: string, version?: string }) {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('update_downloaded', properties)
|
||||
}
|
||||
|
||||
/** User confirmed restart-and-install; the app quits right after. */
|
||||
function trackUpdateInstallClicked(properties: { channel: string, version?: string }) {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('update_install_clicked', properties, { send_instantly: true, transport: 'sendBeacon' })
|
||||
}
|
||||
|
||||
function trackMcpServerAdded() {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('mcp_server_added')
|
||||
}
|
||||
|
||||
function trackMcpServerRemoved() {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('mcp_server_removed')
|
||||
}
|
||||
|
||||
function trackMcpConnectionTestRun(properties: { success: boolean }) {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('mcp_connection_test_run', properties)
|
||||
}
|
||||
|
||||
/** Pairing QR revealed — the funnel start for `device_channel_connected`. */
|
||||
function trackDevicePairingQrShown() {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('device_pairing_qr_shown')
|
||||
}
|
||||
|
||||
// ─── Voice clone (custom TTS voice) ──────────────────────────────────
|
||||
|
||||
function trackVoiceCloneCreated(properties: { provider: string }) {
|
||||
|
|
@ -1082,11 +1243,15 @@ export function useAnalytics() {
|
|||
trackPlanSelected,
|
||||
trackCheckoutStarted,
|
||||
trackPaywallSeen,
|
||||
trackSignup,
|
||||
trackSignupCompleted,
|
||||
trackOauthCallbackFailed,
|
||||
trackPasswordChanged,
|
||||
trackPasswordResetRequested,
|
||||
trackOauthProviderLinkStarted,
|
||||
trackOauthProviderUnlinked,
|
||||
trackAccountDeletionRequested,
|
||||
trackOnboardingStarted,
|
||||
trackOnboardingCompleted,
|
||||
trackFirstModelSelected,
|
||||
trackCharacterCreated,
|
||||
trackVoiceModeActivated,
|
||||
trackModelSwitched,
|
||||
|
|
@ -1146,7 +1311,6 @@ export function useAnalytics() {
|
|||
trackVoicePackBound,
|
||||
trackAttachmentUploaded,
|
||||
trackPresetUsed,
|
||||
trackModelChanged,
|
||||
trackProviderSwitched,
|
||||
trackSettingsChanged,
|
||||
trackSupportContacted,
|
||||
|
|
@ -1159,6 +1323,9 @@ export function useAnalytics() {
|
|||
|
||||
trackAppLoaded,
|
||||
|
||||
trackCardEdited,
|
||||
trackSceneBackgroundSet,
|
||||
trackCharacterUpdated,
|
||||
trackCharacterDeleted,
|
||||
trackCharacterSwitched,
|
||||
trackChatSessionDeleted,
|
||||
|
|
@ -1172,5 +1339,16 @@ export function useAnalytics() {
|
|||
trackFeatureUsed,
|
||||
trackVoiceCloneCreated,
|
||||
trackDeviceChannelConnected,
|
||||
|
||||
trackDataAction,
|
||||
trackSpotlightUsed,
|
||||
trackWidgetOpened,
|
||||
trackUpdateCheckClicked,
|
||||
trackUpdateDownloaded,
|
||||
trackUpdateInstallClicked,
|
||||
trackMcpServerAdded,
|
||||
trackMcpServerRemoved,
|
||||
trackMcpConnectionTestRun,
|
||||
trackDevicePairingQrShown,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,4 +52,75 @@ describe('useLinkedAccounts', () => {
|
|||
errorCallbackURL: 'https://accounts.airi.build/ui/profile',
|
||||
})
|
||||
})
|
||||
|
||||
it('fires analytics hooks on unlink success and link handoff, but not on failure', async () => {
|
||||
const onUnlinked = vi.fn()
|
||||
const onLinkStarted = vi.fn()
|
||||
const unlinkAccount = vi.fn(async (): Promise<{ data: unknown, error: { message?: string } | null }> => ({ data: null, error: null }))
|
||||
const linkSocial = vi.fn(async (): Promise<{ data: { url?: string, redirect?: boolean, status?: boolean } | null, error: { message?: string } | null }> => ({
|
||||
data: { status: true, redirect: false },
|
||||
error: null,
|
||||
}))
|
||||
|
||||
const holder: {
|
||||
linkedAccounts?: ReturnType<typeof useLinkedAccounts>
|
||||
} = {}
|
||||
const app = createSSRApp({
|
||||
setup() {
|
||||
holder.linkedAccounts = useLinkedAccounts({
|
||||
client: {
|
||||
// Two rows so `isLastSignInMethod` doesn't veto the unlink.
|
||||
listAccounts: vi.fn(async () => ({
|
||||
data: [
|
||||
{ id: '1', accountId: 'a-1', providerId: 'github', createdAt: '2026-01-01T00:00:00Z', scopes: [] },
|
||||
{ id: '2', accountId: 'a-2', providerId: 'credential', createdAt: '2026-01-01T00:00:00Z', scopes: [] },
|
||||
],
|
||||
error: null,
|
||||
})),
|
||||
unlinkAccount,
|
||||
linkSocial,
|
||||
},
|
||||
isAuthenticated: ref(false),
|
||||
describeError: () => 'boom',
|
||||
buildCallbackURL: () => 'https://accounts.airi.build/ui/profile',
|
||||
messages: {
|
||||
listFailed: 'list failed',
|
||||
unlinkFailed: 'unlink failed',
|
||||
linkFailed: 'link failed',
|
||||
lastAccount: 'last account',
|
||||
unlinked: provider => `${provider} unlinked`,
|
||||
linkStarted: provider => `${provider} link started`,
|
||||
},
|
||||
onUnlinked,
|
||||
onLinkStarted,
|
||||
})
|
||||
|
||||
return () => null
|
||||
},
|
||||
})
|
||||
|
||||
await renderToString(app)
|
||||
|
||||
if (!holder.linkedAccounts)
|
||||
throw new Error('Expected linked accounts composable to initialize')
|
||||
|
||||
await holder.linkedAccounts.refresh()
|
||||
await holder.linkedAccounts.unlink('github', 'GitHub')
|
||||
expect(onUnlinked).toHaveBeenCalledTimes(1)
|
||||
expect(onUnlinked).toHaveBeenCalledWith('github')
|
||||
|
||||
await holder.linkedAccounts.link('google', 'Google')
|
||||
expect(onLinkStarted).toHaveBeenCalledTimes(1)
|
||||
expect(onLinkStarted).toHaveBeenCalledWith('google')
|
||||
|
||||
// Failure paths must not fire the hooks — a failed unlink is not an
|
||||
// unlink, and a failed handoff never reached the provider.
|
||||
unlinkAccount.mockResolvedValueOnce({ data: null, error: { message: 'nope' } })
|
||||
await holder.linkedAccounts.unlink('github', 'GitHub')
|
||||
expect(onUnlinked).toHaveBeenCalledTimes(1)
|
||||
|
||||
linkSocial.mockResolvedValueOnce({ data: null, error: { message: 'nope' } })
|
||||
await holder.linkedAccounts.link('google', 'Google')
|
||||
expect(onLinkStarted).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -76,6 +76,21 @@ export interface UseLinkedAccountsArgs {
|
|||
* and hash-history routers without further configuration.
|
||||
*/
|
||||
buildCallbackURL?: () => string
|
||||
/**
|
||||
* Analytics hook — fires exactly once per successful unlink, after the
|
||||
* server confirmed the removal. Success is only knowable inside this
|
||||
* composable (errors are swallowed into the `error` ref for the UI), so
|
||||
* callers that need to observe it must hook here instead of inspecting
|
||||
* refs after `unlink()` resolves.
|
||||
*/
|
||||
onUnlinked?: (providerId: string) => void
|
||||
/**
|
||||
* Analytics hook — fires right before the OAuth consent redirect
|
||||
* navigates away (or after a synchronous link succeeded). Link
|
||||
* completion happens on the provider's site and is not observable
|
||||
* from this page; treat this as "link attempt handed off".
|
||||
*/
|
||||
onLinkStarted?: (providerId: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -173,6 +188,7 @@ export function useLinkedAccounts(args: UseLinkedAccountsArgs) {
|
|||
if (apiError)
|
||||
throw new Error(apiError.message ?? 'unlinkAccount failed')
|
||||
message.value = args.messages.unlinked(providerName)
|
||||
args.onUnlinked?.(providerId)
|
||||
await refresh()
|
||||
}
|
||||
catch (err) {
|
||||
|
|
@ -201,11 +217,13 @@ export function useLinkedAccounts(args: UseLinkedAccountsArgs) {
|
|||
if (apiError)
|
||||
throw new Error(apiError.message ?? 'linkSocial failed')
|
||||
if (data?.url) {
|
||||
args.onLinkStarted?.(providerId)
|
||||
window.location.assign(data.url)
|
||||
return
|
||||
}
|
||||
// No URL came back (e.g. provider returned success synchronously) —
|
||||
// refresh so the new row shows up without a navigation.
|
||||
args.onLinkStarted?.(providerId)
|
||||
await refresh()
|
||||
}
|
||||
catch (err) {
|
||||
|
|
|
|||
123
packages/stage-ui/src/services/airi-card-import-export.test.ts
Normal file
123
packages/stage-ui/src/services/airi-card-import-export.test.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import type { ccv3 } from '@proj-airi/ccc'
|
||||
|
||||
import type { AiriCard, AiriExtension } from '../stores/modules/airi-card'
|
||||
|
||||
import JSZip from 'jszip'
|
||||
|
||||
import { exportToJSON } from '@proj-airi/ccc'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { DisplayModelFormat, useDisplayModelsStore } from '../stores/display-models'
|
||||
import { exportAiriCardPackage, importAiriCardPackage } from './airi-card-import-export'
|
||||
|
||||
describe('airi card package import/export', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('exports sanitized packages and restores display models', async () => {
|
||||
const displayModelsStore = useDisplayModelsStore()
|
||||
const fetch = vi.fn(async () => new Response('preset-vrm-model'))
|
||||
vi.stubGlobal('fetch', fetch)
|
||||
vi.spyOn(displayModelsStore, 'getDisplayModel').mockResolvedValue({
|
||||
id: 'preset-vrm-1',
|
||||
format: DisplayModelFormat.VRM,
|
||||
type: 'url' as const,
|
||||
url: '/assets/avatar.vrm',
|
||||
name: 'AvatarSample_A',
|
||||
importedAt: 1,
|
||||
})
|
||||
mockAddDisplayModel(displayModelsStore, 'display-model-imported')
|
||||
|
||||
const exported = await exportAiriCardPackage({ card: createCard(), displayModelsStore })
|
||||
const zip = await JSZip.loadAsync(await exported.arrayBuffer())
|
||||
const cardJson = await readJson<ccv3.CharacterCardV3>(zip, 'card.json')
|
||||
const imported = await importAiriCardPackage({ file: new File([exported], 'card.zip'), displayModelsStore })
|
||||
const airi = airiFrom(cardJson)
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith('/assets/avatar.vrm')
|
||||
expect(await readJson(zip, 'manifest.json')).toMatchObject({ format: 'airi-character-card', version: 1, resources: { displayModel: { path: 'models/body-model.vrm', format: DisplayModelFormat.VRM, name: 'AvatarSample_A.vrm' } } })
|
||||
expect(await zip.file('models/body-model.vrm')?.async('string')).toBe('preset-vrm-model')
|
||||
expect(cardJson.data).toMatchObject({ name: 'AIRI / Test Card', creator: '', tags: [], mes_example: '' })
|
||||
expect(airi.modules).toMatchObject({ consciousness: { provider: 'openai', model: 'gpt-4o' }, speech: { provider: 'elevenlabs', model: 'eleven', voice_id: 'alloy' } })
|
||||
expect(airi.modules).not.toHaveProperty('activeBackgroundId')
|
||||
expect(airi.modules.artistry).not.toHaveProperty('workflowId')
|
||||
expect(airi.agents).toEqual({})
|
||||
expect(displayModelsStore.addDisplayModel).toHaveBeenCalledWith(DisplayModelFormat.VRM, expect.objectContaining({ name: 'AvatarSample_A.vrm' }))
|
||||
expect(airiFrom(imported).modules.displayModelId).toBe('display-model-imported')
|
||||
})
|
||||
|
||||
it('classifies invalid packages', async () => {
|
||||
const emptyZip = new JSZip()
|
||||
const invalidJsonZip = new JSZip()
|
||||
const displayModelsStore = useDisplayModelsStore()
|
||||
invalidJsonZip.file('manifest.json', '{')
|
||||
const cases = [
|
||||
[new File(['not zip'], 'card.zip'), { code: 'invalid-file', message: 'Invalid zip file' }],
|
||||
[new File([await emptyZip.generateAsync({ type: 'arraybuffer' })], 'empty.zip'), { code: 'missing-file' }],
|
||||
[new File([await invalidJsonZip.generateAsync({ type: 'arraybuffer' })], 'invalid-json.zip'), { cause: expect.any(SyntaxError), code: 'invalid-file' }],
|
||||
[await packageFile(exportToJSON(createCard()), { version: 2 }), { code: 'invalid-file' }],
|
||||
] as const
|
||||
|
||||
for (const [file, expected] of cases)
|
||||
await expect(importAiriCardPackage({ file, displayModelsStore })).rejects.toMatchObject(expected)
|
||||
})
|
||||
})
|
||||
|
||||
function mockAddDisplayModel(store: ReturnType<typeof useDisplayModelsStore>, id = 'unused') {
|
||||
return vi.spyOn(store, 'addDisplayModel').mockImplementation(async (format, file) => ({
|
||||
id,
|
||||
format,
|
||||
type: 'file' as const,
|
||||
file,
|
||||
name: file.name,
|
||||
importedAt: 1,
|
||||
}))
|
||||
}
|
||||
|
||||
function createCard(displayModelId = 'preset-vrm-1'): AiriCard {
|
||||
return {
|
||||
name: 'AIRI / Test Card',
|
||||
nickname: 'Tester',
|
||||
version: '1.2.3',
|
||||
description: 'Description',
|
||||
creator: 'Hidden creator',
|
||||
messageExample: [['{{user}}: hidden']],
|
||||
tags: ['hidden'],
|
||||
extensions: {
|
||||
airi: {
|
||||
modules: {
|
||||
consciousness: { provider: 'openai', model: 'gpt-4o' },
|
||||
vision: { provider: 'ollama', model: 'llava' },
|
||||
speech: { provider: 'elevenlabs', model: 'eleven', voice_id: 'alloy', pitch: 1 },
|
||||
displayModelId,
|
||||
activeBackgroundId: 'background-secret',
|
||||
artistry: { provider: 'replicate', model: 'flux', workflowId: 'workflow-secret' },
|
||||
},
|
||||
agents: { minecraft: { prompt: 'secret', enabled: true } },
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function packageFile(cardJson: ccv3.CharacterCardV3, manifestOverrides: Record<string, unknown> = {}) {
|
||||
const zip = new JSZip()
|
||||
zip.file('manifest.json', JSON.stringify({
|
||||
format: 'airi-character-card',
|
||||
version: 1,
|
||||
card: { path: 'card.json', spec: 'chara_card_v3' },
|
||||
...manifestOverrides,
|
||||
}))
|
||||
zip.file('card.json', JSON.stringify(cardJson))
|
||||
return new File([await zip.generateAsync({ type: 'arraybuffer' })], 'card.zip')
|
||||
}
|
||||
|
||||
async function readJson<T = Record<string, unknown>>(zip: JSZip, path: string): Promise<T> {
|
||||
return JSON.parse(await zip.file(path)!.async('string')) as T
|
||||
}
|
||||
|
||||
function airiFrom(card: ccv3.CharacterCardV3): AiriExtension {
|
||||
return card.data.extensions.airi as AiriExtension
|
||||
}
|
||||
269
packages/stage-ui/src/services/airi-card-import-export.ts
Normal file
269
packages/stage-ui/src/services/airi-card-import-export.ts
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
import type { Card, ccv3 } from '@proj-airi/ccc'
|
||||
import type { GenericSchema, InferOutput } from 'valibot'
|
||||
|
||||
import type { DisplayModel, useDisplayModelsStore } from '../stores/display-models'
|
||||
import type { AiriCard, AiriExtension } from '../stores/modules/airi-card'
|
||||
|
||||
import JSZip from 'jszip'
|
||||
|
||||
import { exportToJSON } from '@proj-airi/ccc'
|
||||
import { array, literal, object, optional, parse, picklist, record, string, unknown as unknownSchema } from 'valibot'
|
||||
|
||||
import { DisplayModelFormat } from '../stores/display-models'
|
||||
|
||||
const FORMAT = 'airi-character-card'
|
||||
const VERSION = 1
|
||||
const CARD_PATH = 'card.json'
|
||||
const MANIFEST_PATH = 'manifest.json'
|
||||
const MODEL_EXT: Partial<Record<DisplayModelFormat, string>> = {
|
||||
[DisplayModelFormat.Live2dZip]: 'zip',
|
||||
[DisplayModelFormat.SpineZip]: 'zip',
|
||||
[DisplayModelFormat.VRM]: 'vrm',
|
||||
}
|
||||
|
||||
type DisplayModelsStore = ReturnType<typeof useDisplayModelsStore>
|
||||
type ExportableCard = Card & { extensions: { airi: AiriExtension } }
|
||||
|
||||
const manifestSchema = object({
|
||||
format: literal(FORMAT),
|
||||
version: literal(VERSION),
|
||||
card: object({ path: literal(CARD_PATH), spec: literal('chara_card_v3') }),
|
||||
resources: optional(object({
|
||||
displayModel: object({
|
||||
path: string(),
|
||||
format: picklist([DisplayModelFormat.Live2dZip, DisplayModelFormat.SpineZip, DisplayModelFormat.VRM]),
|
||||
name: string(),
|
||||
}),
|
||||
})),
|
||||
})
|
||||
|
||||
const characterCardV3Schema = object({
|
||||
spec: literal('chara_card_v3'),
|
||||
spec_version: literal('3.0'),
|
||||
data: object({
|
||||
name: string(),
|
||||
nickname: optional(string()),
|
||||
character_version: optional(string(), '1.0.0'),
|
||||
description: optional(string(), ''),
|
||||
personality: optional(string(), ''),
|
||||
scenario: optional(string(), ''),
|
||||
first_mes: optional(string(), ''),
|
||||
alternate_greetings: optional(array(string()), []),
|
||||
creator_notes: optional(string(), ''),
|
||||
system_prompt: optional(string(), ''),
|
||||
post_history_instructions: optional(string(), ''),
|
||||
extensions: optional(record(string(), unknownSchema()), {}),
|
||||
}),
|
||||
})
|
||||
|
||||
type CharacterCardPackageJson = InferOutput<typeof characterCardV3Schema>
|
||||
|
||||
type AiriCardPackageErrorCode = 'missing-file' | 'invalid-file'
|
||||
type Manifest = InferOutput<typeof manifestSchema>
|
||||
|
||||
export class AiriCardPackageError extends Error {
|
||||
constructor(public readonly code: AiriCardPackageErrorCode, message: string, options?: { cause?: unknown }) {
|
||||
super(message, options)
|
||||
this.name = 'AiriCardPackageError'
|
||||
}
|
||||
}
|
||||
|
||||
/** Exports only the creation/edit form whitelist; provider globals and runtime state are never cloned. */
|
||||
export async function exportAiriCardPackage({ card, displayModelsStore }: { card: AiriCard, displayModelsStore: DisplayModelsStore }): Promise<Blob> {
|
||||
const exportableCard = cardFromAiriCard(card)
|
||||
const displayModel = await exportDisplayModel(exportableCard, displayModelsStore)
|
||||
const manifest = {
|
||||
format: FORMAT,
|
||||
version: VERSION,
|
||||
createdAt: new Date().toISOString(),
|
||||
card: { path: CARD_PATH, spec: 'chara_card_v3' },
|
||||
...(displayModel ? { resources: { displayModel: displayModel.manifest } } : {}),
|
||||
}
|
||||
const zip = new JSZip()
|
||||
|
||||
zip.file(MANIFEST_PATH, JSON.stringify(manifest, null, 2))
|
||||
zip.file(CARD_PATH, JSON.stringify(exportToJSON(exportableCard), null, 2))
|
||||
if (displayModel)
|
||||
zip.file(displayModel.manifest.path, displayModel.data)
|
||||
|
||||
return zip.generateAsync({ type: 'blob' })
|
||||
}
|
||||
|
||||
/** Imports a package as sanitized CCv3 JSON; edited zip payloads cannot smuggle extra AIRI fields through. */
|
||||
export async function importAiriCardPackage({ file, displayModelsStore }: { file: File, displayModelsStore: DisplayModelsStore }): Promise<ccv3.CharacterCardV3> {
|
||||
const zip = await loadZip(file)
|
||||
const manifest = await readJsonFile(zip, MANIFEST_PATH, manifestSchema)
|
||||
const cardJson = await readJsonFile(zip, manifest.card.path, characterCardV3Schema)
|
||||
const displayModelId = await importDisplayModel(zip, manifest, displayModelsStore)
|
||||
|
||||
return exportToJSON(cardFromCharacterCard(cardJson, displayModelId))
|
||||
}
|
||||
|
||||
async function exportDisplayModel(card: ExportableCard, store: DisplayModelsStore) {
|
||||
const displayModelId = card.extensions.airi.modules.displayModelId
|
||||
if (!displayModelId)
|
||||
return
|
||||
|
||||
const model = await store.getDisplayModel(displayModelId)
|
||||
if (!model) {
|
||||
if (displayModelId.startsWith('display-model-'))
|
||||
throw error('invalid-file', 'Missing local display model')
|
||||
return
|
||||
}
|
||||
|
||||
const modelExt = MODEL_EXT[model.format]
|
||||
if (!modelExt)
|
||||
throw error('invalid-file', 'Unsupported or empty local display model')
|
||||
|
||||
const payload = await displayModelPayload(model)
|
||||
|
||||
return {
|
||||
data: payload.data,
|
||||
manifest: {
|
||||
format: model.format,
|
||||
name: payload.file.name,
|
||||
path: `models/body-model.${modelExt}`,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function importDisplayModel(zip: JSZip, manifest: Manifest, store: DisplayModelsStore) {
|
||||
const resource = manifest.resources?.displayModel
|
||||
if (!resource)
|
||||
return
|
||||
|
||||
const file = zip.file(resource.path)
|
||||
if (!file)
|
||||
throw error('missing-file', 'Missing display model file')
|
||||
|
||||
try {
|
||||
const data = await file.async('arraybuffer')
|
||||
return (await store.addDisplayModel(resource.format, new File([data], resource.name))).id
|
||||
}
|
||||
catch (cause) {
|
||||
throw error('invalid-file', 'Failed to import display model file', { cause })
|
||||
}
|
||||
}
|
||||
|
||||
async function loadZip(file: File) {
|
||||
try {
|
||||
return await JSZip.loadAsync(await file.arrayBuffer())
|
||||
}
|
||||
catch (cause) {
|
||||
throw error('invalid-file', 'Invalid zip file', { cause })
|
||||
}
|
||||
}
|
||||
|
||||
async function readJsonFile<S extends GenericSchema>(zip: JSZip, path: string, schema: S): Promise<InferOutput<S>> {
|
||||
const file = zip.file(path)
|
||||
if (!file)
|
||||
throw error('missing-file', `Missing ${path}`)
|
||||
|
||||
try {
|
||||
return parse(schema, JSON.parse(await file.async('string')))
|
||||
}
|
||||
catch (cause) {
|
||||
throw error('invalid-file', `Invalid ${path}`, { cause })
|
||||
}
|
||||
}
|
||||
|
||||
function cardFromAiriCard(card: AiriCard): ExportableCard {
|
||||
return {
|
||||
name: card.name,
|
||||
nickname: card.nickname,
|
||||
version: card.version,
|
||||
description: card.description ?? '',
|
||||
personality: card.personality ?? '',
|
||||
scenario: card.scenario ?? '',
|
||||
greetings: card.greetings ?? [],
|
||||
notes: card.notes ?? '',
|
||||
systemPrompt: card.systemPrompt ?? '',
|
||||
postHistoryInstructions: card.postHistoryInstructions ?? '',
|
||||
extensions: { airi: sanitizeAiri(card.extensions?.airi) },
|
||||
}
|
||||
}
|
||||
|
||||
function cardFromCharacterCard(card: CharacterCardPackageJson, displayModelId?: string): ExportableCard {
|
||||
const data = card.data
|
||||
return {
|
||||
name: data.name,
|
||||
nickname: data.nickname,
|
||||
version: data.character_version,
|
||||
description: data.description,
|
||||
personality: data.personality,
|
||||
scenario: data.scenario,
|
||||
greetings: [data.first_mes, ...(data.alternate_greetings ?? [])],
|
||||
notes: data.creator_notes,
|
||||
systemPrompt: data.system_prompt,
|
||||
postHistoryInstructions: data.post_history_instructions,
|
||||
extensions: { airi: sanitizeAiri(data.extensions?.airi, displayModelId) },
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeAiri(value: unknown, displayModelIdOverride?: string): AiriExtension {
|
||||
const source = isRecord(value) ? value : {}
|
||||
const modules = isRecord(source.modules) ? source.modules : {}
|
||||
const artistry = isRecord(modules.artistry) ? modules.artistry : {}
|
||||
const speech = isRecord(modules.speech) ? modules.speech : {}
|
||||
const displayModelId = displayModelIdOverride ?? stringValue(modules.displayModelId)
|
||||
|
||||
return {
|
||||
modules: {
|
||||
consciousness: providerModel(modules.consciousness),
|
||||
vision: providerModel(modules.vision),
|
||||
speech: {
|
||||
...providerModel(modules.speech),
|
||||
voice_id: stringValue(speech.voice_id),
|
||||
},
|
||||
...(displayModelId ? { displayModelId } : {}),
|
||||
artistry: {
|
||||
...(typeof artistry.provider === 'string' ? { provider: artistry.provider } : {}),
|
||||
...(typeof artistry.model === 'string' ? { model: artistry.model } : {}),
|
||||
...(typeof artistry.promptPrefix === 'string' ? { promptPrefix: artistry.promptPrefix } : {}),
|
||||
...(typeof artistry.widgetInstruction === 'string' ? { widgetInstruction: artistry.widgetInstruction } : {}),
|
||||
...(isSpawnMode(artistry.spawnMode) ? { spawnMode: artistry.spawnMode } : {}),
|
||||
...(isRecord(artistry.options) ? { options: artistry.options } : {}),
|
||||
...(typeof artistry.autonomousEnabled === 'boolean' ? { autonomousEnabled: artistry.autonomousEnabled } : {}),
|
||||
...(typeof artistry.autonomousThreshold === 'number' ? { autonomousThreshold: artistry.autonomousThreshold } : {}),
|
||||
},
|
||||
},
|
||||
agents: {},
|
||||
}
|
||||
}
|
||||
|
||||
function providerModel(value: unknown) {
|
||||
const source = isRecord(value) ? value : {}
|
||||
return { provider: stringValue(source.provider), model: stringValue(source.model) }
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function stringValue(value: unknown) {
|
||||
return typeof value === 'string' ? value : ''
|
||||
}
|
||||
|
||||
function isSpawnMode(value: unknown): value is NonNullable<AiriExtension['modules']['artistry']>['spawnMode'] {
|
||||
return value === 'bg' || value === 'widget' || value === 'inline' || value === 'bg_widget'
|
||||
}
|
||||
|
||||
async function displayModelPayload(model: DisplayModel): Promise<{ data: ArrayBuffer, file: File }> {
|
||||
try {
|
||||
const response = model.type === 'url' ? await fetch(model.url) : undefined
|
||||
if (response && !response.ok)
|
||||
throw new Error(`Failed to read display model URL: ${response.status} ${response.statusText}`)
|
||||
|
||||
const file = model.type === 'file' ? model.file : new File([await response!.blob()], `${model.name}.${MODEL_EXT[model.format]}`)
|
||||
if (file.size <= 0)
|
||||
throw new Error('Display model file is empty')
|
||||
return { data: await file.arrayBuffer(), file }
|
||||
}
|
||||
catch (cause) {
|
||||
throw error('invalid-file', 'Failed to read display model file', { cause })
|
||||
}
|
||||
}
|
||||
|
||||
function error(code: AiriCardPackageErrorCode, message: string, options?: { cause?: unknown }) {
|
||||
return new AiriCardPackageError(code, message, options)
|
||||
}
|
||||
|
|
@ -88,9 +88,9 @@ export function registerPosthogBuildInfo(buildInfo: AboutBuildInfo): void {
|
|||
* land on the anonymous device person, PostHog cannot join them.
|
||||
*
|
||||
* Expects:
|
||||
* - `userId` is the Better Auth user id (`user.id`) — must match what
|
||||
* `apps/server/src/routes/stripe/index.ts` passes as `distinctId` in
|
||||
* `capturePaymentCompleted`.
|
||||
* - `userId` is the Better Auth user id (`user.id`) — the same value the
|
||||
* server-side product-events forwarder passes as `distinctId` (see
|
||||
* `apps/server/src/services/domain/product-events.ts`).
|
||||
*/
|
||||
export function identifyPosthogUser(userId: string): void {
|
||||
if (!posthogInitialized || posthog.has_opted_out_capturing())
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ describe('stage-ui exports contract', () => {
|
|||
'./libs/*',
|
||||
'./libs/inference',
|
||||
'./libs/inference/adapters/*',
|
||||
'./services/*',
|
||||
'./stores',
|
||||
'./stores/*',
|
||||
'./stores/analytics',
|
||||
|
|
@ -62,6 +63,7 @@ describe('stage-ui exports contract', () => {
|
|||
|
||||
expect(exportsMap['./stores']).toBe('./src/stores/index.ts')
|
||||
expect(exportsMap['./stores/*']).toBe('./src/stores/*.ts')
|
||||
expect(exportsMap['./services/*']).toBe('./src/services/*.ts')
|
||||
expect(exportsMap['./tools/mcp']).toBe('./src/tools/mcp.ts')
|
||||
expect(exportsMap['./types']).toBe('./src/types/index.ts')
|
||||
expect(exportsMap['./types/*']).toBe('./src/types/*.ts')
|
||||
|
|
|
|||
|
|
@ -115,9 +115,16 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
|||
activeSpeechModel,
|
||||
} = storeToRefs(speechStore)
|
||||
|
||||
const addCard = (card: AiriCard | Card | ccv3.CharacterCardV3) => {
|
||||
/**
|
||||
* `source` feeds the `card_created` analytics event: `scratch` = built in
|
||||
* the creation dialog, `import` = ccv3 JSON upload, `duplicate` = cloned
|
||||
* from an existing card (profile switcher). Required so a new call site
|
||||
* can't silently degrade creation attribution.
|
||||
*/
|
||||
const addCard = (card: AiriCard | Card | ccv3.CharacterCardV3, source: 'scratch' | 'import' | 'duplicate') => {
|
||||
const newCardId = nanoid()
|
||||
cards.value.set(newCardId, newAiriCard(card))
|
||||
capturePosthogEvent('card_created', { card_id: newCardId, source })
|
||||
return newCardId
|
||||
}
|
||||
|
||||
|
|
|
|||
387
pnpm-lock.yaml
generated
387
pnpm-lock.yaml
generated
|
|
@ -873,6 +873,9 @@ catalogs:
|
|||
posthog-js:
|
||||
specifier: 1.306.1
|
||||
version: 1.306.1
|
||||
posthog-node:
|
||||
specifier: ^5.39.4
|
||||
version: 5.39.4
|
||||
postprocessing:
|
||||
specifier: ^6.39.1
|
||||
version: 6.39.1
|
||||
|
|
@ -971,7 +974,7 @@ catalogs:
|
|||
version: 0.184.0
|
||||
tinyexec:
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
version: 1.2.4
|
||||
tinyglobby:
|
||||
specifier: ^0.2.16
|
||||
version: 0.2.16
|
||||
|
|
@ -1287,7 +1290,7 @@ importers:
|
|||
version: 19.11.0
|
||||
tinyexec:
|
||||
specifier: 'catalog:'
|
||||
version: 1.1.1
|
||||
version: 1.2.4
|
||||
tsdown:
|
||||
specifier: 'catalog:'
|
||||
version: 0.21.9(@arethetypeswrong/core@0.18.2)(oxc-resolver@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(publint@0.3.18)(synckit@0.11.12)(typescript@5.9.3)(unplugin-unused@0.5.7)(vue-tsc@3.2.6(typescript@5.9.3))
|
||||
|
|
@ -1523,6 +1526,9 @@ importers:
|
|||
pg:
|
||||
specifier: 'catalog:'
|
||||
version: 8.20.0
|
||||
posthog-node:
|
||||
specifier: 'catalog:'
|
||||
version: 5.39.4(rxjs@7.8.2)
|
||||
resend:
|
||||
specifier: 'catalog:'
|
||||
version: 6.12.2
|
||||
|
|
@ -2257,7 +2263,7 @@ importers:
|
|||
version: 3.0.2(electron@41.2.1)
|
||||
'@electron-toolkit/tsconfig':
|
||||
specifier: 'catalog:'
|
||||
version: 2.0.0(@types/node@25.6.0)
|
||||
version: 2.0.0(@types/node@24.12.2)
|
||||
'@electron-toolkit/utils':
|
||||
specifier: 'catalog:'
|
||||
version: 4.0.0(electron@41.2.1)
|
||||
|
|
@ -2296,7 +2302,7 @@ importers:
|
|||
version: 3.1.0
|
||||
'@intlify/unplugin-vue-i18n':
|
||||
specifier: 'catalog:'
|
||||
version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
'@modelcontextprotocol/sdk':
|
||||
specifier: 'catalog:'
|
||||
version: 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6)
|
||||
|
|
@ -2332,10 +2338,10 @@ importers:
|
|||
version: link:../../packages/ui-transitions
|
||||
'@proj-airi/unplugin-fetch':
|
||||
specifier: 'catalog:'
|
||||
version: 0.2.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 0.2.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
'@proj-airi/unplugin-live2d-sdk':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.7(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
version: 0.1.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
'@types/audioworklet':
|
||||
specifier: 'catalog:'
|
||||
version: 0.0.97
|
||||
|
|
@ -2362,7 +2368,7 @@ importers:
|
|||
version: 2.10.3
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 6.0.6(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/volar':
|
||||
specifier: 'catalog:'
|
||||
version: 3.1.2(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
|
||||
|
|
@ -2395,7 +2401,7 @@ importers:
|
|||
version: 6.8.3
|
||||
electron-vite:
|
||||
specifier: 'catalog:'
|
||||
version: 5.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 5.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
get-port-please:
|
||||
specifier: 'catalog:'
|
||||
version: 3.2.0
|
||||
|
|
@ -2416,31 +2422,31 @@ importers:
|
|||
version: 2.2.6
|
||||
unocss-preset-scrollbar:
|
||||
specifier: 'catalog:'
|
||||
version: 4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
version: 4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
unplugin-info:
|
||||
specifier: 'catalog:'
|
||||
version: 1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
unplugin-yaml:
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.0(@nuxt/kit@3.20.2(magicast@0.5.2))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 4.1.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vite:
|
||||
specifier: 'catalog:'
|
||||
version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
version: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite-bundle-visualizer:
|
||||
specifier: 'catalog:'
|
||||
version: 1.2.1(rolldown@1.0.0-rc.16)(rollup@4.60.1)
|
||||
vite-plugin-mkcert:
|
||||
specifier: 'catalog:'
|
||||
version: 2.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 2.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vite-plugin-vue-devtools:
|
||||
specifier: 'catalog:'
|
||||
version: 8.1.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 8.1.1(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
|
||||
vite-plugin-vue-layouts:
|
||||
specifier: 'catalog:'
|
||||
version: 0.11.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 0.11.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
vue-macros:
|
||||
specifier: 'catalog:'
|
||||
version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
|
||||
vue-tsc:
|
||||
specifier: 'catalog:'
|
||||
version: 3.2.6(typescript@5.9.3)
|
||||
|
|
@ -3204,7 +3210,7 @@ importers:
|
|||
version: 5.1.11
|
||||
tinyexec:
|
||||
specifier: 'catalog:'
|
||||
version: 1.1.1
|
||||
version: 1.2.4
|
||||
vscode-ext-gen:
|
||||
specifier: 'catalog:'
|
||||
version: 1.6.0
|
||||
|
|
@ -3219,7 +3225,7 @@ importers:
|
|||
version: 7.0.0
|
||||
tinyexec:
|
||||
specifier: 'catalog:'
|
||||
version: 1.1.1
|
||||
version: 1.2.4
|
||||
|
||||
packages/audio:
|
||||
dependencies:
|
||||
|
|
@ -3277,7 +3283,7 @@ importers:
|
|||
version: 7.0.0
|
||||
tinyexec:
|
||||
specifier: 'catalog:'
|
||||
version: 1.1.1
|
||||
version: 1.2.4
|
||||
vite:
|
||||
specifier: ^7.0.0 || ^8.0.0-beta.0
|
||||
version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
|
@ -4181,6 +4187,9 @@ importers:
|
|||
idb-keyval:
|
||||
specifier: 'catalog:'
|
||||
version: 6.2.2
|
||||
jszip:
|
||||
specifier: 'catalog:'
|
||||
version: 3.10.1
|
||||
kokoro-js:
|
||||
specifier: 'catalog:'
|
||||
version: 1.2.1
|
||||
|
|
@ -9543,9 +9552,15 @@ packages:
|
|||
'@polka/url@1.0.0-next.29':
|
||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||
|
||||
'@posthog/core@1.39.6':
|
||||
resolution: {integrity: sha512-o6ajIwN5zXoNP0D4H/QPmOyibNTUkSyOR6ya7AG5U2ywXx4awo72L2KnCoiZPQM5x/bXv6jPBdimH8M18Ax0aw==}
|
||||
|
||||
'@posthog/core@1.7.1':
|
||||
resolution: {integrity: sha512-kjK0eFMIpKo9GXIbts8VtAknsoZ18oZorANdtuTj1CbgS28t4ZVq//HAWhnxEuXRTrtkd+SUJ6Ux3j2Af8NCuA==}
|
||||
|
||||
'@posthog/types@1.392.1':
|
||||
resolution: {integrity: sha512-Qg6Gl7/1vlr8+gPtBi5gwnLgAgiyFoKOVmTvTtDcvya9cpTwZfna7rQmkGQ4B63CunUYNNbOlqcwiUwUDyTK6w==}
|
||||
|
||||
'@prisma/client@5.22.0':
|
||||
resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==}
|
||||
engines: {node: '>=16.13'}
|
||||
|
|
@ -13539,9 +13554,6 @@ packages:
|
|||
peerDependencies:
|
||||
eslint: ^9.5.0 || ^10.0.0
|
||||
|
||||
eslint-flat-config-utils@3.1.0:
|
||||
resolution: {integrity: sha512-lM+Nwo2CzpuTS/RASQExlEIwk/BQoKqJWX6VbDlLMb/mveqvt9MMrRXFEkG3bseuK6g8noKZLeX82epkILtv4A==}
|
||||
|
||||
eslint-flat-config-utils@3.2.0:
|
||||
resolution: {integrity: sha512-PHgo1X5uqIorJONLVD9BIaOSdoYFD3z/AeJljdqDPlWVRpeCYkDbK9k0AXoYVqqNJr6FEYIEr5Rm2TSktLQcHw==}
|
||||
|
||||
|
|
@ -13630,12 +13642,6 @@ packages:
|
|||
peerDependencies:
|
||||
eslint: ^8.45.0 || ^9.0.0 || ^10.0.0
|
||||
|
||||
eslint-plugin-perfectionist@5.8.0:
|
||||
resolution: {integrity: sha512-k8uIptWIxkUclonCFGyDzgYs9NI+Qh0a7cUXS3L7IYZDEsjXuimFBVbxXPQQngWqMiaxJRwbtYB4smMGMqF+cw==}
|
||||
engines: {node: ^20.0.0 || >=22.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.45.0 || ^9.0.0 || ^10.0.0
|
||||
|
||||
eslint-plugin-pnpm@1.6.0:
|
||||
resolution: {integrity: sha512-dxmt9r3zvPaft6IugS4i0k16xag3fTbOvm/road5uV9Y8qUCQT0xzheSh3gMlYAlC6vXRpfArBDsTZ7H7JKCbg==}
|
||||
peerDependencies:
|
||||
|
|
@ -16533,6 +16539,15 @@ packages:
|
|||
posthog-js@1.306.1:
|
||||
resolution: {integrity: sha512-wO7bliv/5tlAlfoKCUzwkGXZVNexk0dHigMf9tNp0q1rzs62wThogREY7Tz7h/iWKYiuXy1RumtVlTmHuBXa1w==}
|
||||
|
||||
posthog-node@5.39.4:
|
||||
resolution: {integrity: sha512-+fCQ7htBFRQQFbIzl1T0TA7bDwYyaB9XP308ZFMCUoB5LzTzOFxBa6TYVrxdH/VQl43WXTp6sf0QsG2Z4XlNBg==}
|
||||
engines: {node: ^20.20.0 || >=22.22.0}
|
||||
peerDependencies:
|
||||
rxjs: ^7.0.0
|
||||
peerDependenciesMeta:
|
||||
rxjs:
|
||||
optional: true
|
||||
|
||||
postject@1.0.0-alpha.6:
|
||||
resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
|
@ -17751,10 +17766,6 @@ packages:
|
|||
tinycolor2@1.6.0:
|
||||
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
|
||||
|
||||
tinyexec@1.1.1:
|
||||
resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tinyexec@1.2.4:
|
||||
resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -19300,7 +19311,7 @@ snapshots:
|
|||
cac: 7.0.0
|
||||
eslint: 10.2.1(jiti@2.6.1)
|
||||
eslint-config-flat-gitignore: 2.3.0(eslint@10.2.1(jiti@2.6.1))
|
||||
eslint-flat-config-utils: 3.1.0
|
||||
eslint-flat-config-utils: 3.2.0
|
||||
eslint-merge-processors: 2.0.0(eslint@10.2.1(jiti@2.6.1))
|
||||
eslint-plugin-antfu: 3.2.2(eslint@10.2.1(jiti@2.6.1))
|
||||
eslint-plugin-command: 3.5.2(@typescript-eslint/rule-tester@8.56.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.63.0(typescript@5.9.3))(@typescript-eslint/utils@8.63.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))
|
||||
|
|
@ -19309,7 +19320,7 @@ snapshots:
|
|||
eslint-plugin-jsonc: 3.1.2(eslint@10.2.1(jiti@2.6.1))
|
||||
eslint-plugin-n: 17.24.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint-plugin-no-only-tests: 3.3.0
|
||||
eslint-plugin-perfectionist: 5.8.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint-plugin-perfectionist: 5.10.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint-plugin-pnpm: 1.6.0(eslint@10.2.1(jiti@2.6.1))
|
||||
eslint-plugin-regexp: 3.1.0(eslint@10.2.1(jiti@2.6.1))
|
||||
eslint-plugin-toml: 1.3.1(eslint@10.2.1(jiti@2.6.1))
|
||||
|
|
@ -19340,13 +19351,13 @@ snapshots:
|
|||
'@antfu/install-pkg@1.1.0':
|
||||
dependencies:
|
||||
package-manager-detector: 1.6.0
|
||||
tinyexec: 1.1.1
|
||||
tinyexec: 1.2.4
|
||||
|
||||
'@antfu/ni@30.0.0':
|
||||
dependencies:
|
||||
fzf: 0.5.2
|
||||
package-manager-detector: 1.6.0
|
||||
tinyexec: 1.1.1
|
||||
tinyexec: 1.2.4
|
||||
tinyglobby: 0.2.16
|
||||
|
||||
'@anthropic-ai/claude-code-darwin-arm64@2.1.114':
|
||||
|
|
@ -20730,9 +20741,9 @@ snapshots:
|
|||
dependencies:
|
||||
electron: 41.2.1
|
||||
|
||||
'@electron-toolkit/tsconfig@2.0.0(@types/node@25.6.0)':
|
||||
'@electron-toolkit/tsconfig@2.0.0(@types/node@24.12.2)':
|
||||
dependencies:
|
||||
'@types/node': 25.6.0
|
||||
'@types/node': 24.12.2
|
||||
|
||||
'@electron-toolkit/utils@4.0.0(electron@41.2.1)':
|
||||
dependencies:
|
||||
|
|
@ -20864,7 +20875,7 @@ snapshots:
|
|||
'@es-joy/jsdoccomment@0.84.0':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
'@typescript-eslint/types': 8.58.1
|
||||
'@typescript-eslint/types': 8.63.0
|
||||
comment-parser: 1.4.5
|
||||
esquery: 1.7.0
|
||||
jsdoc-type-pratt-parser: 7.1.1
|
||||
|
|
@ -20872,7 +20883,7 @@ snapshots:
|
|||
'@es-joy/jsdoccomment@0.86.0':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
'@typescript-eslint/types': 8.58.1
|
||||
'@typescript-eslint/types': 8.63.0
|
||||
comment-parser: 1.4.6
|
||||
esquery: 1.7.0
|
||||
jsdoc-type-pratt-parser: 7.2.0
|
||||
|
|
@ -21624,8 +21635,8 @@ snapshots:
|
|||
'@intlify/shared': 11.3.2
|
||||
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.3.2)(@vue/compiler-dom@3.5.32)(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
'@rollup/pluginutils': 5.3.0(rollup@2.80.0)
|
||||
'@typescript-eslint/scope-manager': 8.58.1
|
||||
'@typescript-eslint/typescript-estree': 8.58.1(typescript@5.9.3)
|
||||
'@typescript-eslint/scope-manager': 8.63.0
|
||||
'@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
fast-glob: 3.3.3
|
||||
pathe: 2.0.3
|
||||
|
|
@ -21642,6 +21653,31 @@ snapshots:
|
|||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
|
||||
'@intlify/bundle-utils': 11.0.7(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))
|
||||
'@intlify/shared': 11.3.2
|
||||
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.3.2)(@vue/compiler-dom@3.5.32)(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
'@typescript-eslint/scope-manager': 8.63.0
|
||||
'@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
fast-glob: 3.3.3
|
||||
pathe: 2.0.3
|
||||
picocolors: 1.1.1
|
||||
unplugin: 2.3.11
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
vue-i18n: 11.3.2(vue@3.5.32(typescript@5.9.3))
|
||||
transitivePeerDependencies:
|
||||
- '@vue/compiler-dom'
|
||||
- eslint
|
||||
- rollup
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
|
||||
|
|
@ -21649,8 +21685,8 @@ snapshots:
|
|||
'@intlify/shared': 11.3.2
|
||||
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.3.2)(@vue/compiler-dom@3.5.32)(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
'@typescript-eslint/scope-manager': 8.58.1
|
||||
'@typescript-eslint/typescript-estree': 8.58.1(typescript@5.9.3)
|
||||
'@typescript-eslint/scope-manager': 8.63.0
|
||||
'@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
fast-glob: 3.3.3
|
||||
pathe: 2.0.3
|
||||
|
|
@ -23585,10 +23621,16 @@ snapshots:
|
|||
|
||||
'@polka/url@1.0.0-next.29': {}
|
||||
|
||||
'@posthog/core@1.39.6':
|
||||
dependencies:
|
||||
'@posthog/types': 1.392.1
|
||||
|
||||
'@posthog/core@1.7.1':
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
|
||||
'@posthog/types@1.392.1': {}
|
||||
|
||||
'@prisma/client@5.22.0': {}
|
||||
|
||||
'@proj-airi/chromatic@1.1.1':
|
||||
|
|
@ -23646,11 +23688,35 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
'@proj-airi/unplugin-fetch@0.2.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
ofetch: 1.5.1
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
'@proj-airi/unplugin-fetch@0.2.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
ofetch: 1.5.1
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
'@proj-airi/unplugin-live2d-sdk@0.1.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)':
|
||||
dependencies:
|
||||
ofetch: 1.5.1
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
yauzl: 3.3.0
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- '@vitejs/devtools'
|
||||
- esbuild
|
||||
- jiti
|
||||
- less
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
'@proj-airi/unplugin-live2d-sdk@0.1.7(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)':
|
||||
dependencies:
|
||||
ofetch: 1.5.1
|
||||
|
|
@ -24147,7 +24213,7 @@ snapshots:
|
|||
'@stylistic/eslint-plugin@5.10.0(eslint@10.2.1(jiti@2.6.1))':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
|
||||
'@typescript-eslint/types': 8.58.1
|
||||
'@typescript-eslint/types': 8.63.0
|
||||
eslint: 10.2.1(jiti@2.6.1)
|
||||
eslint-visitor-keys: 4.2.1
|
||||
espree: 10.4.0
|
||||
|
|
@ -24718,8 +24784,8 @@ snapshots:
|
|||
|
||||
'@typescript-eslint/project-service@8.51.0(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.58.1(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.58.1
|
||||
'@typescript-eslint/tsconfig-utils': 8.63.0(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.63.0
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -24727,7 +24793,7 @@ snapshots:
|
|||
|
||||
'@typescript-eslint/project-service@8.56.1(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.58.1(typescript@5.9.3)
|
||||
'@typescript-eslint/tsconfig-utils': 8.63.0(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.63.0
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
typescript: 5.9.3
|
||||
|
|
@ -24736,8 +24802,8 @@ snapshots:
|
|||
|
||||
'@typescript-eslint/project-service@8.58.1(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.58.1(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.58.1
|
||||
'@typescript-eslint/tsconfig-utils': 8.63.0(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.63.0
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -24999,7 +25065,7 @@ snapshots:
|
|||
'@unocss/eslint-plugin@66.6.8(@typescript-eslint/types@8.63.0)(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.63.0
|
||||
'@typescript-eslint/utils': 8.58.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.63.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@unocss/config': 66.6.8
|
||||
'@unocss/core': 66.6.8
|
||||
'@unocss/rule-utils': 66.6.8
|
||||
|
|
@ -25248,6 +25314,12 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@vitejs/plugin-vue@6.0.6(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.0-rc.13
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
|
||||
'@vitejs/plugin-vue@6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.0-rc.13
|
||||
|
|
@ -25334,8 +25406,8 @@ snapshots:
|
|||
|
||||
'@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.4)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.58.1
|
||||
'@typescript-eslint/utils': 8.58.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/scope-manager': 8.63.0
|
||||
'@typescript-eslint/utils': 8.63.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 10.2.1(jiti@2.6.1)
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.58.1(@typescript-eslint/parser@8.51.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
|
|
@ -25525,6 +25597,15 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- vue
|
||||
|
||||
'@vue-macros/devtools@3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
sirv: 3.0.2
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@vue-macros/devtools@3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
sirv: 3.0.2
|
||||
|
|
@ -25947,7 +26028,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@huggingface/transformers': 3.8.1
|
||||
'@moeru/eventa': 0.3.0(electron@41.2.1)(h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.15)))
|
||||
'@moeru/std': 0.1.0-beta.18
|
||||
'@moeru/std': 0.1.0-beta.19
|
||||
'@xsai-ext/shared-providers': 0.4.0-beta.12
|
||||
'@xsai-transformers/shared': 0.1.0(electron@41.2.1)(h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.15)))
|
||||
'@xsai/embed': 0.4.4
|
||||
|
|
@ -25961,7 +26042,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@huggingface/transformers': 3.8.1
|
||||
'@moeru/eventa': 0.3.0(electron@41.2.1)(h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.15)))
|
||||
'@moeru/std': 0.1.0-beta.18
|
||||
'@moeru/std': 0.1.0-beta.19
|
||||
'@xsai-ext/shared-providers': 0.4.0-beta.12
|
||||
'@xsai/shared': 0.4.4
|
||||
onnxruntime-common: 1.24.3
|
||||
|
|
@ -25974,7 +26055,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@huggingface/transformers': 3.8.1
|
||||
'@moeru/eventa': 0.3.0(electron@41.2.1)(h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.15)))
|
||||
'@moeru/std': 0.1.0-beta.18
|
||||
'@moeru/std': 0.1.0-beta.19
|
||||
'@xsai-ext/shared-providers': 0.4.0-beta.12
|
||||
'@xsai-transformers/shared': 0.1.0(electron@41.2.1)(h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.15)))
|
||||
'@xsai/embed': 0.4.4
|
||||
|
|
@ -26613,7 +26694,7 @@ snapshots:
|
|||
jsonc-parser: 3.3.1
|
||||
package-manager-detector: 1.6.0
|
||||
semver: 7.7.4
|
||||
tinyexec: 1.1.1
|
||||
tinyexec: 1.2.4
|
||||
tinyglobby: 0.2.16
|
||||
unconfig: 7.5.0
|
||||
yaml: 2.8.3
|
||||
|
|
@ -27602,7 +27683,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
electron-vite@5.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
electron-vite@5.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0)
|
||||
|
|
@ -27610,7 +27691,7 @@ snapshots:
|
|||
esbuild: 0.25.12
|
||||
magic-string: 0.30.21
|
||||
picocolors: 1.1.1
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -27914,11 +27995,6 @@ snapshots:
|
|||
'@eslint/compat': 2.0.5(eslint@10.2.1(jiti@2.6.1))
|
||||
eslint: 10.2.1(jiti@2.6.1)
|
||||
|
||||
eslint-flat-config-utils@3.1.0:
|
||||
dependencies:
|
||||
'@eslint/config-helpers': 0.5.5
|
||||
pathe: 2.0.3
|
||||
|
||||
eslint-flat-config-utils@3.2.0:
|
||||
dependencies:
|
||||
'@eslint/config-helpers': 0.5.5
|
||||
|
|
@ -28034,15 +28110,6 @@ snapshots:
|
|||
- supports-color
|
||||
- typescript
|
||||
|
||||
eslint-plugin-perfectionist@5.8.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 8.58.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 10.2.1(jiti@2.6.1)
|
||||
natural-orderby: 5.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
eslint-plugin-pnpm@1.6.0(eslint@10.2.1(jiti@2.6.1)):
|
||||
dependencies:
|
||||
empathic: 2.0.0
|
||||
|
|
@ -31040,7 +31107,7 @@ snapshots:
|
|||
dependencies:
|
||||
citty: 0.2.1
|
||||
pathe: 2.0.3
|
||||
tinyexec: 1.1.1
|
||||
tinyexec: 1.2.4
|
||||
|
||||
oauth-sign@0.9.0: {}
|
||||
|
||||
|
|
@ -31666,6 +31733,12 @@ snapshots:
|
|||
preact: 10.28.1
|
||||
web-vitals: 4.2.4
|
||||
|
||||
posthog-node@5.39.4(rxjs@7.8.2):
|
||||
dependencies:
|
||||
'@posthog/core': 1.39.6
|
||||
optionalDependencies:
|
||||
rxjs: 7.8.2
|
||||
|
||||
postject@1.0.0-alpha.6:
|
||||
dependencies:
|
||||
commander: 9.5.0
|
||||
|
|
@ -33123,7 +33196,7 @@ snapshots:
|
|||
pathe: 2.0.3
|
||||
pnpm-workspace-yaml: 1.6.0
|
||||
restore-cursor: 5.1.0
|
||||
tinyexec: 1.1.1
|
||||
tinyexec: 1.2.4
|
||||
tinyglobby: 0.2.16
|
||||
unconfig: 7.5.0
|
||||
yaml: 2.8.3
|
||||
|
|
@ -33245,8 +33318,6 @@ snapshots:
|
|||
|
||||
tinycolor2@1.6.0: {}
|
||||
|
||||
tinyexec@1.1.1: {}
|
||||
|
||||
tinyexec@1.2.4: {}
|
||||
|
||||
tinyglobby@0.2.16:
|
||||
|
|
@ -33357,7 +33428,7 @@ snapshots:
|
|||
rolldown: 1.0.0-rc.16
|
||||
rolldown-plugin-dts: 0.23.2(oxc-resolver@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(rolldown@1.0.0-rc.16)(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))
|
||||
semver: 7.7.4
|
||||
tinyexec: 1.1.1
|
||||
tinyexec: 1.2.4
|
||||
tinyglobby: 0.2.16
|
||||
tree-kill: 1.2.2
|
||||
unconfig-core: 7.5.0
|
||||
|
|
@ -33617,11 +33688,6 @@ snapshots:
|
|||
'@unocss/preset-mini': 66.6.8
|
||||
unocss: 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
unocss-preset-scrollbar@4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))):
|
||||
dependencies:
|
||||
'@unocss/preset-mini': 66.6.8
|
||||
unocss: 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@unocss/cli': 66.6.8
|
||||
|
|
@ -33688,6 +33754,14 @@ snapshots:
|
|||
unplugin: 2.3.11
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
unplugin-combine@2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
optionalDependencies:
|
||||
esbuild: 0.27.2
|
||||
rolldown: 1.0.0-rc.16
|
||||
rollup: 4.60.1
|
||||
unplugin: 2.3.11
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
unplugin-combine@2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
optionalDependencies:
|
||||
esbuild: 0.27.2
|
||||
|
|
@ -33709,6 +33783,19 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
unplugin-info@1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
ci-info: 4.4.0
|
||||
git-url-parse: 16.1.0
|
||||
simple-git: 3.36.0
|
||||
unplugin: 2.3.11
|
||||
optionalDependencies:
|
||||
esbuild: 0.27.2
|
||||
rollup: 4.60.1
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
unplugin-info@1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
ci-info: 4.4.0
|
||||
|
|
@ -33802,6 +33889,17 @@ snapshots:
|
|||
rollup: 2.80.0
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
unplugin-yaml@4.1.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
unplugin: 3.0.0
|
||||
yaml: 2.8.3
|
||||
optionalDependencies:
|
||||
esbuild: 0.27.2
|
||||
rolldown: 1.0.0-rc.16
|
||||
rollup: 4.60.1
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
unplugin@2.3.11:
|
||||
dependencies:
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
|
|
@ -34029,12 +34127,22 @@ snapshots:
|
|||
- rollup
|
||||
- supports-color
|
||||
|
||||
vite-dev-rpc@1.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
birpc: 2.9.0
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite-hot-client: 2.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
vite-dev-rpc@1.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
birpc: 2.9.0
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite-hot-client: 2.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
vite-hot-client@2.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
vite-hot-client@2.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
|
@ -34080,6 +34188,21 @@ snapshots:
|
|||
- tsx
|
||||
- yaml
|
||||
|
||||
vite-plugin-inspect@11.3.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
ansis: 4.2.0
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
error-stack-parser-es: 1.0.5
|
||||
ohash: 2.0.11
|
||||
open: 10.2.0
|
||||
perfect-debounce: 2.1.0
|
||||
sirv: 3.0.2
|
||||
unplugin-utils: 0.3.1
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite-dev-rpc: 1.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-inspect@11.3.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
ansis: 4.2.0
|
||||
|
|
@ -34111,6 +34234,13 @@ snapshots:
|
|||
- typescript
|
||||
- ws
|
||||
|
||||
vite-plugin-mkcert@2.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
supports-color: 10.2.2
|
||||
undici: 8.1.0
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
vite-plugin-mkcert@2.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
|
|
@ -34129,6 +34259,20 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-devtools@8.1.1(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-core': 8.1.1(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue/devtools-kit': 8.1.1
|
||||
'@vue/devtools-shared': 8.1.1
|
||||
sirv: 3.0.2
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite-plugin-inspect: 11.3.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vite-plugin-vue-inspector: 5.3.2(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
vite-plugin-vue-devtools@8.1.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-core': 8.1.1(vue@3.5.32(typescript@5.9.3))
|
||||
|
|
@ -34143,6 +34287,21 @@ snapshots:
|
|||
- supports-color
|
||||
- vue
|
||||
|
||||
vite-plugin-vue-inspector@5.3.2(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0)
|
||||
'@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.29.0)
|
||||
'@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.0)
|
||||
'@vue/compiler-dom': 3.5.32
|
||||
kolorist: 1.8.0
|
||||
magic-string: 0.30.21
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-inspector@5.3.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
|
|
@ -34158,6 +34317,16 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-layouts@0.11.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
fast-glob: 3.3.3
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
vue-router: 5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-layouts@0.11.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
|
|
@ -34299,7 +34468,7 @@ snapshots:
|
|||
picomatch: 4.0.4
|
||||
std-env: 4.1.0
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 1.1.1
|
||||
tinyexec: 1.2.4
|
||||
tinyglobby: 0.2.16
|
||||
tinyrainbow: 3.1.0
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
|
@ -34330,7 +34499,7 @@ snapshots:
|
|||
picomatch: 4.0.4
|
||||
std-env: 4.1.0
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 1.1.1
|
||||
tinyexec: 1.2.4
|
||||
tinyglobby: 0.2.16
|
||||
tinyrainbow: 3.1.0
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
|
@ -34425,6 +34594,54 @@ snapshots:
|
|||
- vue-tsc
|
||||
- webpack
|
||||
|
||||
vue-macros@3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue-macros/better-define': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/boolean-prop': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/chain-call': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/common': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/config': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-emit': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-models': 3.1.2(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-prop': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-props': 3.1.2(@vue-macros/reactivity-transform@3.1.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-props-refs': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-render': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-slots': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-stylex': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/devtools': 3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
'@vue-macros/export-expose': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/export-props': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/export-render': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/hoist-static': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/jsx-directive': 3.1.2(typescript@5.9.3)
|
||||
'@vue-macros/named-template': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/reactivity-transform': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/script-lang': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/setup-block': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/setup-component': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/setup-sfc': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/short-bind': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/short-emits': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/short-vmodel': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/volar': 3.1.2(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
|
||||
unplugin: 2.3.11
|
||||
unplugin-combine: 2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
unplugin-vue-define-options: 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- '@emnapi/core'
|
||||
- '@emnapi/runtime'
|
||||
- '@rspack/core'
|
||||
- '@vueuse/core'
|
||||
- esbuild
|
||||
- rolldown
|
||||
- rollup
|
||||
- typescript
|
||||
- vite
|
||||
- vue-tsc
|
||||
- webpack
|
||||
|
||||
vue-macros@3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue-macros/better-define': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3))
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ overrides:
|
|||
safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44
|
||||
side-channel: npm:@nolyfill/side-channel@^1.0.44
|
||||
string.prototype.matchall: npm:@nolyfill/string.prototype.matchall@^1.0.44
|
||||
|
||||
patchedDependencies:
|
||||
'@mediapipe/tasks-vision': patches/@mediapipe__tasks-vision.patch
|
||||
'@xsai/generate-text@0.5.0-beta.2': patches/@xsai__generate-text@0.5.0-beta.2.patch
|
||||
|
|
@ -320,6 +321,7 @@ catalog:
|
|||
postcss: ^8.5.10
|
||||
postgres: ^3.4.9
|
||||
posthog-js: 1.306.1
|
||||
posthog-node: ^5.39.4
|
||||
postprocessing: ^6.39.1
|
||||
prismarine-block: ^1.23.0
|
||||
prismarine-entity: ^2.6.0
|
||||
|
|
@ -452,7 +454,6 @@ onlyBuiltDependencies:
|
|||
- uiohook-napi
|
||||
- utf-8-validate
|
||||
- vue-demi
|
||||
|
||||
packageExtensions:
|
||||
'@formkit/auto-animate':
|
||||
peerDependencies:
|
||||
|
|
|
|||
|
|
@ -22,4 +22,12 @@ export const POSTHOG_PROJECT_KEY
|
|||
export const DEFAULT_POSTHOG_CONFIG = {
|
||||
api_host: 'https://us.i.posthog.com',
|
||||
person_profiles: 'identified_only', // or 'always' to create profiles for anonymous users as well
|
||||
// Without this, posthog-js only fires `$pageview` on the initial page load.
|
||||
// Every AIRI surface is an SPA (vue-router / VitePress client routing), so
|
||||
// route changes would be invisible in PostHog. The '2025-05-24' defaults
|
||||
// preset switches `capture_pageview` to 'history_change' — and because
|
||||
// `capture_pageleave` defaults to 'if_capture_pageview', every surface
|
||||
// that spreads this config also starts emitting `$pageleave`. That is
|
||||
// intentional: pageleave is what makes route-level dwell time queryable.
|
||||
defaults: '2025-05-24',
|
||||
} as const satisfies Partial<PostHogConfig>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue