Add managed content and keyboard usage insights
CI / verify (push) Has been cancelled
CI / publish (push) Has been cancelled

Introduce versioned official content workflows and privacy-safe keyboard analytics, while preventing repeat DeviceCheck sign-ins from incorrectly restricting eligible accounts.
This commit is contained in:
Rocky
2026-08-21 13:34:03 +08:00
parent b5212dcdc2
commit d0abe27623
55 changed files with 4690 additions and 111 deletions
+3
View File
@@ -7,6 +7,9 @@
- Product analytics installations linked to the account and all of their events
are deleted by database cascade. The service stores only the digest of a
random installation UUID and never stores user content in analytics events.
- Keyboard usage contains daily Chinese, English, other-character and
input-session counters only. It never contains text or keystrokes and is
purged after 90 days regardless of account linkage.
- Pseudonymous immutable credit-ledger entries, StoreKit transaction audit data,
and time-limited anti-abuse tombstones remain after deletion where required to
prevent replay, preserve financial integrity, and stop repeated trial abuse.
+45
View File
@@ -69,6 +69,51 @@ Managed client success events are excluded from this total.
`successful AI requests / distinct value-active users` for the selected period.
## Keyboard input usage
Keyboard input metrics use finalized UTC-day summaries produced on-device.
They describe manually committed OSGKeyboard text only and are independent
from AI value events, billing and referral qualification.
### Keyboard input active users
Distinct account identities, falling back to pseudonymous installations, with
at least one accepted keyboard usage summary in the selected UTC-date window.
### Activation-to-input conversion
Keyboard input active users divided by distinct identities with either a
`KEYBOARD_ACTIVATED` event or an accepted keyboard usage summary in the same
UTC-date window. Including summary-only identities prevents missing activation
telemetry from producing rates above 100%.
### Chinese, English and bilingual active users
- Chinese active: at least one committed Han-script character.
- English active: at least one committed Latin letter.
- Bilingual active: both Chinese and English counts are non-zero.
These populations overlap and must not be summed.
### Character volume and language share
Character volume is the sum of client-classified Chinese, English and other
committed characters. Chinese and English share use only classified language
characters as the denominator:
- Chinese share: `Chinese / (Chinese + English)`.
- English share: `English / (Chinese + English)`.
Both shares are unavailable when the denominator is zero. Other characters
remain visible in total volume but do not dilute the language split.
### Input sessions
An input session is a keyboard activation containing at least one manually
committed character. Chinese-only, English-only, mixed-language and other-only
session counts form a complete partition. Average characters per input session
is `total committed characters / input sessions`.
## Retention
The cohort date is the UTC date of a user's first successful AI feature.
+37 -1
View File
@@ -8,7 +8,7 @@ from billing, provider execution and immutable credit ledgers.
Never send or persist:
- audio or audio-derived content;
- keyboard input, prompts, context, transcripts or model output;
- raw keyboard input, keystrokes, prompts, context, transcripts or model output;
- Apple subjects, email addresses, names, tokens, API keys or provider
credentials;
- arbitrary property names or free-form text.
@@ -18,6 +18,8 @@ the identifier supplied by the client. Event DTO string representations are
redacted. Product analytics rows linked to an account are deleted with that
account. Anonymous installations that never link to an account are retained for
90 days and are eligible for scheduled deletion in a later operational job.
Per-installation keyboard usage summaries are retained for at most 90 days
regardless of account linkage.
## Ingestion API
@@ -40,6 +42,38 @@ account. Anonymous installations that never link to an account are retained for
The successful response reports `accepted` and `replayed` event counts. It does
not return account or installation identifiers.
## Keyboard usage summaries
`POST /v1/analytics/keyboard-usage` accepts 1 to 50 finalized UTC-day
summaries. Authentication, installation hashing, account linking, atomic
batching and replay semantics match the event ingestion API.
- Language classification and counting happen on-device.
- Only text manually committed by OSGKeyboard is counted. Voice transcripts,
pasted text, AI-generated output and text entered with another keyboard are
excluded.
- Chinese counts Unicode characters classified with the Han script. English
counts Latin letters. Digits, punctuation and Emoji are counted as `other`.
- Pinyin composition keystrokes are not counted as English; only the final
committed text is classified.
- An input session is one keyboard activation containing at least one
committed character.
- Chinese-only sessions contain Chinese but no English; English-only sessions
contain English but no Chinese; mixed sessions contain both. Other
characters may occur in any of those sessions. Other-only sessions contain
neither Chinese nor English.
- The four language-session counters must sum to `inputSessionCount`, and the
total committed character count must be at least the session count.
- Each installation may submit only one immutable summary per UTC date.
- Accepted dates are from 35 days ago through yesterday. Current-day partial
summaries are rejected.
- Raw text, per-keystroke events, surrounding context, host application
identifiers and free-form properties are never accepted.
The client should keep counters and the finalized outbox in the shared App
Group so either the containing app or keyboard extension can deliver them.
Retries must reuse the stored `clientSummaryId`.
## Event catalog
### Lifecycle events
@@ -186,6 +220,8 @@ The admin analytics endpoints combine:
- settled credit usage for managed AI counts and credit consumption;
- accounts, StoreKit and referrals for registration, monetization and referral
outcomes.
- daily keyboard summaries for privacy-minimized Chinese and English input
activity, character volume and input sessions.
Metric formulas are defined in
[`ANALYTICS_METRICS_DICTIONARY.md`](ANALYTICS_METRICS_DICTIONARY.md).
+13
View File
@@ -21,6 +21,7 @@ GRANT SELECT ON osg_account.credit_reservations TO 'osg_account_runtime'@'10.20.
GRANT SELECT ON osg_account.referral_campaigns TO 'osg_account_runtime'@'10.20.%';
GRANT SELECT ON osg_account.referral_campaign_budgets TO 'osg_account_runtime'@'10.20.%';
GRANT SELECT ON osg_account.referral_codes TO 'osg_account_runtime'@'10.20.%';
GRANT SELECT ON osg_account.referral_owner_codes TO 'osg_account_runtime'@'10.20.%';
GRANT SELECT ON osg_account.referral_bindings TO 'osg_account_runtime'@'10.20.%';
GRANT SELECT ON osg_account.credit_usage_records TO 'osg_account_runtime'@'10.20.%';
GRANT SELECT ON osg_account.credit_ledger TO 'osg_account_runtime'@'10.20.%';
@@ -44,6 +45,11 @@ GRANT SELECT ON osg_account.storekit_credit_purchases TO 'osg_account_runtime'@'
GRANT SELECT ON osg_account.product_analytics_installations TO 'osg_account_runtime'@'10.20.%';
GRANT SELECT ON osg_account.product_analytics_events TO 'osg_account_runtime'@'10.20.%';
GRANT SELECT ON osg_account.product_analytics_daily_counters TO 'osg_account_runtime'@'10.20.%';
GRANT SELECT ON osg_account.keyboard_usage_daily_summaries TO 'osg_account_runtime'@'10.20.%';
GRANT SELECT ON osg_account.official_content_catalog TO 'osg_account_runtime'@'10.20.%';
GRANT SELECT ON osg_account.official_skills TO 'osg_account_runtime'@'10.20.%';
GRANT SELECT ON osg_account.official_skill_localizations TO 'osg_account_runtime'@'10.20.%';
GRANT SELECT ON osg_account.official_hint_packs TO 'osg_account_runtime'@'10.20.%';
GRANT INSERT, UPDATE, DELETE ON osg_account.accounts TO 'osg_account_runtime'@'10.20.%';
GRANT INSERT, UPDATE ON osg_account.apple_credentials TO 'osg_account_runtime'@'10.20.%';
@@ -53,6 +59,7 @@ GRANT INSERT, UPDATE ON osg_account.credit_accounts TO 'osg_account_runtime'@'10
GRANT INSERT, UPDATE ON osg_account.credit_reservations TO 'osg_account_runtime'@'10.20.%';
GRANT UPDATE ON osg_account.referral_campaign_budgets TO 'osg_account_runtime'@'10.20.%';
GRANT INSERT ON osg_account.referral_codes TO 'osg_account_runtime'@'10.20.%';
GRANT INSERT ON osg_account.referral_owner_codes TO 'osg_account_runtime'@'10.20.%';
GRANT INSERT, UPDATE ON osg_account.referral_bindings TO 'osg_account_runtime'@'10.20.%';
GRANT INSERT ON osg_account.credit_usage_records TO 'osg_account_runtime'@'10.20.%';
GRANT INSERT ON osg_account.credit_ledger TO 'osg_account_runtime'@'10.20.%';
@@ -81,6 +88,12 @@ GRANT INSERT, UPDATE, DELETE ON osg_account.product_analytics_installations
GRANT INSERT ON osg_account.product_analytics_events TO 'osg_account_runtime'@'10.20.%';
GRANT INSERT, UPDATE ON osg_account.product_analytics_daily_counters
TO 'osg_account_runtime'@'10.20.%';
GRANT INSERT, DELETE ON osg_account.keyboard_usage_daily_summaries
TO 'osg_account_runtime'@'10.20.%';
GRANT UPDATE ON osg_account.official_content_catalog TO 'osg_account_runtime'@'10.20.%';
GRANT INSERT, UPDATE ON osg_account.official_skills TO 'osg_account_runtime'@'10.20.%';
GRANT INSERT, UPDATE ON osg_account.official_skill_localizations TO 'osg_account_runtime'@'10.20.%';
GRANT INSERT, UPDATE ON osg_account.official_hint_packs TO 'osg_account_runtime'@'10.20.%';
-- Deliberately absent: global privileges, GRANT OPTION, FILE, PROCESS, SUPER,
-- CREATE USER, and UPDATE/DELETE on immutable ledger or usage-history tables.
+512
View File
@@ -100,6 +100,33 @@ paths:
"409": { $ref: "#/components/responses/Error" }
"422": { $ref: "#/components/responses/Error" }
default: { $ref: "#/components/responses/Error" }
/v1/analytics/keyboard-usage:
post:
security:
- {}
- bearerAuth: []
summary: Idempotently accept privacy-minimized daily keyboard usage summaries
description: |
Accepts finalized UTC-day counters for text manually committed by
OSGKeyboard. Language classification happens on-device. Raw text,
keystrokes, surrounding context, host application identifiers, voice
transcripts and AI output are never accepted. The current UTC date is
not accepted because daily summaries are immutable once submitted.
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/KeyboardUsageBatchRequest" }
responses:
"200":
description: Atomic batch acceptance and replay counts
content:
application/json:
schema: { $ref: "#/components/schemas/ProductAnalyticsBatchResponse" }
"400": { $ref: "#/components/responses/Error" }
"409": { $ref: "#/components/responses/Error" }
"422": { $ref: "#/components/responses/Error" }
default: { $ref: "#/components/responses/Error" }
/v1/account:
get:
summary: Return the account profile
@@ -477,6 +504,100 @@ paths:
responses:
"101": { description: WebSocket upgrade }
default: { $ref: "#/components/responses/GatewayError" }
/v1/content/skills:
get:
security: []
summary: Return the enabled official Skill catalog
parameters:
- name: If-None-Match
in: header
schema: { type: string }
responses:
"200":
description: Versioned official Skill catalog
headers:
ETag: { schema: { type: string } }
Cache-Control: { schema: { type: string, const: "public,max-age=300" } }
content:
application/json:
schema: { $ref: "#/components/schemas/OfficialSkillCatalog" }
"304": { description: The caller already has the current revision }
/v1/content/hints/manifest:
get:
security: []
summary: Return the published AI Hint pack manifest
parameters:
- name: If-None-Match
in: header
schema: { type: string }
responses:
"200":
description: Published locale manifest
headers:
ETag: { schema: { type: string } }
Cache-Control: { schema: { type: string, const: "public,max-age=300" } }
content:
application/json:
schema: { $ref: "#/components/schemas/AIHintManifest" }
"304": { description: The caller already has the current manifest }
/v1/content/hints/{locale}:
get:
security: []
summary: Return a published AI Hint pack
parameters:
- $ref: "#/components/parameters/HintLocale"
- name: If-None-Match
in: header
schema: { type: string }
responses:
"200":
description: Published AI Hint pack
headers:
ETag: { schema: { type: string } }
Cache-Control: { schema: { type: string, const: "public,max-age=300" } }
content:
application/json:
schema: { $ref: "#/components/schemas/AIHintPack" }
"304": { description: The caller already has this pack version }
"404": { description: Locale is unsupported or has not been published }
/hints/manifest.json:
get:
security: []
summary: Return the AI Hint manifest at the legacy file path
parameters:
- name: If-None-Match
in: header
schema: { type: string }
responses:
"200":
description: Same payload and cache validators as /v1/content/hints/manifest
headers:
ETag: { schema: { type: string } }
Cache-Control: { schema: { type: string, const: "public,max-age=300" } }
content:
application/json:
schema: { $ref: "#/components/schemas/AIHintManifest" }
"304": { description: The caller already has the current manifest }
/hints/hints-{locale}.json:
get:
security: []
summary: Return an AI Hint pack at the legacy file path
parameters:
- $ref: "#/components/parameters/HintLocale"
- name: If-None-Match
in: header
schema: { type: string }
responses:
"200":
description: Same payload and cache validators as /v1/content/hints/{locale}
headers:
ETag: { schema: { type: string } }
Cache-Control: { schema: { type: string, const: "public,max-age=300" } }
content:
application/json:
schema: { $ref: "#/components/schemas/AIHintPack" }
"304": { description: The caller already has this pack version }
"404": { description: Locale is unsupported or has not been published }
/.well-known/apple-app-site-association:
get:
servers:
@@ -522,6 +643,125 @@ paths:
"200": { description: Bilingual HTML landing page }
"404": { description: Invalid, unknown, or expired invitation }
"503": { description: Invitation lookup is temporarily unavailable }
/v1/admin/content/skills:
get:
security:
- adminMtls: []
adminSession: []
summary: List all official Skills including disabled entries
responses:
"200":
description: Administrative Skill catalog
content:
application/json:
schema: { $ref: "#/components/schemas/AdminOfficialSkillCatalog" }
"403": { description: SUPER_ADMIN or SUPPORT role is required }
post:
security:
- adminMtls: []
adminSession: []
summary: Create a disabled official Skill
parameters:
- $ref: "#/components/parameters/AdminCsrf"
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/CreateOfficialSkillRequest" }
responses:
"201":
description: Skill created
content:
application/json:
schema: { $ref: "#/components/schemas/AdminOfficialSkill" }
"400": { description: Request is invalid }
"403": { description: SUPER_ADMIN role and valid CSRF are required }
"409": { description: Skill ID already exists }
/v1/admin/content/skills/{id}:
put:
security:
- adminMtls: []
adminSession: []
summary: Update an official Skill and increment catalog revision
parameters:
- $ref: "#/components/parameters/OfficialSkillId"
- $ref: "#/components/parameters/AdminCsrf"
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/UpdateOfficialSkillRequest" }
responses:
"200":
description: Skill updated
content:
application/json:
schema: { $ref: "#/components/schemas/AdminOfficialSkill" }
"400": { description: Request is invalid }
"403": { description: SUPER_ADMIN role and valid CSRF are required }
"404": { description: Skill was not found }
/v1/admin/content/skills/{id}/enable:
post:
security:
- adminMtls: []
adminSession: []
summary: Enable an official Skill and increment catalog revision
parameters:
- $ref: "#/components/parameters/OfficialSkillId"
- $ref: "#/components/parameters/AdminCsrf"
responses:
"204": { description: Skill enabled }
"403": { description: SUPER_ADMIN role and valid CSRF are required }
"404": { description: Skill was not found }
/v1/admin/content/skills/{id}/disable:
post:
security:
- adminMtls: []
adminSession: []
summary: Disable an official Skill and increment catalog revision
parameters:
- $ref: "#/components/parameters/OfficialSkillId"
- $ref: "#/components/parameters/AdminCsrf"
responses:
"204": { description: Skill disabled }
"403": { description: SUPER_ADMIN role and valid CSRF are required }
"404": { description: Skill was not found }
/v1/admin/content/hints/{locale}:
get:
security:
- adminMtls: []
adminSession: []
summary: Return a locale Hint pack for editing
parameters:
- $ref: "#/components/parameters/HintLocale"
responses:
"200":
description: Existing pack or an empty version-zero editor document
content:
application/json:
schema: { $ref: "#/components/schemas/AdminAIHintPack" }
"403": { description: SUPER_ADMIN or SUPPORT role is required }
put:
security:
- adminMtls: []
adminSession: []
summary: Immediately publish a locale Hint pack and increment its version
parameters:
- $ref: "#/components/parameters/HintLocale"
- $ref: "#/components/parameters/AdminCsrf"
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/UpdateAIHintPackRequest" }
responses:
"200":
description: Published pack
content:
application/json:
schema: { $ref: "#/components/schemas/AdminAIHintPack" }
"400": { description: Pack is invalid }
"403": { description: SUPER_ADMIN role and valid CSRF are required }
/v1/admin/auth/session:
get:
security:
@@ -941,6 +1181,11 @@ paths:
- OPERATOR_CREDENTIALS_RESET
- OPERATOR_SESSIONS_REVOKED
- MANUAL_CREDIT_GRANTED
- CONTENT_SKILL_CREATED
- CONTENT_SKILL_UPDATED
- CONTENT_SKILL_ENABLED
- CONTENT_SKILL_DISABLED
- CONTENT_HINT_PACK_PUBLISHED
- name: result
in: query
schema: { type: string, enum: [success, rejected] }
@@ -982,6 +1227,19 @@ components:
in: header
required: true
schema: { type: string, minLength: 8, maxLength: 128 }
OfficialSkillId:
name: id
in: path
required: true
schema:
type: string
maxLength: 100
pattern: "^official\\.[a-z0-9._-]+$"
HintLocale:
name: locale
in: path
required: true
schema: { type: string, enum: [zh, en] }
AdminCsrf:
name: X-CSRF-Token
in: header
@@ -1125,6 +1383,55 @@ components:
minItems: 1
maxItems: 50
items: { $ref: "#/components/schemas/ProductAnalyticsEvent" }
KeyboardUsageSummary:
type: object
additionalProperties: false
required:
- clientSummaryId
- summaryDate
- chineseCharacterCount
- englishCharacterCount
- otherCharacterCount
- inputSessionCount
- chineseOnlySessionCount
- englishOnlySessionCount
- mixedLanguageSessionCount
- otherOnlySessionCount
properties:
clientSummaryId: { type: string, format: uuid }
summaryDate:
type: string
format: date
description: Finalized UTC date; accepted from 35 days ago through yesterday.
chineseCharacterCount: { type: integer, format: int64, minimum: 0, maximum: 1000000 }
englishCharacterCount: { type: integer, format: int64, minimum: 0, maximum: 1000000 }
otherCharacterCount: { type: integer, format: int64, minimum: 0, maximum: 1000000 }
inputSessionCount: { type: integer, format: int64, minimum: 1, maximum: 100000 }
chineseOnlySessionCount: { type: integer, format: int64, minimum: 0, maximum: 100000 }
englishOnlySessionCount: { type: integer, format: int64, minimum: 0, maximum: 100000 }
mixedLanguageSessionCount: { type: integer, format: int64, minimum: 0, maximum: 100000 }
otherOnlySessionCount: { type: integer, format: int64, minimum: 0, maximum: 100000 }
appVersion:
type: string
minLength: 1
maxLength: 32
pattern: "^[A-Za-z0-9._+-]+$"
osVersion:
type: string
minLength: 1
maxLength: 32
pattern: "^[A-Za-z0-9._+-]+$"
KeyboardUsageBatchRequest:
type: object
additionalProperties: false
required: [installationId, summaries]
properties:
installationId: { type: string, format: uuid }
summaries:
type: array
minItems: 1
maxItems: 50
items: { $ref: "#/components/schemas/KeyboardUsageSummary" }
ProductAnalyticsBatchResponse:
type: object
additionalProperties: false
@@ -1132,6 +1439,172 @@ components:
properties:
accepted: { type: integer, minimum: 0, maximum: 50 }
replayed: { type: integer, minimum: 0, maximum: 50 }
SkillLocalization:
type: object
additionalProperties: false
required: [name, summary, prompt]
properties:
name: { type: string, minLength: 1, maxLength: 40 }
summary: { type: string, minLength: 1, maxLength: 200 }
prompt: { type: string, minLength: 1, maxLength: 6000 }
SkillLocalizations:
type: object
additionalProperties: false
required: [zh-Hans, en]
properties:
zh-Hans: { $ref: "#/components/schemas/SkillLocalization" }
en: { $ref: "#/components/schemas/SkillLocalization" }
OfficialSkill:
type: object
additionalProperties: false
required: [id, systemImage, sortOrder, kind, thinkingEnabled, localizations]
properties:
id:
type: string
maxLength: 100
pattern: "^official\\.[a-z0-9._-]+$"
systemImage: { type: string, minLength: 1, maxLength: 100 }
sortOrder: { type: integer, minimum: 0, maximum: 100000 }
kind: { type: string, const: transform }
thinkingEnabled: { type: boolean }
localizations: { $ref: "#/components/schemas/SkillLocalizations" }
AdminOfficialSkill:
type: object
additionalProperties: false
required: [id, systemImage, sortOrder, kind, thinkingEnabled, enabled, localizations]
properties:
id:
type: string
maxLength: 100
pattern: "^official\\.[a-z0-9._-]+$"
systemImage: { type: string, minLength: 1, maxLength: 100 }
sortOrder: { type: integer, minimum: 0, maximum: 100000 }
kind: { type: string, const: transform }
thinkingEnabled: { type: boolean }
enabled: { type: boolean }
localizations: { $ref: "#/components/schemas/SkillLocalizations" }
OfficialSkillCatalog:
type: object
additionalProperties: false
required: [schemaVersion, revision, skills]
properties:
schemaVersion: { type: integer, const: 1 }
revision: { type: integer, format: int64, minimum: 0 }
generatedAt: { type: string, format: date-time }
skills:
type: array
maxItems: 100
items: { $ref: "#/components/schemas/OfficialSkill" }
AdminOfficialSkillCatalog:
type: object
additionalProperties: false
required: [revision, skills]
properties:
revision: { type: integer, format: int64, minimum: 0 }
generatedAt: { type: string, format: date-time }
skills:
type: array
items: { $ref: "#/components/schemas/AdminOfficialSkill" }
CreateOfficialSkillRequest:
type: object
additionalProperties: false
required: [id, systemImage, sortOrder, thinkingEnabled, localizations]
properties:
id:
type: string
maxLength: 100
pattern: "^official\\.[a-z0-9._-]+$"
systemImage: { type: string, minLength: 1, maxLength: 100 }
sortOrder: { type: integer, minimum: 0, maximum: 100000 }
thinkingEnabled: { type: boolean }
localizations: { $ref: "#/components/schemas/SkillLocalizations" }
UpdateOfficialSkillRequest:
type: object
additionalProperties: false
required: [systemImage, sortOrder, thinkingEnabled, localizations]
properties:
systemImage: { type: string, minLength: 1, maxLength: 100 }
sortOrder: { type: integer, minimum: 0, maximum: 100000 }
thinkingEnabled: { type: boolean }
localizations: { $ref: "#/components/schemas/SkillLocalizations" }
AIHintCard:
type: object
additionalProperties: false
required: [id, prompt, category, priority, source, locale, conditions]
anyOf:
- required: [displayText]
- required: [text]
properties:
id: { type: string, minLength: 1, maxLength: 128 }
displayText: { type: string, minLength: 1, maxLength: 500 }
text: { type: string, minLength: 1, maxLength: 500 }
prompt: { type: string, minLength: 1, maxLength: 16000 }
category: { type: string, minLength: 1, maxLength: 64 }
priority: { type: integer, minimum: -10000, maximum: 10000 }
source: { type: string, minLength: 1, maxLength: 64 }
locale: { type: string, enum: [zh, en] }
conditions:
type: array
maxItems: 20
items: { type: string, minLength: 1, maxLength: 64 }
metadata:
type: object
additionalProperties: true
AIHintPack:
type: object
additionalProperties: false
required: [locale, version, cards]
properties:
locale: { type: string, enum: [zh, en] }
generatedAt: { type: string, format: date-time }
expiresAt: { type: string, format: date-time }
version: { type: integer, minimum: 1 }
cards:
type: array
maxItems: 500
items: { $ref: "#/components/schemas/AIHintCard" }
AdminAIHintPack:
type: object
additionalProperties: false
required: [locale, version, cards]
properties:
locale: { type: string, enum: [zh, en] }
generatedAt: { type: string, format: date-time }
expiresAt: { type: string, format: date-time }
intervalHours: { type: integer, minimum: 1, maximum: 168 }
version: { type: integer, minimum: 0 }
cards:
type: array
maxItems: 500
items: { $ref: "#/components/schemas/AIHintCard" }
UpdateAIHintPackRequest:
type: object
additionalProperties: false
required: [cards]
properties:
generatedAt: { type: string, format: date-time }
expiresAt: { type: string, format: date-time }
intervalHours: { type: integer, minimum: 1, maximum: 168 }
cards:
type: array
maxItems: 500
items: { $ref: "#/components/schemas/AIHintCard" }
AIHintManifest:
type: object
additionalProperties: false
required: [locales, files]
properties:
generatedAt: { type: string, format: date-time }
expiresAt: { type: string, format: date-time }
intervalHours: { type: integer, minimum: 1, maximum: 168 }
locales:
type: array
uniqueItems: true
items: { type: string, enum: [zh, en] }
files:
type: object
additionalProperties:
type: [string, "null"]
AdminSessionState:
type: object
additionalProperties: false
@@ -1286,6 +1759,42 @@ components:
executionMode: { type: string, enum: [MANAGED, LOCAL, BYOK] }
users: { type: integer, format: int64, minimum: 0 }
successes: { type: integer, format: int64, minimum: 0 }
AdminAnalyticsKeyboardUsage:
type: object
additionalProperties: false
required:
- activeUsers
- activationToInput
- chineseActiveUsers
- englishActiveUsers
- bilingualActiveUsers
- totalCharacters
- chineseCharacters
- englishCharacters
- otherCharacters
- inputSessions
- chineseOnlySessions
- englishOnlySessions
- mixedLanguageSessions
- otherOnlySessions
properties:
activeUsers: { type: integer, format: int64, minimum: 0 }
activationToInput: { $ref: "#/components/schemas/AdminAnalyticsRate" }
chineseActiveUsers: { type: integer, format: int64, minimum: 0 }
englishActiveUsers: { type: integer, format: int64, minimum: 0 }
bilingualActiveUsers: { type: integer, format: int64, minimum: 0 }
totalCharacters: { type: integer, format: int64, minimum: 0 }
chineseCharacters: { type: integer, format: int64, minimum: 0 }
englishCharacters: { type: integer, format: int64, minimum: 0 }
otherCharacters: { type: integer, format: int64, minimum: 0 }
chineseSharePercent: { type: ["number", "null"], minimum: 0, maximum: 100 }
englishSharePercent: { type: ["number", "null"], minimum: 0, maximum: 100 }
inputSessions: { type: integer, format: int64, minimum: 0 }
averageCharactersPerInputSession: { type: ["number", "null"], minimum: 0 }
chineseOnlySessions: { type: integer, format: int64, minimum: 0 }
englishOnlySessions: { type: integer, format: int64, minimum: 0 }
mixedLanguageSessions: { type: integer, format: int64, minimum: 0 }
otherOnlySessions: { type: integer, format: int64, minimum: 0 }
AdminProductAnalytics:
type: object
additionalProperties: false
@@ -1299,6 +1808,7 @@ components:
- growthFunnel
- retention
- aiFeatures
- keyboardUsage
- referralFunnel
- guardrails
properties:
@@ -1370,6 +1880,8 @@ components:
aiFeatures:
type: array
items: { $ref: "#/components/schemas/AdminAnalyticsFeatureUsage" }
keyboardUsage:
$ref: "#/components/schemas/AdminAnalyticsKeyboardUsage"
referralFunnel:
type: array
items: { $ref: "#/components/schemas/AdminAnalyticsFunnelStep" }