Migrate AI Hint feed generation

Bring dynamic hint generation into the account service while preserving the legacy key.osglab.com deployment for existing clients.
This commit is contained in:
Rocky
2026-08-21 15:17:15 +08:00
parent d0abe27623
commit 454ba8ddc5
40 changed files with 2838 additions and 10 deletions
+42
View File
@@ -0,0 +1,42 @@
# AI Hint Feed migration and coexistence
## Boundary
`key.osglab.com` remains the legacy AI Hint Feed for installed clients that still
use that origin. This repository must not:
- change the `key.osglab.com` DNS record;
- redirect `key.osglab.com` to `account.osglab.com`;
- reuse or delete the legacy container, image, settings file, or data volume;
- require the legacy service to call this account service.
The migrated generator runs independently inside OSGAccountServer and publishes:
- `https://account.osglab.com/v1/content/hints/manifest`
- `https://account.osglab.com/v1/content/hints/{locale}`
- `https://account.osglab.com/hints/manifest.json`
- `https://account.osglab.com/hints/hints-{locale}.json`
## Safe rollout
1. Back up the legacy `settings.json`, `manifest.json`, `hints-zh.json`, and
`hints-en.json` from its persistent volume.
2. Deploy OSGAccountServer with `HINT_FEED_ENABLED=false`.
3. Apply Flyway migration `V24__hint_feed_generation.sql` and the matching
runtime grants.
4. Use the protected admin console to review generation settings and run one
manual generation.
5. Verify both v1 and legacy paths on `account.osglab.com`, including ETag/304.
6. Set `HINT_FEED_ENABLED=true` only after the generated packs are accepted.
7. Point only new client releases at `account.osglab.com`. Existing clients may
continue to use `key.osglab.com`.
## Rollback
Disable `HINT_FEED_ENABLED` to stop scheduled generation. Published packs remain
available from MySQL and manual publishing remains available. No rollback step
depends on or modifies `key.osglab.com`.
Provider credentials such as `TOPHUB_API_KEY` stay in environment-backed secret
storage. They are never written to the generation settings table or returned to
the admin browser.
+4
View File
@@ -50,6 +50,8 @@ GRANT SELECT ON osg_account.official_content_catalog TO 'osg_account_runtime'@'1
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 SELECT ON osg_account.hint_feed_settings TO 'osg_account_runtime'@'10.20.%';
GRANT SELECT ON osg_account.hint_feed_generation_state 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.%';
@@ -94,6 +96,8 @@ GRANT UPDATE ON osg_account.official_content_catalog TO 'osg_account_runtime'@'1
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.%';
GRANT UPDATE ON osg_account.hint_feed_settings TO 'osg_account_runtime'@'10.20.%';
GRANT UPDATE ON osg_account.hint_feed_generation_state 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.
+145
View File
@@ -726,6 +726,69 @@ paths:
"204": { description: Skill disabled }
"403": { description: SUPER_ADMIN role and valid CSRF are required }
"404": { description: Skill was not found }
/v1/admin/content/hints/generation/settings:
get:
security:
- adminMtls: []
adminSession: []
summary: Return non-secret AI Hint generation settings
responses:
"200":
description: Current generation settings and secret availability flags
content:
application/json:
schema: { $ref: "#/components/schemas/HintFeedSettings" }
"403": { description: SUPER_ADMIN or SUPPORT role is required }
put:
security:
- adminMtls: []
adminSession: []
summary: Update non-secret AI Hint generation settings
parameters:
- $ref: "#/components/parameters/AdminCsrf"
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/UpdateHintFeedSettingsRequest" }
responses:
"200":
description: Updated generation settings
content:
application/json:
schema: { $ref: "#/components/schemas/HintFeedSettings" }
"400": { description: Settings are invalid }
"403": { description: SUPER_ADMIN role and valid CSRF are required }
/v1/admin/content/hints/generation/status:
get:
security:
- adminMtls: []
adminSession: []
summary: Return AI Hint generation and scheduler status
responses:
"200":
description: Durable generation status and current pack versions
content:
application/json:
schema: { $ref: "#/components/schemas/HintFeedGenerationStatus" }
"403": { description: SUPER_ADMIN or SUPPORT role is required }
/v1/admin/content/hints/generation/regenerate:
post:
security:
- adminMtls: []
adminSession: []
summary: Generate and atomically publish both AI Hint locale packs
parameters:
- $ref: "#/components/parameters/AdminCsrf"
responses:
"200":
description: Both locale packs were generated and published
content:
application/json:
schema: { $ref: "#/components/schemas/HintFeedGenerationResponse" }
"403": { description: SUPER_ADMIN role and valid CSRF are required }
"409": { description: A generation is already in progress }
"502": { description: Generation failed and the previous packs remain published }
/v1/admin/content/hints/{locale}:
get:
security:
@@ -1186,6 +1249,8 @@ paths:
- CONTENT_SKILL_ENABLED
- CONTENT_SKILL_DISABLED
- CONTENT_HINT_PACK_PUBLISHED
- CONTENT_HINT_FEED_SETTINGS_UPDATED
- CONTENT_HINT_FEED_GENERATED
- name: result
in: query
schema: { type: string, enum: [success, rejected] }
@@ -1605,6 +1670,86 @@ components:
type: object
additionalProperties:
type: [string, "null"]
sources:
type: object
additionalProperties:
type: array
items: { type: string }
HintFeedSettings:
type: object
additionalProperties: false
required:
- enabled
- topHubApiKeyConfigured
- generationIntervalHours
- holidayCountriesZh
- holidayCountriesEn
- weatherCitiesZh
- weatherCitiesEn
- googleTrendsGeos
properties:
enabled: { type: boolean }
topHubApiKeyConfigured: { type: boolean }
generationIntervalHours: { type: integer, minimum: 1, maximum: 168 }
holidayCountriesZh: { type: string, minLength: 2, maxLength: 255 }
holidayCountriesEn: { type: string, minLength: 2, maxLength: 255 }
weatherCitiesZh: { type: string, minLength: 1, maxLength: 2000 }
weatherCitiesEn: { type: string, minLength: 1, maxLength: 2000 }
googleTrendsGeos: { type: string, minLength: 2, maxLength: 255 }
UpdateHintFeedSettingsRequest:
type: object
additionalProperties: false
required:
- generationIntervalHours
- holidayCountriesZh
- holidayCountriesEn
- weatherCitiesZh
- weatherCitiesEn
- googleTrendsGeos
properties:
generationIntervalHours: { type: integer, minimum: 1, maximum: 168 }
holidayCountriesZh: { type: string, minLength: 2, maxLength: 255 }
holidayCountriesEn: { type: string, minLength: 2, maxLength: 255 }
weatherCitiesZh: { type: string, minLength: 1, maxLength: 2000 }
weatherCitiesEn: { type: string, minLength: 1, maxLength: 2000 }
googleTrendsGeos: { type: string, minLength: 2, maxLength: 255 }
HintFeedGenerationStatus:
type: object
additionalProperties: false
required:
- enabled
- outcome
- intervalHours
- topHubApiKeyConfigured
properties:
enabled: { type: boolean }
outcome: { type: string, enum: [IDLE, RUNNING, SUCCEEDED, FAILED] }
intervalHours: { type: integer, minimum: 1, maximum: 168 }
lastStartedAt: { type: string, format: date-time }
lastCompletedAt: { type: string, format: date-time }
lastErrorCode: { type: string, maxLength: 64 }
nextScheduledAt: { type: string, format: date-time }
topHubApiKeyConfigured: { type: boolean }
zhVersion: { type: integer, minimum: 1 }
zhCardCount: { type: integer, minimum: 0, maximum: 40 }
enVersion: { type: integer, minimum: 1 }
enCardCount: { type: integer, minimum: 0, maximum: 40 }
HintFeedGenerationResponse:
type: object
additionalProperties: false
required: [generationId, generatedAt, zh, en]
properties:
generationId: { type: string, format: uuid }
generatedAt: { type: string, format: date-time }
zh: { $ref: "#/components/schemas/HintFeedPackGenerationResult" }
en: { $ref: "#/components/schemas/HintFeedPackGenerationResult" }
HintFeedPackGenerationResult:
type: object
additionalProperties: false
required: [version, cardCount]
properties:
version: { type: integer, minimum: 1 }
cardCount: { type: integer, minimum: 0, maximum: 40 }
AdminSessionState:
type: object
additionalProperties: false