diff --git a/.changeset/28064-data-lifecycle-settings-translations.md b/.changeset/28064-data-lifecycle-settings-translations.md new file mode 100644 index 00000000000..405934e9f83 --- /dev/null +++ b/.changeset/28064-data-lifecycle-settings-translations.md @@ -0,0 +1,12 @@ +--- +'@objectstack/service-settings': patch +--- + +fix(i18n): the Data Lifecycle settings page reads in the admin's language + +Clause-②: no +Metadata-protocol: none + +Setup → All Settings → Data Lifecycle read English under Chinese (zh-CN), Japanese (ja-JP) and Spanish (es-ES): the hub card's title and description and every field on the page, while every other settings page and the Setup sidebar entry for this page were translated. The page now reads in those languages too: its title, description, and each field's label and help (Enforce lifecycle policies, Retention overrides, Row quotas, Per-class quota defaults, Growth alert threshold). The hub card and the sidebar entry now carry the same name in every shipped locale. + +Nothing to do: the translations ship in the built-in settings bundle and load with the settings service, as the other pages' do. The JSON examples inside the help text stay as written, since they are the values you type. diff --git a/.changeset/28069-ai-knowledge-help-business-words.md b/.changeset/28069-ai-knowledge-help-business-words.md new file mode 100644 index 00000000000..4eff66e80e4 --- /dev/null +++ b/.changeset/28069-ai-knowledge-help-business-words.md @@ -0,0 +1,18 @@ +--- +'@objectstack/service-settings': patch +--- + +fix(service-settings): AI & Embedder and Knowledge help is in business words + +Clause-②: no +Metadata-protocol: none + +On Setup → Settings → AI & Embedder and Knowledge, the help under several fields read like code comments, in every language: "Forwarded as OPENAI_API_KEY", traces saved "to sys_ai_trace", "Chunks per embed() call", "Per-source values on KnowledgeSource.adapterConfig take precedence", "topK × overFetch". The help now says what each setting does: + +- A provider API key or the gateway model is "used instead of the one set in the server configuration". +- Record traces keeps a record of each AI request and its response, so a conversation can be inspected and replayed. +- Batch size is the number of chunks sent to the embedding provider in one request. +- Indexing defaults: a value set on a knowledge source itself wins over these defaults. +- Over-fetch multiplier: search fetches this many times the requested number of results, so enough are left after filtering. + +The English, Chinese, Japanese and Spanish text changed together. No setting, default or accepted value changed, and model examples such as openai/gpt-4o stay as written because they are what you type. diff --git a/packages/services/service-settings/src/manifests/ai.manifest.ts b/packages/services/service-settings/src/manifests/ai.manifest.ts index 2121b65c207..2a6727549ea 100644 --- a/packages/services/service-settings/src/manifests/ai.manifest.ts +++ b/packages/services/service-settings/src/manifests/ai.manifest.ts @@ -57,9 +57,9 @@ const manifest = { // ── Vercel AI Gateway ───────────────────────────────────────── { type: 'group', id: 'gateway', label: 'Vercel AI Gateway', required: false, visible: "${data.provider === 'gateway'}", - description: 'Multi-provider router. The model spec follows `provider/model`, e.g. `openai/gpt-4o`.' }, + description: 'Multi-provider router. The model is written provider/model, e.g. openai/gpt-4o.' }, { type: 'text', key: 'gateway_model', label: 'Gateway model', required: true, - description: 'Forwarded as AI_GATEWAY_MODEL. Format: provider/model, e.g. openai/gpt-4o or anthropic/claude-sonnet-4.6.', + description: 'Used instead of the model set in the server configuration. Format: provider/model, e.g. openai/gpt-4o or anthropic/claude-sonnet-4.6.', pattern: '^[A-Za-z0-9_-]+\\/[A-Za-z0-9._:-]+$', visible: "${data.provider === 'gateway'}" }, { type: 'password', key: 'gateway_api_key', label: 'Gateway API key', @@ -72,7 +72,7 @@ const manifest = { visible: "${data.provider === 'openai'}" }, { type: 'password', key: 'openai_api_key', label: 'OpenAI API key', required: true, encrypted: true, - description: 'Forwarded as OPENAI_API_KEY. Stored encrypted at rest.', + description: 'Used instead of the key set in the server configuration. Stored encrypted at rest.', visible: "${data.provider === 'openai'}" }, { type: 'text', key: 'openai_model', label: 'Model', required: false, default: 'gpt-4o', description: 'Default model id. Per-agent overrides take precedence.', @@ -86,7 +86,7 @@ const manifest = { visible: "${data.provider === 'anthropic'}" }, { type: 'password', key: 'anthropic_api_key', label: 'Anthropic API key', required: true, encrypted: true, - description: 'Forwarded as ANTHROPIC_API_KEY. Stored encrypted at rest.', + description: 'Used instead of the key set in the server configuration. Stored encrypted at rest.', visible: "${data.provider === 'anthropic'}" }, { type: 'text', key: 'anthropic_model', label: 'Model', required: false, default: 'claude-sonnet-4-20250514', @@ -97,7 +97,7 @@ const manifest = { visible: "${data.provider === 'google'}" }, { type: 'password', key: 'google_api_key', label: 'Google API key', required: true, encrypted: true, - description: 'Forwarded as GOOGLE_GENERATIVE_AI_API_KEY. Stored encrypted at rest.', + description: 'Used instead of the key set in the server configuration. Stored encrypted at rest.', visible: "${data.provider === 'google'}" }, { type: 'text', key: 'google_model', label: 'Model', required: false, default: 'gemini-2.0-flash', @@ -138,14 +138,14 @@ const manifest = { { type: 'group', id: 'cloudflare', label: 'Cloudflare AI Gateway', required: false, visible: "${data.provider === 'cloudflare'}", description: - 'Uses the /compat endpoint so the model id is `provider/model` (e.g. ' + - '`openai/gpt-4o-mini`, `anthropic/claude-3-5-sonnet`, `deepseek/deepseek-chat`). ' + + 'Uses the /compat endpoint, so the model is written provider/model (e.g. ' + + 'openai/gpt-4o-mini, anthropic/claude-3-5-sonnet, deepseek/deepseek-chat). ' + 'Note: alibaba/qwen* is NOT supported by Cloudflare /compat — use the DashScope provider for Qwen.' }, { type: 'text', key: 'cloudflare_account_id', label: 'Cloudflare account id', required: true, description: 'The 32-char hex id from your Cloudflare dashboard URL.', visible: "${data.provider === 'cloudflare'}" }, { type: 'text', key: 'cloudflare_gateway_id', label: 'Gateway id', required: false, default: 'default', - description: 'Gateway name configured in Cloudflare → AI Gateway. Defaults to `default`.', + description: 'Gateway name configured in Cloudflare → AI Gateway. Defaults to "default".', visible: "${data.provider === 'cloudflare'}" }, { type: 'password', key: 'cloudflare_api_key', label: 'Cloudflare AI Gateway token', required: true, encrypted: true, @@ -226,7 +226,7 @@ const manifest = { { type: 'group', id: 'observability', label: 'Observability', required: false }, { type: 'toggle', key: 'trace_enabled', label: 'Record traces', required: false, default: true, - description: 'Persist prompt/response traces to sys_ai_trace for debugging and replay.' }, + description: 'Keep a record of each AI request and its response, so a conversation can be inspected and replayed when something goes wrong.' }, { type: 'toggle', key: 'log_prompts', label: 'Log full prompts', required: false, default: false, description: 'Include rendered prompts (not just metadata) in trace rows.' }, @@ -303,7 +303,7 @@ const manifest = { visible: "${data.embedder_provider && data.embedder_provider !== 'none'}" }, { type: 'number', key: 'embedder_batch_size', label: 'Batch size', required: false, default: 64, min: 1, max: 2048, - description: 'Chunks per embed() call. Reduce if hitting provider rate / size limits.', + description: 'Chunks sent to the embedding provider in one request. Reduce if hitting provider rate / size limits.', visible: "${data.embedder_provider && data.embedder_provider !== 'none'}" }, { type: 'action_button', id: 'test_embedder', label: 'Test embedder', required: false, icon: 'Plug', diff --git a/packages/services/service-settings/src/manifests/knowledge.manifest.ts b/packages/services/service-settings/src/manifests/knowledge.manifest.ts index e7596275746..42b8e249d23 100644 --- a/packages/services/service-settings/src/manifests/knowledge.manifest.ts +++ b/packages/services/service-settings/src/manifests/knowledge.manifest.ts @@ -73,8 +73,8 @@ const manifest = { visible: "${data.adapter === 'turso'}", description: 'Works against managed Turso (libsql://…), local file (file:./knowledge.db), ' + - 'or in-memory (:memory:). For per-tenant cloud deployments, leave blank to ' + - 'reuse the tenant\'s primary libSQL connection.' }, + 'or in-memory (:memory:). In a cloud deployment with a database per organization, leave blank to ' + + 'reuse that organization\'s own database connection.' }, { type: 'text', key: 'turso_url', label: 'Connection URL', required: false, description: 'Examples: libsql://your-tenant.turso.io · file:./.objectstack/knowledge.db · :memory:', visible: "${data.adapter === 'turso'}" }, @@ -95,12 +95,12 @@ const manifest = { visible: "${data.adapter === 'ragflow'}" }, { type: 'text', key: 'ragflow_default_dataset', label: 'Default dataset id', required: false, - description: 'Used when a KnowledgeSource does not specify its own RAGFlow dataset.', + description: 'Used when a knowledge source does not name its own RAGFlow dataset.', visible: "${data.adapter === 'ragflow'}" }, // ── Indexing defaults ───────────────────────────────────────── { type: 'group', id: 'indexing', label: 'Indexing defaults', required: false, - description: 'Per-source values on KnowledgeSource.adapterConfig take precedence.', + description: 'A value set on a knowledge source itself wins over these defaults.', visible: "${data.adapter !== 'memory'}" }, { type: 'number', key: 'chunk_target', label: 'Target chunk size (chars)', required: false, default: 800, min: 64, max: 8192, @@ -112,14 +112,14 @@ const manifest = { visible: "${data.adapter !== 'memory'}" }, { type: 'number', key: 'over_fetch', label: 'Over-fetch multiplier', required: false, default: 4, min: 1, max: 20, - description: 'Internal `topK * overFetch` candidates fetched so JS-side metadata filtering still has rows to return.', + description: 'Search fetches this many times the requested number of results, so enough are left to return after they are filtered by document details.', visible: "${data.adapter === 'turso'}" }, // ── Permissions ─────────────────────────────────────────────── { 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.' }, + description: 'Re-check every hit against the caller\'s record-level permissions.' }, { type: 'info_banner', id: 'enforce_rls_warning', label: 'Leave RLS on in production', required: false, bannerSeverity: 'warning', bannerText: diff --git a/packages/services/service-settings/src/settings-help-business-words.pin.test.ts b/packages/services/service-settings/src/settings-help-business-words.pin.test.ts index c0d6224451e..2e7c4b4cceb 100644 --- a/packages/services/service-settings/src/settings-help-business-words.pin.test.ts +++ b/packages/services/service-settings/src/settings-help-business-words.pin.test.ts @@ -12,7 +12,13 @@ * The technical detail lives in the deployment docs; the help names the * effect in business words. * - * The sweep reads every string an administrator is shown for those three + * AI & Embedder and Knowledge had the same problem (#28069): the help named the + * env vars a saved key or model stands in for (`OPENAI_API_KEY`, + * `AI_GATEWAY_MODEL`), the trace object, the embedding call and the knowledge + * source's config path. They now say what the setting does, and a saved key + * is described as used instead of the one in the server configuration. + * + * The sweep reads every string an administrator is shown for those five * namespaces — the manifest literals (the English fallback) and each shipped * locale's copy — and refuses the four shapes that only a developer can read. * It pins SHAPES, never wording: a rewrite that stays in business words keeps @@ -24,12 +30,17 @@ import type { SettingsManifest } from '@objectstack/spec/system'; import { authSettingsManifest } from './manifests/auth.manifest.js'; import { featureFlagsSettingsManifest } from './manifests/feature-flags.manifest.js'; import { mailSettingsManifest } from './manifests/mail.manifest.js'; +import { aiSettingsManifest } from './manifests/ai.manifest.js'; +import { knowledgeSettingsManifest } from './manifests/knowledge.manifest.js'; import { en, zhCN, jaJP, esES } from './translations/index.js'; const MANIFESTS: SettingsManifest[] = [ authSettingsManifest, featureFlagsSettingsManifest, mailSettingsManifest, + // #28069 + aiSettingsManifest, + knowledgeSettingsManifest, ]; const LOCALES: Array<[string, { settings?: Record; settingsCommon?: Record }]> = [ @@ -89,7 +100,7 @@ function packStrings(tr: Record | undefined, ns: string): Array<[st return out; } -describe('Settings help is in business words (#27853)', () => { +describe('Settings help is in business words (#27853, #28069)', () => { it('the sweep has teeth: each shape catches the string it was written for', () => { // Every shape is exercised by a string the old help carried, so a regex // that stops matching anything goes red here instead of passing quietly. @@ -98,6 +109,12 @@ describe('Settings help is in business words (#27853)', () => { expect(shapesIn('emailAndPassword.requireEmailVerification: false')).not.toEqual([]); expect(shapesIn('declared via defineWebhook() is read')).not.toEqual([]); expect(shapesIn('Pin via env vars (e.g. `X`)')).not.toEqual([]); + // The strings #28069 was filed for. + expect(shapesIn('Forwarded as GOOGLE_GENERATIVE_AI_API_KEY.')).not.toEqual([]); + expect(shapesIn('Persist traces to sys_ai_trace for replay.')).not.toEqual([]); + expect(shapesIn('Per-source values on KnowledgeSource.adapterConfig take precedence.')).not.toEqual([]); + expect(shapesIn('Chunks per embed() call.')).not.toEqual([]); + expect(shapesIn('Format: provider/model, e.g. openai/gpt-4o. Examples: deepseek-chat (V3).')).toEqual([]); expect(shapesIn('Invitation only closes self-registration (SCIM, e.g. smtp.example.com).')).toEqual([]); }); diff --git a/packages/services/service-settings/src/setup-nav-hub-parity.test.ts b/packages/services/service-settings/src/setup-nav-hub-parity.test.ts index ef286c1f02a..d93ab3d8376 100644 --- a/packages/services/service-settings/src/setup-nav-hub-parity.test.ts +++ b/packages/services/service-settings/src/setup-nav-hub-parity.test.ts @@ -27,11 +27,11 @@ * the bundled manifests (as `os serve` does) and registers * `lifecycleSettingsManifest` the way `ObjectQLPlugin` does at `kernel:ready`. * - * Data Lifecycle is `@objectstack/objectql`'s manifest, and no bundle carries a - * translated title for it, so its hub card reads "Data Lifecycle" in every - * locale. That gap is pinned by name below ({@link HUB_UNTRANSLATED}), so a - * translation added later turns the label comparison on for it rather than - * leaving it unchecked. + * Every page the hub lists carries a translated title in each shipped locale, + * Data Lifecycle included: it is `@objectstack/objectql`'s manifest, and its + * copy lives in {@link settingsBuiltinTranslations} beside the bundled ones + * (the coverage test there holds it to the same rule), so the label comparison + * runs for every page and no page is exempt from it. */ import { describe, expect, it } from 'vitest'; @@ -48,9 +48,6 @@ const SETTINGS_PAGE = /^\/apps\/setup\/system\/settings\/([a-z_]+)$/; const TRANSLATED_LOCALES = ['zh-CN', 'ja-JP', 'es-ES'] as const; -/** Namespaces the hub lists with no translated title in any shipped locale. */ -const HUB_UNTRANSLATED = ['lifecycle']; - interface NavItem { id?: string; type?: string; label?: string; url?: string; children?: NavItem[] } async function bootHub(): Promise<{ manifests: SettingsManifest[]; registered: unknown[] }> { @@ -145,8 +142,8 @@ describe('Setup sidebar and settings hub list the same pages under the same name expect(drift).toEqual([]); }); - it(`${locale}: the hub pages with no translated title are exactly the named ones`, () => { - expect(manifests.filter((m) => hubTitle(locale, m) == null).map((m) => m.namespace).sort()).toEqual(HUB_UNTRANSLATED); + it(`${locale}: every hub page carries a translated title`, () => { + expect(manifests.filter((m) => hubTitle(locale, m) == null).map((m) => m.namespace)).toEqual([]); }); } }); diff --git a/packages/services/service-settings/src/translations/en.ts b/packages/services/service-settings/src/translations/en.ts index eaba9945315..3f60f8597b9 100644 --- a/packages/services/service-settings/src/translations/en.ts +++ b/packages/services/service-settings/src/translations/en.ts @@ -293,7 +293,7 @@ export const en: PlatformTranslationData = { provider: { title: 'Provider', description: 'Choose the LLM backend. Memory mode echoes input — useful for tests but never for production.' }, gateway: { title: 'Vercel AI Gateway', - description: 'Multi-provider router. The model spec follows `provider/model`, e.g. `openai/gpt-4o`.' }, + description: 'Multi-provider router. The model is written provider/model, e.g. openai/gpt-4o.' }, openai: { title: 'OpenAI' }, anthropic: { title: 'Anthropic' }, google: { title: 'Google' }, @@ -317,20 +317,20 @@ export const en: PlatformTranslationData = { }, }, gateway_model: { label: 'Gateway model', - help: 'Forwarded as AI_GATEWAY_MODEL. Example: openai/gpt-4o' }, + help: 'Used instead of the model set in the server configuration. Example: openai/gpt-4o' }, gateway_api_key: { label: 'Gateway API key', help: 'Optional — required only if the gateway enforces auth.' }, openai_api_key: { label: 'OpenAI API key', - help: 'Forwarded as OPENAI_API_KEY. Stored encrypted at rest.' }, + help: 'Used instead of the key set in the server configuration. Stored encrypted at rest.' }, openai_model: { label: 'Model', help: 'Default model id. Per-agent overrides take precedence.' }, openai_base_url: { label: 'Base URL', help: 'Override for Azure OpenAI or self-hosted gateways. Leave blank for api.openai.com.' }, anthropic_api_key: { label: 'Anthropic API key', - help: 'Forwarded as ANTHROPIC_API_KEY. Stored encrypted at rest.' }, + help: 'Used instead of the key set in the server configuration. Stored encrypted at rest.' }, anthropic_model: { label: 'Model' }, google_api_key: { label: 'Google API key', - help: 'Forwarded as GOOGLE_GENERATIVE_AI_API_KEY. Stored encrypted at rest.' }, + help: 'Used instead of the key set in the server configuration. Stored encrypted at rest.' }, google_model: { label: 'Model' }, temperature: { label: 'Temperature', help: '0 = deterministic, 2 = highly creative.' }, @@ -338,7 +338,7 @@ export const en: PlatformTranslationData = { help: 'Hard cap on tokens generated per response.' }, request_timeout_ms: { label: 'Request timeout (ms)' }, trace_enabled: { label: 'Record traces', - help: 'Persist prompt/response traces to sys_ai_trace for debugging and replay.' }, + help: 'Keep a record of each AI request and its response, so a conversation can be inspected and replayed when something goes wrong.' }, log_prompts: { label: 'Log full prompts', help: 'Include rendered prompts (not just metadata) in trace rows.' }, embedder_provider: { @@ -365,7 +365,7 @@ export const en: PlatformTranslationData = { embedder_dimensions: { label: 'Dimensions', help: 'Override output dimensionality (Matryoshka models only). Leave blank to use the model default.' }, embedder_batch_size: { label: 'Batch size', - help: 'Chunks per embed() call. Reduce if hitting provider rate / size limits.' }, + help: 'Chunks sent to the embedding provider in one request. Reduce if hitting provider rate / size limits.' }, }, actions: { test: { label: 'Test connection' }, @@ -389,7 +389,7 @@ export const en: PlatformTranslationData = { ragflow: { title: 'RAGFlow', description: 'External RAGFlow deployment. See https://ragflow.io for self-host instructions.' }, indexing: { title: 'Indexing defaults', - description: 'Per-source values on KnowledgeSource.adapterConfig take precedence.' }, + description: 'A value set on a knowledge source itself wins over these defaults.' }, permissions: { title: 'Permissions' }, }, keys: { @@ -408,13 +408,13 @@ export const en: PlatformTranslationData = { ragflow_base_url: { label: 'Base URL', help: 'Example: http://localhost:9380' }, ragflow_api_key: { label: 'API key' }, ragflow_default_dataset: { label: 'Default dataset id', - help: 'Used when a KnowledgeSource does not specify its own RAGFlow dataset.' }, + help: 'Used when a knowledge source does not name its own RAGFlow dataset.' }, chunk_target: { label: 'Target chunk size (chars)', help: 'Soft cap on chunk size before token-aware splitting kicks in.' }, chunk_overlap: { label: 'Chunk overlap (chars)', help: 'Characters retained from the previous chunk so context survives the boundary.' }, over_fetch: { label: 'Over-fetch multiplier', - help: 'Internal topK × overFetch candidates fetched so JS-side metadata filtering still has rows.' }, + help: 'Search fetches this many times the requested number of results, so enough are left to return after they are filtered by document details.' }, enforce_rls: { label: 'Enforce RLS on search', help: 'Re-check every hit against the caller\'s record-level permissions.' }, }, diff --git a/packages/services/service-settings/src/translations/es-ES.ts b/packages/services/service-settings/src/translations/es-ES.ts index 00caaa9b1b6..a79c6112f66 100644 --- a/packages/services/service-settings/src/translations/es-ES.ts +++ b/packages/services/service-settings/src/translations/es-ES.ts @@ -417,7 +417,7 @@ export const esES: PlatformTranslationData = { provider: { title: 'Proveedor', description: 'Elige el backend de LLM. El modo Memory repite la entrada: útil para pruebas, nunca para producción.' }, gateway: { title: 'Vercel AI Gateway', - description: 'Enrutador multiproveedor. La especificación del modelo sigue `provider/model`, p. ej. `openai/gpt-4o`.' }, + description: 'Enrutador multiproveedor. El modelo se escribe provider/model, p. ej. openai/gpt-4o.' }, openai: { title: 'OpenAI' }, anthropic: { title: 'Anthropic' }, google: { title: 'Google' }, @@ -452,27 +452,27 @@ export const esES: PlatformTranslationData = { }, }, gateway_model: { label: 'Modelo de Gateway', - help: 'Se reenvía como AI_GATEWAY_MODEL. Ejemplo: openai/gpt-4o' }, + help: 'Se usa en lugar del modelo definido en la configuración del servidor. Ejemplo: openai/gpt-4o' }, gateway_api_key: { label: 'Clave de API de Gateway', help: 'Opcional: solo se requiere si el gateway exige autenticación.' }, openai_api_key: { label: 'Clave de API de OpenAI', - help: 'Se reenvía como OPENAI_API_KEY. Se almacena cifrada en reposo.' }, + help: 'Se usa en lugar de la clave definida en la configuración del servidor. Se almacena cifrada en reposo.' }, openai_model: { label: 'Modelo', help: 'ID de modelo predeterminado. Las anulaciones por agente tienen prioridad.' }, openai_base_url: { label: 'Base URL', help: 'Anulación para Azure OpenAI o gateways autoalojados. Déjalo en blanco para api.openai.com.' }, anthropic_api_key: { label: 'Clave de API de Anthropic', - help: 'Se reenvía como ANTHROPIC_API_KEY. Se almacena cifrada en reposo.' }, + help: 'Se usa en lugar de la clave definida en la configuración del servidor. Se almacena cifrada en reposo.' }, anthropic_model: { label: 'Modelo' }, google_api_key: { label: 'Clave de API de Google', - help: 'Se reenvía como GOOGLE_GENERATIVE_AI_API_KEY. Se almacena cifrada en reposo.' }, + help: 'Se usa en lugar de la clave definida en la configuración del servidor. Se almacena cifrada en reposo.' }, google_model: { label: 'Modelo' }, deepseek_api_key: { label: 'Clave de API de DeepSeek', help: 'sk-…, emitida en platform.deepseek.com.' }, deepseek_model: { label: 'Modelo', help: 'Ejemplos: deepseek-chat (V3), deepseek-reasoner (razonamiento R1).' }, dashscope_api_key: { label: 'Clave de API de DashScope', help: 'sk-…, emitida en dashscope.console.aliyun.com.' }, dashscope_model: { label: 'Modelo' }, cloudflare_account_id: { label: 'ID de cuenta de Cloudflare', help: 'El ID hexadecimal de 32 caracteres de la URL de tu panel de Cloudflare.' }, - cloudflare_gateway_id: { label: 'ID de gateway', help: 'Nombre del gateway configurado en Cloudflare → AI Gateway. Por defecto `default`.' }, + cloudflare_gateway_id: { label: 'ID de gateway', help: 'Nombre del gateway configurado en Cloudflare → AI Gateway. Por defecto, «default».' }, cloudflare_api_key: { label: 'Token de Cloudflare AI Gateway', help: 'Emitido en AI Gateway → pestaña «API tokens» (cfut_… o sk_…).' }, cloudflare_model: { label: 'Modelo' }, siliconflow_api_key: { label: 'Clave de API de SiliconFlow' }, @@ -487,7 +487,7 @@ export const esES: PlatformTranslationData = { help: 'Límite estricto de tokens generados por respuesta.' }, request_timeout_ms: { label: 'Tiempo de espera de la solicitud (ms)' }, trace_enabled: { label: 'Registrar trazas', - help: 'Persiste las trazas de prompt/respuesta en sys_ai_trace para depuración y reproducción.' }, + help: 'Guarda un registro de cada solicitud a la IA y de su respuesta, para poder revisar y reproducir una conversación cuando algo falla.' }, log_prompts: { label: 'Registrar prompts completos', help: 'Incluye los prompts renderizados (no solo metadatos) en las filas de traza.' }, embedder_provider: { @@ -514,7 +514,7 @@ export const esES: PlatformTranslationData = { embedder_dimensions: { label: 'Dimensiones', help: 'Anula la dimensionalidad de salida (solo modelos Matryoshka). Déjalo en blanco para usar el valor predeterminado del modelo.' }, embedder_batch_size: { label: 'Tamaño de lote', - help: 'Fragmentos por llamada a embed(). Redúcelo si alcanzas los límites de tasa o tamaño del proveedor.' }, + help: 'Fragmentos que se envían al proveedor de embeddings en cada solicitud. Redúcelo si alcanzas los límites de tasa o tamaño del proveedor.' }, }, actions: { test: { label: 'Probar conexión' }, @@ -538,7 +538,7 @@ export const esES: PlatformTranslationData = { ragflow: { title: 'RAGFlow', description: 'Despliegue externo de RAGFlow. Consulta https://ragflow.io para instrucciones de autoalojamiento.' }, indexing: { title: 'Valores predeterminados de indexación', - description: 'Los valores por fuente en KnowledgeSource.adapterConfig tienen prioridad.' }, + description: 'Un valor definido en la propia fuente de conocimiento prevalece sobre estos valores predeterminados.' }, permissions: { title: 'Permisos' }, }, keys: { @@ -557,13 +557,13 @@ export const esES: PlatformTranslationData = { ragflow_base_url: { label: 'Base URL', help: 'Ejemplo: http://localhost:9380' }, ragflow_api_key: { label: 'Clave de API' }, ragflow_default_dataset: { label: 'ID de dataset predeterminado', - help: 'Se usa cuando una KnowledgeSource no especifica su propio dataset de RAGFlow.' }, + help: 'Se usa cuando una fuente de conocimiento no indica su propio dataset de RAGFlow.' }, chunk_target: { label: 'Tamaño objetivo de fragmento (caracteres)', help: 'Límite flexible del tamaño de fragmento antes de que actúe la división consciente de tokens.' }, chunk_overlap: { label: 'Solapamiento de fragmentos (caracteres)', help: 'Caracteres conservados del fragmento anterior para que el contexto sobreviva al límite.' }, 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.' }, + help: 'La búsqueda obtiene este múltiplo del número de resultados pedido, para que queden suficientes tras filtrarlos por los datos del documento.' }, enforce_rls: { label: 'Aplicar RLS en la búsqueda', help: 'Vuelve a comprobar cada resultado contra los permisos a nivel de registro del solicitante.' }, }, @@ -577,5 +577,35 @@ export const esES: PlatformTranslationData = { text: 'Desactivarlo omite la salvaguarda exclusiva de la plataforma.' }, }, }, + + lifecycle: { + title: 'Ciclo de vida de los datos', + description: 'Cuánto tiempo se conservan los datos del sistema: sustituye las ventanas de retención, limita el número de filas por tabla y recibe una alerta cuando una tabla crece de forma inesperada. La plataforma los aplica en su barrido de limpieza periódico.', + keys: { + enabled: { + label: 'Aplicar las políticas de ciclo de vida', + help: 'Interruptor general del barrido periódico. Desactivado, las políticas de retención, TTL y rotación declaradas dejan de aplicarse (los datos vuelven a crecer sin límite).', + }, + retention_overrides: { + label: 'Sustituciones de retención', + help: 'Sustituciones de la ventana por objeto: { "": { "maxAge": "1y", "expireAfter": "30d" } }. Las duraciones usan h/d/w/y. ' + + 'Se define por organización, de modo que una organización regulada puede conservar años y una de desarrollo, días. ' + + 'Una sustitución más corta que el mínimo que necesita una función de la plataforma (por ejemplo, la ventana de detección de duplicados de la cola de trabajos) ' + + 'se ignora y se registra como error, y se sigue aplicando la ventana predeterminada.', + }, + quotas: { + label: 'Cuotas de filas', + help: 'Número máximo de filas por objeto: { "": 1000000 }. Superarlo genera una alerta de gobernanza: las cuotas nunca eliminan más allá de la política declarada.', + }, + quota_defaults: { + label: 'Cuotas predeterminadas por clase', + help: 'Cuotas de respaldo por clase de ciclo de vida: { "telemetry": 1000000, "transient": 500000, "event": 100000, "audit": 5000000 }.', + }, + growth_alert_rows: { + label: 'Umbral de alerta de crecimiento (filas por barrido)', + help: 'Avisa cuando una tabla con ciclo de vida declarado crece más de este número de filas entre dos barridos. 0 desactiva las alertas de crecimiento.', + }, + }, + }, }, }; diff --git a/packages/services/service-settings/src/translations/index.ts b/packages/services/service-settings/src/translations/index.ts index 433513984b9..9d4c61b2919 100644 --- a/packages/services/service-settings/src/translations/index.ts +++ b/packages/services/service-settings/src/translations/index.ts @@ -10,6 +10,11 @@ * Hosts merge `settingsBuiltinTranslations` into the i18next resource tree * under whatever namespace makes sense (the console wires it as `system`), * making keys resolvable as `.settings..{title,description,...}`. + * + * Besides the manifests bundled here, the locale files carry the copy for + * `lifecycle` (Data Lifecycle), the manifest `@objectstack/objectql` registers + * into the same hub: that package ships no translation bundle of its own. + * `settings-translation-coverage.test.ts` holds both to the same coverage. */ import type { PlatformTranslationBundle } from '@objectstack/spec/system'; diff --git a/packages/services/service-settings/src/translations/ja-JP.ts b/packages/services/service-settings/src/translations/ja-JP.ts index 4d3ec75110b..e4dbaec8cba 100644 --- a/packages/services/service-settings/src/translations/ja-JP.ts +++ b/packages/services/service-settings/src/translations/ja-JP.ts @@ -415,7 +415,7 @@ export const jaJP: PlatformTranslationData = { provider: { title: 'プロバイダー', description: 'LLM バックエンドを選択します。Memory モードは入力をそのまま返します。テスト用であり、本番では使用しないでください。' }, gateway: { title: 'Vercel AI Gateway', - description: 'マルチプロバイダールーター。モデル指定は `provider/model` 形式に従います(例: `openai/gpt-4o`)。' }, + description: 'マルチプロバイダールーター。モデルは provider/model の形式で指定します(例: openai/gpt-4o)。' }, openai: { title: 'OpenAI' }, anthropic: { title: 'Anthropic' }, google: { title: 'Google' }, @@ -450,27 +450,27 @@ export const jaJP: PlatformTranslationData = { }, }, gateway_model: { label: 'Gateway モデル', - help: 'AI_GATEWAY_MODEL として転送されます。例: openai/gpt-4o' }, + help: 'サーバー設定で指定したモデルの代わりに使われます。例: openai/gpt-4o' }, gateway_api_key: { label: 'Gateway API キー', help: '任意 — Gateway が認証を要求する場合のみ必要です。' }, openai_api_key: { label: 'OpenAI API キー', - help: 'OPENAI_API_KEY として転送されます。保存時に暗号化されます。' }, + help: 'サーバー設定で指定したキーの代わりに使われます。保存時に暗号化されます。' }, openai_model: { label: 'モデル', help: 'デフォルトのモデル ID。エージェント単位の上書きが優先されます。' }, openai_base_url: { label: 'Base URL', help: 'Azure OpenAI や自己ホスト型ゲートウェイ用の上書き。api.openai.com の場合は空欄にします。' }, anthropic_api_key: { label: 'Anthropic API キー', - help: 'ANTHROPIC_API_KEY として転送されます。保存時に暗号化されます。' }, + help: 'サーバー設定で指定したキーの代わりに使われます。保存時に暗号化されます。' }, anthropic_model: { label: 'モデル' }, google_api_key: { label: 'Google API キー', - help: 'GOOGLE_GENERATIVE_AI_API_KEY として転送されます。保存時に暗号化されます。' }, + help: 'サーバー設定で指定したキーの代わりに使われます。保存時に暗号化されます。' }, google_model: { label: 'モデル' }, deepseek_api_key: { label: 'DeepSeek API キー', help: 'sk-…。platform.deepseek.com で発行。' }, deepseek_model: { label: 'モデル', help: '例:deepseek-chat(V3)、deepseek-reasoner(R1 推論)。' }, dashscope_api_key: { label: 'DashScope API キー', help: 'sk-…。dashscope.console.aliyun.com で発行。' }, dashscope_model: { label: 'モデル' }, cloudflare_account_id: { label: 'Cloudflare アカウント ID', help: 'Cloudflare ダッシュボード URL に含まれる 32 桁の 16 進数 ID。' }, - cloudflare_gateway_id: { label: 'Gateway ID', help: 'Cloudflare → AI Gateway で設定したゲートウェイ名。既定は `default`。' }, + cloudflare_gateway_id: { label: 'Gateway ID', help: 'Cloudflare → AI Gateway で設定したゲートウェイ名。既定は「default」。' }, cloudflare_api_key: { label: 'Cloudflare AI Gateway トークン', help: 'AI Gateway →「API tokens」タブで発行(cfut_… または sk_…)。' }, cloudflare_model: { label: 'モデル' }, siliconflow_api_key: { label: 'SiliconFlow API キー' }, @@ -485,7 +485,7 @@ export const jaJP: PlatformTranslationData = { help: 'レスポンスごとに生成されるトークンの上限。' }, request_timeout_ms: { label: 'リクエストタイムアウト (ms)' }, trace_enabled: { label: 'トレースを記録', - help: 'デバッグと再生のため prompt/response トレースを sys_ai_trace に保存します。' }, + help: 'AI へのリクエストとその応答を記録し、問題が起きたときに会話を確認・再生できるようにします。' }, log_prompts: { label: '完全なプロンプトを記録', help: 'メタデータだけでなくレンダリングされたプロンプトをトレース行に含めます。' }, embedder_provider: { @@ -512,7 +512,7 @@ export const jaJP: PlatformTranslationData = { embedder_dimensions: { label: '次元数', help: '出力次元数を上書きします(Matryoshka モデルのみ)。空欄の場合はモデルのデフォルトを使用します。' }, embedder_batch_size: { label: 'バッチサイズ', - help: 'embed() 呼び出しごとのチャンク数。プロバイダーのレート/サイズ制限に達する場合は減らします。' }, + help: '埋め込みプロバイダーに 1 回のリクエストで送るチャンク数。プロバイダーのレート/サイズ制限に達する場合は減らします。' }, }, actions: { test: { label: '接続テスト' }, @@ -536,7 +536,7 @@ export const jaJP: PlatformTranslationData = { ragflow: { title: 'RAGFlow', description: '外部 RAGFlow デプロイ。セルフホストの手順は https://ragflow.io を参照してください。' }, indexing: { title: 'インデックスのデフォルト値', - description: 'KnowledgeSource.adapterConfig のソース単位の値が優先されます。' }, + description: 'ナレッジソース自体に設定した値が、これらの既定値より優先されます。' }, permissions: { title: '権限' }, }, keys: { @@ -555,13 +555,13 @@ export const jaJP: PlatformTranslationData = { ragflow_base_url: { label: 'Base URL', help: '例: http://localhost:9380' }, ragflow_api_key: { label: 'API キー' }, ragflow_default_dataset: { label: 'デフォルトデータセット ID', - help: 'KnowledgeSource が独自の RAGFlow データセットを指定しない場合に使用されます。' }, + help: 'ナレッジソースが独自の RAGFlow データセットを指定しない場合に使用されます。' }, chunk_target: { label: '目標チャンクサイズ(文字数)', help: 'トークン単位の分割が行われる前のチャンクサイズのソフト上限。' }, chunk_overlap: { label: 'チャンクの重なり(文字数)', help: '境界を越えてコンテキストを維持するため、前のチャンクから保持する文字数。' }, over_fetch: { label: 'オーバーフェッチ倍率', - help: 'JS 側のメタデータフィルタリングでも行が残るよう、内部で topK × overFetch 件の候補を取得します。' }, + help: '検索では要求された件数のこの倍数の結果を取得し、ドキュメント情報で絞り込んだ後も十分な件数が残るようにします。' }, enforce_rls: { label: '検索時に RLS を強制', help: '各ヒットを呼び出し元のレコードレベル権限に対して再確認します。' }, }, @@ -575,5 +575,34 @@ export const jaJP: PlatformTranslationData = { text: '無効化するとプラットフォーム固有のセーフガードがスキップされます。' }, }, }, + + lifecycle: { + title: 'データライフサイクル', + description: 'システムデータを保持する期間:保持期間の上書き、テーブルごとの行数上限、テーブルが想定外に増えたときのアラート。プラットフォームは定期クリーンアップの実行時にこれらを適用します。', + keys: { + enabled: { + label: 'ライフサイクルポリシーを適用', + help: '定期クリーンアップのマスタースイッチ。オフにすると、宣言済みの保持期間・TTL・ローテーションのポリシーが適用されなくなります(データは再び無制限に増えます)。', + }, + retention_overrides: { + label: '保持期間の上書き', + help: 'オブジェクトごとの保持ウィンドウの上書き:{ "": { "maxAge": "1y", "expireAfter": "30d" } }。期間は h/d/w/y で指定します。' + + '組織ごとに設定するため、規制対象の組織は数年、開発用の組織は数日だけ保持できます。' + + 'プラットフォーム機能が必要とする最小期間(例:ジョブキューの重複検出ウィンドウ)より短い上書きは無視されてエラーとして記録され、既定のウィンドウが引き続き適用されます。', + }, + quotas: { + label: '行数クォータ', + help: 'オブジェクトごとの最大行数:{ "": 1000000 }。超過するとガバナンスアラートが発生します。クォータが宣言済みのポリシーを超えて削除することはありません。', + }, + quota_defaults: { + label: 'クラス別の既定クォータ', + help: 'ライフサイクルクラスごとのフォールバッククォータ:{ "telemetry": 1000000, "transient": 500000, "event": 100000, "audit": 5000000 }。', + }, + growth_alert_rows: { + label: '増加アラートのしきい値(クリーンアップ 1 回あたりの行数)', + help: 'ライフサイクルを宣言したテーブルが 2 回のクリーンアップの間にこの行数を超えて増えたときにアラートを出します。0 で増加アラートを無効にします。', + }, + }, + }, }, }; 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 6a14ccbcd31..79c87c8f49e 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 @@ -22,12 +22,13 @@ import { describe, it, expect } from 'vitest'; import type { SettingsManifest } from '@objectstack/spec/system'; +import { lifecycleSettingsManifest } from '@objectstack/objectql'; import * as manifestsModule from '../manifests/index.js'; import { en, zhCN, jaJP, esES } from './index.js'; // The manifests barrel also exports action handlers and the aggregate array; // keep only the manifest objects. -const manifests = Object.values(manifestsModule).filter( +const bundledManifests = Object.values(manifestsModule).filter( (v): v is SettingsManifest => !!v && typeof v === 'object' && @@ -35,6 +36,19 @@ const manifests = Object.values(manifestsModule).filter( Array.isArray((v as SettingsManifest).specifiers), ); +/** + * Platform manifests another package registers into the same settings hub, + * whose copy this bundle carries because that package ships no translation + * bundle of its own. `@objectstack/objectql` registers `lifecycle` (Data + * Lifecycle) at `kernel:ready`. Each one listed here is held to the same full + * coverage as a bundled manifest, and its namespace is a declared one for the + * no-undeclared-copy check below. + */ +const hostedManifests: SettingsManifest[] = [lifecycleSettingsManifest]; + +/** Every manifest whose `settings..…` copy lives in this bundle. */ +const manifests = [...bundledManifests, ...hostedManifests]; + const LOCALES: Array<[string, { settings?: Record }]> = [ ['zh-CN', zhCN], ['ja-JP', jaJP], @@ -112,7 +126,13 @@ function undeclaredIn(data: { settings?: Record }): string[] { describe('settings translation coverage', () => { it('covers every built-in settings manifest', () => { - expect(manifests.length).toBeGreaterThanOrEqual(10); + expect(bundledManifests.length).toBeGreaterThanOrEqual(10); + }); + + it('covers the hosted platform manifests by namespace', () => { + expect(manifests.map((m) => m.namespace)).toContain('lifecycle'); + const namespaces = manifests.map((m) => m.namespace); + expect(new Set(namespaces).size, 'a namespace is declared twice').toBe(namespaces.length); }); for (const [locale, data] of LOCALES) { diff --git a/packages/services/service-settings/src/translations/zh-CN.ts b/packages/services/service-settings/src/translations/zh-CN.ts index 1248369196e..795b99e37d4 100644 --- a/packages/services/service-settings/src/translations/zh-CN.ts +++ b/packages/services/service-settings/src/translations/zh-CN.ts @@ -474,7 +474,7 @@ export const zhCN: PlatformTranslationData = { description: '平台 AI 与知识库服务使用的 LLM 提供商、模型、凭据与向量化配置。', groups: { provider: { title: '提供商', description: '选择 LLM 后端。Memory 模式仅原样回显输入,仅用于测试,严禁用于生产。' }, - gateway: { title: 'Vercel AI Gateway', description: '多提供商路由器。模型规格遵循 `provider/model` 格式,例如 `openai/gpt-4o`。' }, + gateway: { title: 'Vercel AI Gateway', description: '多提供商路由器。模型写作 provider/model,例如 openai/gpt-4o。' }, openai: { title: 'OpenAI' }, anthropic: { title: 'Anthropic' }, google: { title: 'Google' }, @@ -504,21 +504,21 @@ export const zhCN: PlatformTranslationData = { openrouter: 'OpenRouter(OpenAI 兼容)', }, }, - gateway_model: { label: 'Gateway 模型', help: '作为 AI_GATEWAY_MODEL 转发。示例:openai/gpt-4o' }, + gateway_model: { label: 'Gateway 模型', help: '代替服务器配置中设定的模型。示例:openai/gpt-4o' }, gateway_api_key: { label: 'Gateway API Key', help: '可选 —— 仅当 Gateway 强制鉴权时需要。' }, - openai_api_key: { label: 'OpenAI API Key', help: '作为 OPENAI_API_KEY 转发,加密存储。' }, + openai_api_key: { label: 'OpenAI API Key', help: '代替服务器配置中设定的密钥,加密存储。' }, openai_model: { label: '模型', help: '默认模型 ID。Agent 级覆盖优先生效。' }, openai_base_url: { label: 'Base URL', help: '用于 Azure OpenAI 或自建网关。留空走 api.openai.com。' }, - anthropic_api_key: { label: 'Anthropic API Key', help: '作为 ANTHROPIC_API_KEY 转发,加密存储。' }, + anthropic_api_key: { label: 'Anthropic API Key', help: '代替服务器配置中设定的密钥,加密存储。' }, anthropic_model: { label: '模型' }, - google_api_key: { label: 'Google API Key', help: '作为 GOOGLE_GENERATIVE_AI_API_KEY 转发,加密存储。' }, + google_api_key: { label: 'Google API Key', help: '代替服务器配置中设定的密钥,加密存储。' }, google_model: { label: '模型' }, deepseek_api_key: { label: 'DeepSeek API Key', help: 'sk-…,在 platform.deepseek.com 签发。' }, - deepseek_model: { label: '模型', help: '示例:deepseek-chat(V3)、deepseek-reasoner(R1 推理)。' }, + deepseek_model: { label: '模型', help: '示例:deepseek-chat (V3)、deepseek-reasoner (R1 推理)。' }, dashscope_api_key: { label: 'DashScope API Key', help: 'sk-…,在 dashscope.console.aliyun.com 签发。' }, dashscope_model: { label: '模型' }, cloudflare_account_id: { label: 'Cloudflare Account ID', help: '来自 Cloudflare 控制台 URL 的 32 位十六进制 ID。' }, - cloudflare_gateway_id: { label: 'Gateway ID', help: '在 Cloudflare → AI Gateway 中配置的网关名称,默认为 `default`。' }, + cloudflare_gateway_id: { label: 'Gateway ID', help: '在 Cloudflare → AI Gateway 中配置的网关名称,默认为「default」。' }, cloudflare_api_key: { label: 'Cloudflare AI Gateway Token', help: '在 AI Gateway →「API tokens」标签页签发(cfut_… 或 sk_…)。' }, cloudflare_model: { label: '模型' }, siliconflow_api_key: { label: 'SiliconFlow API Key' }, @@ -530,7 +530,7 @@ export const zhCN: PlatformTranslationData = { temperature: { label: '温度', help: '0 = 确定性,2 = 高度发散。' }, max_tokens: { label: '最大输出 tokens', help: '单次响应生成的硬上限。' }, request_timeout_ms: { label: '请求超时(毫秒)' }, - trace_enabled: { label: '记录 Trace', help: '将 prompt/response 落入 sys_ai_trace,便于调试与回放。' }, + trace_enabled: { label: '记录 Trace', help: '记录每次 AI 请求及其响应,出现问题时可以查看并回放对话。' }, log_prompts: { label: '记录完整 Prompt', help: '在 trace 行中包含完整 prompt 而非仅元数据。' }, embedder_provider: { label: '提供商', @@ -551,7 +551,7 @@ export const zhCN: PlatformTranslationData = { embedder_model: { label: '模型', help: '示例 — OpenAI: text-embedding-3-small · 阿里通义: text-embedding-v3 · 智谱: embedding-3 · 硅基流动: BAAI/bge-m3 · Ollama: bge-m3' }, embedder_base_url: { label: 'Base URL', help: '端点根路径(不含 /embeddings)。预设会自动填充,可覆盖为代理或自建网关。' }, embedder_dimensions: { label: '维度', help: '覆盖输出维度(仅 Matryoshka 模型支持)。留空使用模型默认值。' }, - embedder_batch_size: { label: '批量大小', help: '单次 embed() 调用的 chunk 数。命中速率/大小限制时调小。' }, + embedder_batch_size: { label: '批量大小', help: '每次请求发送给嵌入服务商的 chunk 数。命中速率/大小限制时调小。' }, }, actions: { test: { label: '测试连接' }, @@ -570,7 +570,7 @@ export const zhCN: PlatformTranslationData = { adapter: { title: '后端', description: '选择文档分块及其向量的存储位置。' }, turso: { title: 'Turso / libSQL', description: '支持托管 Turso、本地文件、内存三种模式。' }, ragflow: { title: 'RAGFlow', description: '外部 RAGFlow 部署。自部署文档见 https://ragflow.io 。' }, - indexing: { title: '索引默认值', description: 'KnowledgeSource.adapterConfig 上的逐源覆盖优先生效。' }, + indexing: { title: '索引默认值', description: '在知识源上单独设定的值优先于这些默认值。' }, permissions: { title: '权限' }, }, keys: { @@ -586,11 +586,11 @@ export const zhCN: PlatformTranslationData = { turso_auth_token: { label: 'Auth Token', help: '仅托管 Turso URL 需要。' }, ragflow_base_url: { label: 'Base URL', help: '示例:http://localhost:9380' }, ragflow_api_key: { label: 'API Key' }, - ragflow_default_dataset: { label: '默认 Dataset ID', help: 'KnowledgeSource 未指定时使用。' }, + ragflow_default_dataset: { label: '默认 Dataset ID', help: '知识源未指定自己的 RAGFlow 数据集时使用。' }, 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 再次校验调用方的行级权限。' }, + over_fetch: { label: '过取倍数', help: '检索时按请求结果数的这一倍数取回候选,以便按文档信息过滤后仍有足够结果可返回。' }, + enforce_rls: { label: '搜索时强制 RLS', help: '对每条命中再次校验调用方的行级权限。' }, }, actions: { test: { label: '测试连接' }, @@ -600,5 +600,34 @@ export const zhCN: PlatformTranslationData = { enforce_rls_warning: { title: '生产环境请保持开启', text: '关闭将跳过平台对向量存储数据外泄的独有防护。' }, }, }, + + lifecycle: { + title: '数据生命周期', + description: '系统数据保留多久:覆盖保留期、为每张表设定行数上限,并在表意外增长时发出告警。平台在周期性清理时应用这些设置。', + keys: { + enabled: { + label: '执行生命周期策略', + help: '周期性清理的总开关。关闭后,已声明的保留期、TTL 与轮转策略不再执行(数据重新无限增长)。', + }, + retention_overrides: { + label: '保留期覆盖', + help: '按对象覆盖保留窗口:{ "": { "maxAge": "1y", "expireAfter": "30d" } }。时长单位为 h/d/w/y。' + + '按组织设置,因此受监管的组织可以保留数年,开发用的组织只保留数天。' + + '短于某项平台功能所需最短窗口(例如作业队列的重复检测窗口)的覆盖会被忽略并记为错误,继续沿用默认窗口。', + }, + quotas: { + label: '行数配额', + help: '按对象的最大行数:{ "": 1000000 }。超出时触发治理告警——配额绝不会删除已声明策略之外的数据。', + }, + quota_defaults: { + label: '按类别的默认配额', + help: '按生命周期类别的兜底配额:{ "telemetry": 1000000, "transient": 500000, "event": 100000, "audit": 5000000 }。', + }, + growth_alert_rows: { + label: '增长告警阈值(每次清理的行数)', + help: '声明了生命周期的表在两次清理之间增长超过此行数时发出告警。0 表示关闭增长告警。', + }, + }, + }, }, };