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
+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" }