mirror of
https://github.com/moeru-ai/airi.git
synced 2026-10-30 19:34:10 +08:00
feat(stage-ui): let AIRI Cloud set the default of opt-in flags
A `cloud-opt-in` policy can now send `defaultEnabled`. It sets the state of the flag before the user makes a choice. A saved choice still wins, and "Use default" restores the Cloud default. A missing value means off, so the behavior does not change until Cloud sends it. Register the `official-responses` Cloud flag. It stays hidden and off until Cloud grants it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
747d4f68aa
commit
5b423beb54
@@ -2353,6 +2353,9 @@ pages:
|
||||
description: Choose where the workspace opens.
|
||||
single_window: 'Single window: the character grows into the workspace'
|
||||
multi_window: 'Separate window: the character stays on the desktop'
|
||||
official_responses:
|
||||
title: Responses API for the official provider
|
||||
description: Send chat requests to the official provider through the Responses API. If a model does not support it, chat uses Chat Completions for that model.
|
||||
color-scheme:
|
||||
description: Change the color scheme of the stage.
|
||||
title: Color Scheme
|
||||
|
||||
@@ -2153,6 +2153,9 @@ pages:
|
||||
description: 选择工作区开在哪里。
|
||||
single_window: '单窗口:角色自己展开成工作区'
|
||||
multi_window: '多窗口:角色留在桌面上'
|
||||
official_responses:
|
||||
title: 官方提供商使用 Responses API
|
||||
description: 通过 Responses API 向官方提供商发送聊天请求。如果模型不支持,这个模型改用 Chat Completions。
|
||||
color-scheme:
|
||||
description: 更改舞台的配色方案
|
||||
title: 配色方案
|
||||
|
||||
@@ -7,7 +7,9 @@ Shared core for stage
|
||||
Register flags in `libs/feature-flags.ts`. Read their state through `useFeatureFlagsStore().isEnabled(key)`.
|
||||
Set `availability` to `local` or `cloud`. Local flags expose device switches without Cloud access.
|
||||
Cloud decides whether each granted Cloud flag allows account opt-in or uses direct control. Client registrations do not duplicate this mode.
|
||||
`cloud-opt-in` flags expose switches only after Cloud grants access to a verified account. They start disabled and retain choices per account on this device.
|
||||
`cloud-opt-in` flags expose switches only after Cloud grants access to a verified account. They retain choices per account on this device.
|
||||
A `cloud-opt-in` policy can send `defaultEnabled`. It sets the state before the user makes a choice. A missing value means off.
|
||||
A saved choice always wins over the Cloud default. **Use default** removes the saved choice and restores the Cloud default.
|
||||
`cloud-controlled` flags follow Cloud grants directly and never expose a switch.
|
||||
Missing Cloud grants disable Cloud features. Refresh failures and account changes clear grants, not local choices.
|
||||
`useCloudFetch()` in `composables/cloud.ts` shares the Cloud origin and authenticated fetch boundary. The store reads `GET /v1/feature-flags`.
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { FeatureFlag } from './feature-flags'
|
||||
import { parse } from 'valibot'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { CHARACTER_CARD_SYNC_FLAG, featureFlagResponseSchema, featureFlags, FULLSCREEN_WORKSPACE_FLAG, offeredFeatureFlags, resolveFeatureFlag, resolveFeatureVariant } from './feature-flags'
|
||||
import { CHARACTER_CARD_SYNC_FLAG, featureFlagResponseSchema, featureFlags, FULLSCREEN_WORKSPACE_FLAG, offeredFeatureFlags, OFFICIAL_RESPONSES_FLAG, resolveFeatureFlag, resolveFeatureVariant } from './feature-flags'
|
||||
|
||||
const feature: FeatureFlag = { key: 'test-feature', titleKey: 'test.title', descriptionKey: 'test.description', defaultEnabled: false, availability: 'local' }
|
||||
|
||||
@@ -31,11 +31,28 @@ describe('feature flag decisions', () => {
|
||||
expect(resolveFeatureFlag(optIn, false, policy, true).enabled).toBe(false)
|
||||
})
|
||||
|
||||
// Cloud can turn an opt-in flag on for accounts that made no choice. The user can still turn it off or reset it.
|
||||
it('starts opt-in grants in the Cloud default state until the user chooses', () => {
|
||||
const optIn: FeatureFlag = { ...feature, availability: 'cloud' }
|
||||
const policy = { key: feature.key, mode: 'cloud-opt-in', source: 'global', defaultEnabled: true } as const
|
||||
expect(resolveFeatureFlag(optIn, undefined, policy, true)).toEqual({ enabled: true, source: 'global', selectable: true })
|
||||
expect(resolveFeatureFlag(optIn, false, policy, true)).toEqual({ enabled: false, source: 'local', selectable: true })
|
||||
expect(resolveFeatureFlag(optIn, true, { ...policy, defaultEnabled: false }, true)).toEqual({ enabled: true, source: 'local', selectable: true })
|
||||
expect(resolveFeatureFlag(optIn, undefined, { ...policy, defaultEnabled: false }, true)).toEqual({ enabled: false, source: 'global', selectable: true })
|
||||
})
|
||||
|
||||
it('keeps the Cloud default off for signed-out users', () => {
|
||||
const optIn: FeatureFlag = { ...feature, availability: 'cloud' }
|
||||
const policy = { key: feature.key, mode: 'cloud-opt-in', source: 'global', defaultEnabled: true } as const
|
||||
expect(resolveFeatureFlag(optIn, undefined, policy, false)).toEqual({ enabled: false, source: 'default', selectable: false })
|
||||
})
|
||||
|
||||
it('applies cloud control without exposing a toggle or accepting device preferences', () => {
|
||||
const controlled: FeatureFlag = { ...feature, availability: 'cloud' }
|
||||
const policy = { key: feature.key, mode: 'cloud-controlled', source: 'global' } as const
|
||||
expect(resolveFeatureFlag(controlled, false, policy, false)).toEqual({ enabled: true, source: 'global', selectable: false })
|
||||
expect(resolveFeatureFlag(controlled, true, undefined, true).enabled).toBe(false)
|
||||
expect(resolveFeatureFlag(controlled, undefined, { ...policy, defaultEnabled: false }, true)).toEqual({ enabled: true, source: 'global', selectable: false })
|
||||
})
|
||||
|
||||
it('lets Cloud change modes without changing client registration', () => {
|
||||
@@ -55,6 +72,20 @@ describe('feature flag decisions', () => {
|
||||
expect(() => parse(featureFlagResponseSchema, { flags: [{ key: feature.key, mode: 'local', source: 'account' }] })).toThrow()
|
||||
})
|
||||
|
||||
it('accepts policies with and without a Cloud default, and rejects a non-boolean default', () => {
|
||||
expect(parse(featureFlagResponseSchema, { flags: [{ key: feature.key, mode: 'cloud-opt-in', source: 'global', defaultEnabled: true }] }).flags[0].defaultEnabled).toBe(true)
|
||||
expect(parse(featureFlagResponseSchema, { flags: [{ key: feature.key, mode: 'cloud-opt-in', source: 'global' }] }).flags[0].defaultEnabled).toBeUndefined()
|
||||
expect(() => parse(featureFlagResponseSchema, { flags: [{ key: feature.key, mode: 'cloud-opt-in', source: 'global', defaultEnabled: 'true' }] })).toThrow()
|
||||
})
|
||||
|
||||
// The client registration never turns the Responses preference on. Only a Cloud grant shows the switch.
|
||||
it('registers the official Responses preference as a Cloud flag that stays off without a grant', () => {
|
||||
expect(featureFlags).toContain(OFFICIAL_RESPONSES_FLAG)
|
||||
expect(OFFICIAL_RESPONSES_FLAG.availability).toBe('cloud')
|
||||
expect(OFFICIAL_RESPONSES_FLAG.defaultEnabled).toBe(false)
|
||||
expect(resolveFeatureFlag(OFFICIAL_RESPONSES_FLAG, undefined, undefined, true)).toEqual({ enabled: false, source: 'default', selectable: false })
|
||||
})
|
||||
|
||||
it('registers character card sync as a local, off-by-default choice', () => {
|
||||
expect(featureFlags).toContain(CHARACTER_CARD_SYNC_FLAG)
|
||||
expect(CHARACTER_CARD_SYNC_FLAG.availability).toBe('local')
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import type { InferOutput } from 'valibot'
|
||||
|
||||
import { array, boolean, object, picklist, record, string } from 'valibot'
|
||||
import { array, boolean, object, optional, picklist, record, string } from 'valibot'
|
||||
|
||||
export const featureFlagPolicySchema = object({
|
||||
key: string(),
|
||||
mode: picklist(['cloud-opt-in', 'cloud-controlled']),
|
||||
source: picklist(['global', 'account']),
|
||||
/**
|
||||
* The state of a `cloud-opt-in` flag before the user makes a choice. A saved choice always wins. A missing value
|
||||
* means off. `cloud-controlled` flags ignore it.
|
||||
*/
|
||||
defaultEnabled: optional(boolean()),
|
||||
})
|
||||
|
||||
export const featureFlagResponseSchema = object({ flags: array(featureFlagPolicySchema) })
|
||||
@@ -83,8 +88,20 @@ export const FULLSCREEN_WORKSPACE_FLAG = {
|
||||
},
|
||||
} as const satisfies FeatureFlag
|
||||
|
||||
/**
|
||||
* Gates the Responses API preference of the official chat provider. The Cloud policy decides if an account gets the
|
||||
* switch and its state before the user makes a choice. The registration stays off, so a missing grant keeps it off.
|
||||
*/
|
||||
export const OFFICIAL_RESPONSES_FLAG: FeatureFlag = {
|
||||
key: 'official-responses',
|
||||
titleKey: 'settings.pages.system.experimental.features.official_responses.title',
|
||||
descriptionKey: 'settings.pages.system.experimental.features.official_responses.description',
|
||||
defaultEnabled: false,
|
||||
availability: 'cloud',
|
||||
}
|
||||
|
||||
/** Only implemented experiments belong here. An empty catalog produces an empty settings page. */
|
||||
export const featureFlags: readonly FeatureFlag[] = [CHARACTER_CARD_SYNC_FLAG, DISPLAY_MODEL_SYNC_FLAG, FULLSCREEN_WORKSPACE_FLAG]
|
||||
export const featureFlags: readonly FeatureFlag[] = [CHARACTER_CARD_SYNC_FLAG, DISPLAY_MODEL_SYNC_FLAG, FULLSCREEN_WORKSPACE_FLAG, OFFICIAL_RESPONSES_FLAG]
|
||||
|
||||
/** The variant of a feature: the saved one while the feature still offers it, otherwise the first one. */
|
||||
export function resolveFeatureVariant(feature: FeatureFlag | undefined, saved: string | undefined): string | undefined {
|
||||
@@ -99,7 +116,10 @@ export function offeredFeatureFlags(desktop: boolean): readonly FeatureFlag[] {
|
||||
return featureFlags.filter(feature => desktop || !feature.desktopOnly)
|
||||
}
|
||||
|
||||
/** Missing cloud grants disable features. Only local and account-granted opt-in features expose settings. */
|
||||
/**
|
||||
* Missing cloud grants disable features. Only local and account-granted opt-in features expose settings.
|
||||
* For a `cloud-opt-in` grant, the saved choice wins over the Cloud default, and the Cloud default wins over off.
|
||||
*/
|
||||
export function resolveFeatureFlag(feature: FeatureFlag, preference: boolean | undefined, policy: InferOutput<typeof featureFlagPolicySchema> | undefined, authenticated: boolean): FeatureFlagDecision {
|
||||
if (feature.availability === 'local')
|
||||
return { enabled: preference ?? feature.defaultEnabled, source: preference === undefined ? 'default' : 'local', selectable: true }
|
||||
@@ -113,5 +133,8 @@ export function resolveFeatureFlag(feature: FeatureFlag, preference: boolean | u
|
||||
if (!authenticated)
|
||||
return { enabled: false, source: 'default', selectable: false }
|
||||
|
||||
return { enabled: preference ?? false, source: preference === undefined ? policy.source : 'local', selectable: true }
|
||||
if (preference === undefined)
|
||||
return { enabled: policy.defaultEnabled ?? false, source: policy.source, selectable: true }
|
||||
|
||||
return { enabled: preference, source: 'local', selectable: true }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user