From c396797fd5ecedb4a59ba9284ee78f2f02f2e1f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 13:25:09 +0000 Subject: [PATCH] fix(service-settings): move the storage, knowledge and AI warnings into info_banner rows Each of the four warnings that sat inside a namespace description or a field's help string becomes an info_banner row with bannerSeverity 'warning', an id, a title and a body, placed in the section it concerns directly under the field it warns about. The translations move with them into settings.NAMESPACE.banners.BANNER_ID.{title,text} in every shipped locale, and the feature-flags beta_notice banner gains its copies too. The settings translation coverage test now covers banners in both directions, and a sweep pins that no manifest or bundle description or help string carries the warning glyph. Claude-Session: https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP Co-authored-by: Claude --- .changeset/23839-settings-warning-banners.md | 18 +++++ .../src/manifests/ai.manifest.test.ts | 23 ++++++ .../src/manifests/ai.manifest.ts | 8 +- .../src/manifests/knowledge.manifest.test.ts | 30 ++++++++ .../src/manifests/knowledge.manifest.ts | 21 +++-- .../src/manifests/storage.manifest.test.ts | 23 ++++++ .../src/manifests/storage.manifest.ts | 15 ++-- .../service-settings/src/translations/en.ts | 35 ++++++--- .../src/translations/es-ES.ts | 33 +++++--- .../src/translations/ja-JP.ts | 30 ++++++-- .../settings-translation-coverage.test.ts | 77 +++++++++++++++++-- .../src/translations/zh-CN.ts | 25 ++++-- 12 files changed, 287 insertions(+), 51 deletions(-) create mode 100644 .changeset/23839-settings-warning-banners.md diff --git a/.changeset/23839-settings-warning-banners.md b/.changeset/23839-settings-warning-banners.md new file mode 100644 index 00000000000..9fc25c47b8f --- /dev/null +++ b/.changeset/23839-settings-warning-banners.md @@ -0,0 +1,18 @@ +--- +'@objectstack/service-settings': patch +--- + +Clause-②: no + +fix(service-settings): the File Storage, Knowledge and AI settings pages show their warnings as `info_banner` callouts with `bannerSeverity: 'warning'`, not as a sentence inside a subtitle or a field's help text + +Four warnings were written into ordinary strings with a hand-typed warning glyph, so the admin read them as one more sentence of the page subtitle or of a toggle's help. Each one is now an `info_banner` row in the section it concerns, directly under the field it warns about: + +- `storage` — `adapter_switch_warning`, under the Adapter select: switching adapter does not migrate existing files. +- `knowledge` — `adapter_switch_warning`, under the Adapter select: switching adapter does not migrate existing indices. +- `knowledge` — `enforce_rls_warning`, under the Enforce RLS on search toggle: disabling it skips the vector-store leakage safeguard. +- `ai` — `log_prompts_warning`, under the Log full prompts toggle: full prompts may leak PII. + +The namespace description or field help of each keeps its non-warning text. The translations moved with them: in the `en`, `zh-CN`, `ja-JP` and `es-ES` bundles each warning sentence left the `description` / `help` copy and sits under `settings.NAMESPACE.banners.BANNER_ID.{title,text}`, with a translated title per banner. The existing Feature Flags `beta_notice` banner gained the same copies, so no shipped locale reads it in English any more. + +No setting key, value, default or save behaviour changes. A host that renders `info_banner` rows from the manifest literals draws the English title and text; translated copy is read through `resolveSettingsBannerTitle` / `resolveSettingsBannerText` from `@objectstack/spec/system`. diff --git a/packages/services/service-settings/src/manifests/ai.manifest.test.ts b/packages/services/service-settings/src/manifests/ai.manifest.test.ts index 79c2b40a48a..d3688da1982 100644 --- a/packages/services/service-settings/src/manifests/ai.manifest.test.ts +++ b/packages/services/service-settings/src/manifests/ai.manifest.test.ts @@ -237,6 +237,29 @@ describe('aiSettingsManifest — temperature declares a window, not a grid (#655 }); }); +describe('aiSettingsManifest — the log_prompts PII warning is an info_banner', () => { + const specs = aiSettingsManifest.specifiers as any[]; + const sectionOf = (i: number) => specs.slice(0, i + 1).reverse().find((s) => s.type === 'group')?.id; + + it('declares it as a warning-severity info_banner with an id, a title and a body', () => { + const banners = specs.filter((s) => s.type === 'info_banner'); + expect(banners.map((b) => [b.id, b.bannerSeverity])).toEqual([['log_prompts_warning', 'warning']]); + expect(typeof banners[0].label).toBe('string'); + expect(banners[0].label.length).toBeGreaterThan(0); + expect(banners[0].bannerText.length).toBeGreaterThan(0); + }); + + it('places it in the Observability section, directly under the log_prompts toggle', () => { + const i = specs.findIndex((s) => s.id === 'log_prompts_warning'); + expect(sectionOf(i)).toBe('observability'); + expect(specs[i - 1].key).toBe('log_prompts'); + }); + + it('carries the warning glyph nowhere — the banner severity is the only warning marker', () => { + expect(JSON.stringify(aiSettingsManifest)).not.toContain('⚠'); + }); +}); + describe('aiSettingsManifest — embedder section', () => { it('exposes embedder_provider select with 10 options incl. none + 5 Chinese providers', () => { const f = (aiSettingsManifest.specifiers as any[]).find( diff --git a/packages/services/service-settings/src/manifests/ai.manifest.ts b/packages/services/service-settings/src/manifests/ai.manifest.ts index 1a4f8b32e68..79900f6f6e3 100644 --- a/packages/services/service-settings/src/manifests/ai.manifest.ts +++ b/packages/services/service-settings/src/manifests/ai.manifest.ts @@ -229,7 +229,13 @@ const manifest = { description: 'Persist prompt/response traces to sys_ai_trace for debugging and replay.' }, { type: 'toggle', key: 'log_prompts', label: 'Log full prompts', required: false, default: false, - description: 'Include rendered prompts (not just metadata) in trace rows. ⚠ May leak PII — disable in regulated environments.' }, + description: 'Include rendered prompts (not just metadata) in trace rows.' }, + // The PII warning is an `info_banner` row (`bannerSeverity: 'warning'`), not + // a sentence in the toggle's help; its copy lives under + // `settings.ai.banners.log_prompts_warning` in the locale bundles. + { type: 'info_banner', id: 'log_prompts_warning', label: 'Prompts may contain PII', + required: false, bannerSeverity: 'warning', + bannerText: 'May leak PII — disable in regulated environments.' }, // ── Probe ──────────────────────────────────────────────────── { type: 'action_button', id: 'test', label: 'Test connection', diff --git a/packages/services/service-settings/src/manifests/knowledge.manifest.test.ts b/packages/services/service-settings/src/manifests/knowledge.manifest.test.ts index 7eb2e25c3de..5a24c275054 100644 --- a/packages/services/service-settings/src/manifests/knowledge.manifest.test.ts +++ b/packages/services/service-settings/src/manifests/knowledge.manifest.test.ts @@ -57,6 +57,36 @@ describe('knowledgeSettingsManifest', () => { }); }); +describe('knowledgeSettingsManifest — both warnings are info_banner rows', () => { + const specs = knowledgeSettingsManifest.specifiers as any[]; + const sectionOf = (i: number) => specs.slice(0, i + 1).reverse().find((s) => s.type === 'group')?.id; + + it('declares exactly the two warning-severity banners, each with an id, a title and a body', () => { + const banners = specs.filter((s) => s.type === 'info_banner'); + expect(banners.map((b) => [b.id, b.bannerSeverity])).toEqual([ + ['adapter_switch_warning', 'warning'], + ['enforce_rls_warning', 'warning'], + ]); + for (const b of banners) { + expect(typeof b.label, `${b.id} label`).toBe('string'); + expect(b.label.length, `${b.id} label`).toBeGreaterThan(0); + expect(b.bannerText.length, `${b.id} bannerText`).toBeGreaterThan(0); + } + }); + + it('places each banner in the section it concerns, directly under the field it warns about', () => { + const at = (id: string) => specs.findIndex((s) => s.id === id); + expect(sectionOf(at('adapter_switch_warning'))).toBe('adapter'); + expect(specs[at('adapter_switch_warning') - 1].key).toBe('adapter'); + expect(sectionOf(at('enforce_rls_warning'))).toBe('permissions'); + expect(specs[at('enforce_rls_warning') - 1].key).toBe('enforce_rls'); + }); + + it('carries the warning glyph nowhere — the banner severity is the only warning marker', () => { + expect(JSON.stringify(knowledgeSettingsManifest)).not.toContain('⚠'); + }); +}); + describe('knowledgeTestActionHandler', () => { it('warns for memory adapter (no service to probe)', async () => { const r = await knowledgeTestActionHandler({ values: { adapter: 'memory' } } as any); diff --git a/packages/services/service-settings/src/manifests/knowledge.manifest.ts b/packages/services/service-settings/src/manifests/knowledge.manifest.ts index 6edd7dad221..8e41db0e17f 100644 --- a/packages/services/service-settings/src/manifests/knowledge.manifest.ts +++ b/packages/services/service-settings/src/manifests/knowledge.manifest.ts @@ -42,9 +42,7 @@ const manifest = { icon: 'BookOpen', description: 'Vector-store backend for RAG / knowledge sources. ' + - '⚠ Switching adapter does NOT migrate existing indices — documents ' + - 'indexed under the previous adapter become unreachable until ' + - 're-indexed. The embedder is configured separately under AI.', + 'The embedder is configured separately under AI.', scope: 'global', readPermission: 'manage_platform_settings', writePermission: 'manage_platform_settings', @@ -61,6 +59,14 @@ const manifest = { { value: 'ragflow', label: 'RAGFlow (external)' }, ], }, + // Warnings are `info_banner` rows with `bannerSeverity: 'warning'`, never a + // sentence inside a description; each `id` addresses its copy under + // `settings.knowledge.banners.` in the locale bundles. + { type: 'info_banner', id: 'adapter_switch_warning', label: 'Existing indices are not migrated', + required: false, bannerSeverity: 'warning', + bannerText: + 'Switching adapter does NOT migrate existing indices — documents indexed under the ' + + 'previous adapter become unreachable until re-indexed.' }, // ── Turso / libSQL ──────────────────────────────────────────── { type: 'group', id: 'turso', label: 'Turso / libSQL', required: false, @@ -113,9 +119,12 @@ const manifest = { { type: 'group', id: 'permissions', label: 'Permissions', required: false }, { type: 'toggle', key: 'enforce_rls', label: 'Enforce RLS on search', required: false, default: true, - description: - 'Re-check every hit against the caller\'s record-level permissions via IDataEngine. ' + - '⚠ Disabling skips the platform\'s unique safeguard against vector-store data leakage — leave on in production.' }, + description: 'Re-check every hit against the caller\'s record-level permissions via IDataEngine.' }, + { type: 'info_banner', id: 'enforce_rls_warning', label: 'Leave RLS on in production', + required: false, bannerSeverity: 'warning', + bannerText: + 'Disabling skips the platform\'s unique safeguard against vector-store data leakage — ' + + 'leave on in production.' }, // ── Probe ───────────────────────────────────────────────────── { type: 'action_button', id: 'test', label: 'Test connection', diff --git a/packages/services/service-settings/src/manifests/storage.manifest.test.ts b/packages/services/service-settings/src/manifests/storage.manifest.test.ts index cdf58c89792..2f32e422ca3 100644 --- a/packages/services/service-settings/src/manifests/storage.manifest.test.ts +++ b/packages/services/service-settings/src/manifests/storage.manifest.test.ts @@ -50,6 +50,29 @@ describe('storageSettingsManifest', () => { }); }); +describe('storageSettingsManifest — the adapter-switch warning is an info_banner', () => { + const specs = storageSettingsManifest.specifiers as any[]; + const sectionOf = (i: number) => specs.slice(0, i + 1).reverse().find((s) => s.type === 'group')?.id; + + it('declares it as a warning-severity info_banner with an id, a title and a body', () => { + const banners = specs.filter((s) => s.type === 'info_banner'); + expect(banners.map((b) => [b.id, b.bannerSeverity])).toEqual([['adapter_switch_warning', 'warning']]); + expect(typeof banners[0].label).toBe('string'); + expect(banners[0].label.length).toBeGreaterThan(0); + expect(banners[0].bannerText.length).toBeGreaterThan(0); + }); + + it('places it in the Backend section, directly under the adapter select it concerns', () => { + const i = specs.findIndex((s) => s.id === 'adapter_switch_warning'); + expect(sectionOf(i)).toBe('adapter'); + expect(specs[i - 1].key).toBe('adapter'); + }); + + it('carries the warning glyph nowhere — the banner severity is the only warning marker', () => { + expect(JSON.stringify(storageSettingsManifest)).not.toContain('⚠'); + }); +}); + describe('storageTestActionHandler (fallback)', () => { it('rejects local adapter without local_root', async () => { const r = await storageTestActionHandler({ diff --git a/packages/services/service-settings/src/manifests/storage.manifest.ts b/packages/services/service-settings/src/manifests/storage.manifest.ts index a727230f5eb..53281346541 100644 --- a/packages/services/service-settings/src/manifests/storage.manifest.ts +++ b/packages/services/service-settings/src/manifests/storage.manifest.ts @@ -12,11 +12,7 @@ const manifest = { version: 1, label: 'File Storage', icon: 'HardDrive', - description: - 'Backend used for attachments, exports, and user uploads. ' + - '⚠ Switching adapter does not migrate existing files — files ' + - 'uploaded under the previous adapter become unreachable through ' + - 'the new one.', + description: 'Backend used for attachments, exports, and user uploads.', scope: 'global', readPermission: 'manage_platform_settings', writePermission: 'manage_platform_settings', @@ -31,6 +27,15 @@ const manifest = { { value: 's3', label: 'S3 / S3-compatible' }, ], }, + // The adapter-switch warning is a structured callout, not a sentence in + // the namespace description: `bannerSeverity` carries the severity an + // inline warning glyph used to spell by hand, and `id` addresses its copy + // under `settings.storage.banners.adapter_switch_warning` in each bundle. + { type: 'info_banner', id: 'adapter_switch_warning', label: 'Existing files are not migrated', + required: false, bannerSeverity: 'warning', + bannerText: + 'Switching adapter does not migrate existing files — files uploaded under the ' + + 'previous adapter become unreachable through the new one.' }, { type: 'group', id: 'local', label: 'Local', required: false, visible: "${data.adapter === 'local'}" }, diff --git a/packages/services/service-settings/src/translations/en.ts b/packages/services/service-settings/src/translations/en.ts index e64b3b5130e..6dcf8a6f856 100644 --- a/packages/services/service-settings/src/translations/en.ts +++ b/packages/services/service-settings/src/translations/en.ts @@ -216,15 +216,15 @@ export const en: PlatformTranslationData = { realtime_cursors: { label: 'Realtime cursors' }, inline_comments: { label: 'Inline comments' }, }, + banners: { + beta_notice: { title: 'Heads up', + text: 'Beta features may change without notice. Pin via env vars (e.g. `OS_FEATURE_FLAGS_AI_ENABLED=true`) to lock for the whole deployment.' }, + }, }, storage: { title: 'File Storage', - description: - 'Backend used for attachments, exports, and user uploads. ' + - '⚠ Switching adapter does not migrate existing files — files ' + - 'uploaded under the previous adapter become unreachable through ' + - 'the new one.', + description: 'Backend used for attachments, exports, and user uploads.', groups: { adapter: { title: 'Backend', description: 'Choose where uploaded files are stored.' }, local: { title: 'Local' }, @@ -255,6 +255,11 @@ export const en: PlatformTranslationData = { actions: { test: { label: 'Test connection' }, }, + banners: { + adapter_switch_warning: { title: 'Existing files are not migrated', + text: 'Switching adapter does not migrate existing files — files uploaded under the ' + + 'previous adapter become unreachable through the new one.' }, + }, }, ai: { @@ -313,7 +318,7 @@ export const en: PlatformTranslationData = { trace_enabled: { label: 'Record traces', help: 'Persist prompt/response traces to sys_ai_trace for debugging and replay.' }, log_prompts: { label: 'Log full prompts', - help: 'Include rendered prompts (not just metadata) in trace rows. ⚠ May leak PII — disable in regulated environments.' }, + help: 'Include rendered prompts (not just metadata) in trace rows.' }, embedder_provider: { label: 'Provider', options: { @@ -344,13 +349,15 @@ export const en: PlatformTranslationData = { test: { label: 'Test connection' }, test_embedder: { label: 'Test embedder' }, }, + banners: { + log_prompts_warning: { title: 'Prompts may contain PII', + text: 'May leak PII — disable in regulated environments.' }, + }, }, knowledge: { title: 'Knowledge', - description: - 'Vector-store backend for RAG / knowledge sources. ' + - '⚠ Switching adapter does NOT migrate existing indices.', + description: 'Vector-store backend for RAG / knowledge sources.', groups: { adapter: { title: 'Backend', description: 'Choose where document chunks and their vectors are stored.' }, @@ -386,11 +393,19 @@ export const en: PlatformTranslationData = { over_fetch: { label: 'Over-fetch multiplier', help: 'Internal topK × overFetch candidates fetched so JS-side metadata filtering still has rows.' }, enforce_rls: { label: 'Enforce RLS on search', - help: 'Re-check every hit against the caller\'s record-level permissions. ⚠ Disabling skips the platform\'s unique safeguard.' }, + help: 'Re-check every hit against the caller\'s record-level permissions.' }, }, actions: { test: { label: 'Test connection' }, }, + banners: { + adapter_switch_warning: { title: 'Existing indices are not migrated', + text: 'Switching adapter does NOT migrate existing indices — documents indexed under the ' + + 'previous adapter become unreachable until re-indexed.' }, + enforce_rls_warning: { title: 'Leave RLS on in production', + text: 'Disabling skips the platform\'s unique safeguard against vector-store data leakage — ' + + 'leave on in production.' }, + }, }, }, }; diff --git a/packages/services/service-settings/src/translations/es-ES.ts b/packages/services/service-settings/src/translations/es-ES.ts index d8603430e1b..ad6e3e62871 100644 --- a/packages/services/service-settings/src/translations/es-ES.ts +++ b/packages/services/service-settings/src/translations/es-ES.ts @@ -340,15 +340,15 @@ export const esES: PlatformTranslationData = { realtime_cursors: { label: 'Cursores en tiempo real' }, inline_comments: { label: 'Comentarios en línea' }, }, + banners: { + beta_notice: { title: 'Atención', + text: 'Las funciones beta pueden cambiar sin previo aviso. Fíjalas con variables de entorno (p. ej., `OS_FEATURE_FLAGS_AI_ENABLED=true`) para bloquearlas en todo el despliegue.' }, + }, }, storage: { title: 'Almacenamiento de archivos', - description: - 'Backend usado para adjuntos, exportaciones y subidas de usuarios. ' + - '⚠ Cambiar de adaptador no migra los archivos existentes: los archivos ' + - 'subidos con el adaptador anterior dejan de ser accesibles a través ' + - 'del nuevo.', + description: 'Backend usado para adjuntos, exportaciones y subidas de usuarios.', groups: { adapter: { title: 'Backend', description: 'Elige dónde se almacenan los archivos subidos.' }, local: { title: 'Local' }, @@ -379,6 +379,11 @@ export const esES: PlatformTranslationData = { actions: { test: { label: 'Probar conexión' }, }, + banners: { + adapter_switch_warning: { title: 'Los archivos existentes no se migran', + text: 'Cambiar de adaptador no migra los archivos existentes: los archivos ' + + 'subidos con el adaptador anterior dejan de ser accesibles a través del nuevo.' }, + }, }, ai: { @@ -462,7 +467,7 @@ export const esES: PlatformTranslationData = { trace_enabled: { label: 'Registrar trazas', help: 'Persiste las trazas de prompt/respuesta en sys_ai_trace para depuración y reproducción.' }, log_prompts: { label: 'Registrar prompts completos', - help: 'Incluye los prompts renderizados (no solo metadatos) en las filas de traza. ⚠ Puede filtrar PII: desactívalo en entornos regulados.' }, + help: 'Incluye los prompts renderizados (no solo metadatos) en las filas de traza.' }, embedder_provider: { label: 'Proveedor', options: { @@ -493,13 +498,15 @@ export const esES: PlatformTranslationData = { test: { label: 'Probar conexión' }, test_embedder: { label: 'Probar embedder' }, }, + banners: { + log_prompts_warning: { title: 'Los prompts pueden contener PII', + text: 'Puede filtrar PII: desactívalo en entornos regulados.' }, + }, }, knowledge: { title: 'Conocimiento', - description: - 'Backend de almacén de vectores para RAG / fuentes de conocimiento. ' + - '⚠ Cambiar de adaptador NO migra los índices existentes.', + description: 'Backend de almacén de vectores para RAG / fuentes de conocimiento.', groups: { adapter: { title: 'Backend', description: 'Elige dónde se almacenan los fragmentos de documento y sus vectores.' }, @@ -535,11 +542,17 @@ export const esES: PlatformTranslationData = { over_fetch: { label: 'Multiplicador de sobre-obtención', help: 'Se obtienen topK × overFetch candidatos internos para que el filtrado de metadatos en JS siga teniendo filas.' }, enforce_rls: { label: 'Aplicar RLS en la búsqueda', - help: 'Vuelve a comprobar cada resultado contra los permisos a nivel de registro del solicitante. ⚠ Desactivarlo omite la salvaguarda exclusiva de la plataforma.' }, + help: 'Vuelve a comprobar cada resultado contra los permisos a nivel de registro del solicitante.' }, }, actions: { test: { label: 'Probar conexión' }, }, + banners: { + adapter_switch_warning: { title: 'Los índices existentes no se migran', + text: 'Cambiar de adaptador NO migra los índices existentes.' }, + enforce_rls_warning: { title: 'Mantenlo activado en producción', + text: 'Desactivarlo omite la salvaguarda exclusiva de la plataforma.' }, + }, }, }, }; diff --git a/packages/services/service-settings/src/translations/ja-JP.ts b/packages/services/service-settings/src/translations/ja-JP.ts index b37cc1c6d40..f9ba2e25f00 100644 --- a/packages/services/service-settings/src/translations/ja-JP.ts +++ b/packages/services/service-settings/src/translations/ja-JP.ts @@ -340,13 +340,15 @@ export const jaJP: PlatformTranslationData = { realtime_cursors: { label: 'リアルタイムカーソル' }, inline_comments: { label: 'インラインコメント' }, }, + banners: { + beta_notice: { title: 'ご注意ください', + text: 'ベータ機能は予告なく変更される場合があります。デプロイ全体で固定するには環境変数 (例: `OS_FEATURE_FLAGS_AI_ENABLED=true`) を使用してください。' }, + }, }, storage: { title: 'ファイルストレージ', - description: - '添付ファイル・エクスポート・ユーザーアップロードに使用するバックエンド。' + - '⚠ アダプターを切替えても既存ファイルは移行されません。以前のアダプターでアップロードされたファイルは新しいアダプターからアクセスできなくなります。', + description: '添付ファイル・エクスポート・ユーザーアップロードに使用するバックエンド。', groups: { adapter: { title: 'バックエンド', description: 'アップロードファイルの保存先を選択します。' }, local: { title: 'ローカル' }, @@ -377,6 +379,10 @@ export const jaJP: PlatformTranslationData = { actions: { test: { label: '接続テスト' }, }, + banners: { + adapter_switch_warning: { title: '既存ファイルは移行されません', + text: 'アダプターを切替えても既存ファイルは移行されません。以前のアダプターでアップロードされたファイルは新しいアダプターからアクセスできなくなります。' }, + }, }, ai: { @@ -459,7 +465,7 @@ export const jaJP: PlatformTranslationData = { trace_enabled: { label: 'トレースを記録', help: 'デバッグと再生のため prompt/response トレースを sys_ai_trace に保存します。' }, log_prompts: { label: '完全なプロンプトを記録', - help: 'メタデータだけでなくレンダリングされたプロンプトをトレース行に含めます。⚠ PII が漏えいする可能性があります。規制環境では無効にしてください。' }, + help: 'メタデータだけでなくレンダリングされたプロンプトをトレース行に含めます。' }, embedder_provider: { label: 'プロバイダー', options: { @@ -490,13 +496,15 @@ export const jaJP: PlatformTranslationData = { test: { label: '接続テスト' }, test_embedder: { label: 'Embedder をテスト' }, }, + banners: { + log_prompts_warning: { title: 'PII 漏えいの可能性', + text: 'PII が漏えいする可能性があります。規制環境では無効にしてください。' }, + }, }, knowledge: { title: 'ナレッジ', - description: - 'RAG / ナレッジソース用のベクトルストアバックエンド。' + - '⚠ アダプターを切替えても既存のインデックスは移行されません。', + description: 'RAG / ナレッジソース用のベクトルストアバックエンド。', groups: { adapter: { title: 'バックエンド', description: 'ドキュメントチャンクとそのベクトルの保存先を選択します。' }, @@ -532,11 +540,17 @@ export const jaJP: PlatformTranslationData = { over_fetch: { label: 'オーバーフェッチ倍率', help: 'JS 側のメタデータフィルタリングでも行が残るよう、内部で topK × overFetch 件の候補を取得します。' }, enforce_rls: { label: '検索時に RLS を強制', - help: '各ヒットを呼び出し元のレコードレベル権限に対して再確認します。⚠ 無効化するとプラットフォーム固有のセーフガードがスキップされます。' }, + help: '各ヒットを呼び出し元のレコードレベル権限に対して再確認します。' }, }, actions: { test: { label: '接続テスト' }, }, + banners: { + adapter_switch_warning: { title: '既存のインデックスは移行されません', + text: 'アダプターを切替えても既存のインデックスは移行されません。' }, + enforce_rls_warning: { title: '本番環境では有効のままにしてください', + text: '無効化するとプラットフォーム固有のセーフガードがスキップされます。' }, + }, }, }, }; diff --git a/packages/services/service-settings/src/translations/settings-translation-coverage.test.ts b/packages/services/service-settings/src/translations/settings-translation-coverage.test.ts index b381c1d99ae..e44a94b39be 100644 --- a/packages/services/service-settings/src/translations/settings-translation-coverage.test.ts +++ b/packages/services/service-settings/src/translations/settings-translation-coverage.test.ts @@ -11,7 +11,8 @@ * * This test pins each shipped non-English locale to full coverage of every * manifest's structural strings — namespace title/description, each group - * title, and each field label. Dropdown *option* labels are intentionally + * title, each field label, and each `info_banner`'s title and text (keyed by + * the banner's `id` under `settings..banners`). Dropdown *option* labels are intentionally * excluded: several must not be "translated" (the `timezone` option "UTC" is a * code; each `locale` option names its language in that language), and * option-level parity is filled per-locale on a best-effort basis. @@ -46,6 +47,15 @@ function missingFor(data: { settings?: Record }, m: SettingsManifes for (const s of m.specifiers) { if (s.type === 'group') { if (s.id && tr?.groups?.[s.id]?.title == null) missing.push(`group:${s.id}`); + } else if (s.type === 'info_banner') { + // A banner without an `id` has no address in the bundle, so no locale + // can ever translate it: that is a gap in the manifest, reported here. + if (!s.id) { + missing.push('banner without an id'); + continue; + } + if (tr?.banners?.[s.id]?.title == null) missing.push(`banner:${s.id}.title`); + if (tr?.banners?.[s.id]?.text == null) missing.push(`banner:${s.id}.text`); } else if (s.key) { if (tr?.keys?.[s.key]?.label == null) missing.push(`key:${s.key}`); } @@ -54,8 +64,8 @@ function missingFor(data: { settings?: Record }, m: SettingsManifes } /** - * The other direction: copy for a group or key no manifest declares. Such an - * entry renders nowhere, so it survives every review as dead text. The four + * The other direction: copy for a group, key or banner no manifest declares. + * Such an entry renders nowhere, so it survives every review as dead text. The four * Localization format keys retired by #21958 are the case that named it. */ function undeclaredIn(data: { settings?: Record }): string[] { @@ -69,15 +79,19 @@ function undeclaredIn(data: { settings?: Record }): string[] { } const groups = new Set(); const keys = new Set(); + const banners = new Set(); for (const s of m.specifiers) { if (s.type === 'group') { if (s.id) groups.add(s.id); + } else if (s.type === 'info_banner') { + if (s.id) banners.add(s.id); } else if (s.key) { keys.add(s.key); } } for (const g of Object.keys(tr?.groups ?? {})) if (!groups.has(g)) extra.push(`${ns}.group:${g}`); for (const k of Object.keys(tr?.keys ?? {})) if (!keys.has(k)) extra.push(`${ns}.key:${k}`); + for (const b of Object.keys(tr?.banners ?? {})) if (!banners.has(b)) extra.push(`${ns}.banner:${b}`); } return extra; } @@ -89,7 +103,7 @@ describe('settings translation coverage', () => { for (const [locale, data] of LOCALES) { for (const m of manifests) { - it(`${locale} covers settings.${m.namespace} (title / groups / keys)`, () => { + it(`${locale} covers settings.${m.namespace} (title / groups / keys / banners)`, () => { const missing = missingFor(data, m); expect(missing, `${locale} missing for ${m.namespace}: ${missing.join(', ')}`).toEqual([]); }); @@ -97,9 +111,62 @@ describe('settings translation coverage', () => { } for (const [locale, data] of [['en', en], ...LOCALES] as typeof LOCALES) { - it(`${locale} carries no copy for a group or key no manifest declares`, () => { + it(`${locale} carries no copy for a group, key or banner no manifest declares`, () => { const extra = undeclaredIn(data); expect(extra, `${locale} has undeclared settings copy: ${extra.join(', ')}`).toEqual([]); }); } }); + +/** + * A warning reaches the admin as an `info_banner` row with + * `bannerSeverity: 'warning'` — never as a hand-spelled warning glyph inside a + * description or help string, where it reads as one more sentence of the + * subtitle or the field's help. This sweeps every manifest's description, its + * `helpText` and each specifier's help (`description`), and every locale's + * copy of them (`description`, group `description`, key `help`). + */ +const WARNING_GLYPH = '⚠'; + +function manifestHelpStrings(m: SettingsManifest): Array<[string, string]> { + const out: Array<[string, string]> = []; + if (m.description) out.push([`${m.namespace}.description`, m.description]); + if (m.helpText) out.push([`${m.namespace}.helpText`, m.helpText]); + for (const s of m.specifiers) { + if (s.description) out.push([`${m.namespace}.${s.key ?? s.id ?? s.type}.description`, s.description]); + } + return out; +} + +function bundleHelpStrings(data: { settings?: Record }): Array<[string, string]> { + const out: Array<[string, string]> = []; + for (const [ns, tr] of Object.entries(data.settings ?? {})) { + if (typeof tr?.description === 'string') out.push([`${ns}.description`, tr.description]); + for (const [g, gt] of Object.entries(tr?.groups ?? {})) { + if (typeof gt?.description === 'string') out.push([`${ns}.groups.${g}.description`, gt.description]); + } + for (const [k, kt] of Object.entries(tr?.keys ?? {})) { + if (typeof kt?.help === 'string') out.push([`${ns}.keys.${k}.help`, kt.help]); + } + } + return out; +} + +describe('warnings live in info_banner rows, not in description or help strings', () => { + it('no manifest description, helpText or specifier help carries the warning glyph', () => { + const strings = manifests.flatMap(manifestHelpStrings); + // Non-vacuity: the sweep reads real strings, not an empty set. + expect(strings.length).toBeGreaterThan(50); + const hits = strings.filter(([, v]) => v.includes(WARNING_GLYPH)).map(([path]) => path); + expect(hits, `warning glyph in manifest help strings: ${hits.join(', ')}`).toEqual([]); + }); + + for (const [locale, data] of [['en', en], ...LOCALES] as typeof LOCALES) { + it(`${locale} carries the warning glyph in no description or help copy`, () => { + const strings = bundleHelpStrings(data); + expect(strings.length).toBeGreaterThan(50); + const hits = strings.filter(([, v]) => v.includes(WARNING_GLYPH)).map(([path]) => path); + expect(hits, `${locale} has the warning glyph in: ${hits.join(', ')}`).toEqual([]); + }); + } +}); diff --git a/packages/services/service-settings/src/translations/zh-CN.ts b/packages/services/service-settings/src/translations/zh-CN.ts index 605e0fbcc67..234f8f8bfe1 100644 --- a/packages/services/service-settings/src/translations/zh-CN.ts +++ b/packages/services/service-settings/src/translations/zh-CN.ts @@ -405,13 +405,15 @@ export const zhCN: PlatformTranslationData = { realtime_cursors: { label: '实时光标' }, inline_comments: { label: '行内评论' }, }, + banners: { + beta_notice: { title: '注意', + text: 'Beta 功能可能随时变更,恕不另行通知。可通过环境变量固定(例如 `OS_FEATURE_FLAGS_AI_ENABLED=true`),对整个部署生效。' }, + }, }, storage: { title: '文件存储', - description: - '附件、导出文件与用户上传所使用的存储后端。' + - '⚠ 切换适配器不会迁移已有文件 —— 通过旧适配器上传的文件,在新适配器中将不可访问。', + description: '附件、导出文件与用户上传所使用的存储后端。', groups: { adapter: { title: '存储后端', description: '选择上传文件的存放位置。' }, local: { title: '本地' }, @@ -442,6 +444,10 @@ export const zhCN: PlatformTranslationData = { actions: { test: { label: '测试连接' }, }, + banners: { + adapter_switch_warning: { title: '已有文件不会迁移', + text: '切换适配器不会迁移已有文件 —— 通过旧适配器上传的文件,在新适配器中将不可访问。' }, + }, }, ai: { @@ -506,7 +512,7 @@ export const zhCN: PlatformTranslationData = { max_tokens: { label: '最大输出 tokens', help: '单次响应生成的硬上限。' }, request_timeout_ms: { label: '请求超时(毫秒)' }, trace_enabled: { label: '记录 Trace', help: '将 prompt/response 落入 sys_ai_trace,便于调试与回放。' }, - log_prompts: { label: '记录完整 Prompt', help: '在 trace 行中包含完整 prompt 而非仅元数据。⚠ 可能泄露 PII,合规场景请关闭。' }, + log_prompts: { label: '记录完整 Prompt', help: '在 trace 行中包含完整 prompt 而非仅元数据。' }, embedder_provider: { label: '提供商', options: { @@ -532,11 +538,14 @@ export const zhCN: PlatformTranslationData = { test: { label: '测试连接' }, test_embedder: { label: '测试 Embedder' }, }, + banners: { + log_prompts_warning: { title: 'Prompt 可能含有 PII', text: '可能泄露 PII,合规场景请关闭。' }, + }, }, knowledge: { title: '知识库', - description: 'RAG / 知识源使用的向量存储后端。⚠ 切换适配器不会迁移已有索引。', + description: 'RAG / 知识源使用的向量存储后端。', groups: { adapter: { title: '后端', description: '选择文档分块及其向量的存储位置。' }, turso: { title: 'Turso / libSQL', description: '支持托管 Turso、本地文件、内存三种模式。' }, @@ -561,11 +570,15 @@ export const zhCN: PlatformTranslationData = { chunk_target: { label: '目标 chunk 大小(字符)', help: '在按 token 切分之前的软上限。' }, chunk_overlap: { label: 'Chunk 重叠(字符)', help: '保留上一个 chunk 末尾的字符,以保证跨界上下文。' }, over_fetch: { label: '过取倍数', help: '内部按 topK × overFetch 拉取候选,以便 JS 端元数据过滤仍有行可返回。' }, - enforce_rls: { label: '搜索时强制 RLS', help: '对每条命中通过 IDataEngine 再次校验调用方的行级权限。⚠ 关闭将跳过平台对向量存储数据外泄的独有防护。' }, + enforce_rls: { label: '搜索时强制 RLS', help: '对每条命中通过 IDataEngine 再次校验调用方的行级权限。' }, }, actions: { test: { label: '测试连接' }, }, + banners: { + adapter_switch_warning: { title: '已有索引不会迁移', text: '切换适配器不会迁移已有索引。' }, + enforce_rls_warning: { title: '生产环境请保持开启', text: '关闭将跳过平台对向量存储数据外泄的独有防护。' }, + }, }, }, };