feat(web): 移除首启内测声明,遥测保持默认关闭

This commit is contained in:
creatixchu
2026-08-13 13:47:09 +08:00
parent dc3fc72d57
commit 5061ec2ef0
35 changed files with 122 additions and 978 deletions

View File

@@ -1,155 +0,0 @@
.page {
position: relative;
z-index: 1;
width: min(640px, calc(100vw - 64px));
max-height: 100vh;
padding: clamp(64px, 9vh, 104px) 0 40px;
box-sizing: border-box;
overflow-y: auto;
color: var(--dsw-alias-label-primary);
--welcome-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
}
.brand {
display: flex;
align-items: center;
margin-bottom: 42px;
color: var(--dsw-alias-label-primary);
}
.title {
margin: 0;
font-size: 28px;
line-height: 36px;
font-weight: 600;
letter-spacing: -0.02em;
outline: none;
}
.opening,
.reflection,
.feedback,
.error {
margin: 0;
}
.opening {
margin-top: 30px;
}
.reflection {
margin-top: 36px;
padding: 0;
}
.feedback {
margin-top: 30px;
}
.opening,
.reflection,
.feedback {
font-size: 16px;
line-height: 28px;
color: var(--dsw-alias-label-secondary);
}
.feedback strong {
color: inherit;
font-weight: 500;
}
.footer {
display: flex;
justify-content: flex-end;
margin-top: 32px;
}
.error {
margin-top: 20px;
font-size: 14px;
line-height: 22px;
color: var(--dsw-alias-state-error-primary);
}
.primary {
min-width: 120px;
transition: transform 140ms var(--welcome-ease-out);
}
.primary:active:not(:disabled) {
transform: scale(0.97);
}
.brand,
.title,
.opening,
.reflection,
.feedback,
.footer {
animation: welcome-enter 280ms var(--welcome-ease-out) both;
}
.title { animation-delay: 40ms; }
.opening { animation-delay: 80ms; }
.reflection { animation-delay: 120ms; }
.feedback { animation-delay: 160ms; }
.footer { animation-delay: 200ms; }
@keyframes welcome-enter {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.brand,
.title,
.opening,
.reflection,
.feedback,
.footer {
animation: none;
}
.primary {
transition: none;
}
}
@media (max-width: 560px) {
.page {
width: calc(100vw - 40px);
padding-top: 38px;
}
.brand {
margin-bottom: 30px;
}
.opening {
margin-top: 24px;
}
.reflection {
margin-top: 28px;
}
.feedback {
margin-top: 28px;
}
.footer {
margin-top: 30px;
}
.primary {
width: 100%;
}
}

View File

@@ -1,91 +0,0 @@
/** Product-wide, versioned first-run welcome step. */
import { useCallback, useEffect, useRef } from 'react'
import type { ReactNode } from 'react'
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import { BrandWordmark, Button, OnboardingSurface } from '@deepseek-ai/dsh-client-ui-primitives'
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-web-react'
import type { WelcomeNoticeState, WelcomeNoticeStore } from './welcome-store.ts'
import css from './WelcomeNotice.module.css'
function emphasizedFeedback(paragraph: string, emphasis: string): ReactNode {
const index = paragraph.indexOf(emphasis)
/* v8 ignore next -- both locale values derive from one owner object that contains the emphasis */
if (index < 0) return paragraph
return (
<>
{paragraph.slice(0, index)}
<strong>{emphasis}</strong>
{paragraph.slice(index + emphasis.length)}
</>
)
}
/** Registrant-owned dependencies of {@link WelcomeNotice}. */
export interface WelcomeNoticeInjected {
controller: WelcomeNoticeStore
useSnapshot: SnapshotSelectorHook<WelcomeNoticeState>
}
/** Coordinator owner props plus the welcome step's injected face. */
export type WelcomeNoticeProps =
PropsRuntime<'settings.onboarding'> & PropsLocale<'settings'> & WelcomeNoticeInjected
/** Render the mandatory notice until its current version is acknowledged. */
export function WelcomeNotice(props: WelcomeNoticeProps): ReactNode {
const { complete, controller, useSnapshot, t } = props
const state = useSnapshot(snapshot => snapshot)
const finished = useRef(false)
const titleRef = useRef<HTMLHeadingElement | null>(null)
const finish = useCallback((): void => {
if (finished.current) return
finished.current = true
complete()
}, [complete])
useEffect(() => {
if (state.status === 'idle') void controller.load()
}, [controller, state.status])
useEffect(() => {
if (state.acknowledged) finish()
}, [finish, state.acknowledged])
useEffect(() => {
if (state.status === 'ready' && !state.acknowledged) titleRef.current?.focus()
}, [state.acknowledged, state.status])
// Null while the acknowledgement fact is still loading (or already given):
// the takeover chrome below is part of THIS render, so deciding not to
// show paints and blocks nothing.
if (state.status === 'idle' || state.status === 'loading' || state.acknowledged) return null
const acknowledge = async (): Promise<void> => {
if (await controller.acknowledge()) finish()
}
return (
<OnboardingSurface>
<section className={css.page} role="region" aria-labelledby="welcome-notice-title">
<div className={css.brand} aria-hidden="true"><BrandWordmark size={24} /></div>
<h2 ref={titleRef} id="welcome-notice-title" className={css.title} tabIndex={-1}>{t('welcome.title')}</h2>
<p className={css.opening}>{t('welcome.paragraph.0')}</p>
<blockquote className={css.reflection}>{t('welcome.paragraph.1')}</blockquote>
<p className={css.feedback}>
{emphasizedFeedback(t('welcome.paragraph.2'), t('welcome.feedbackEmphasis'))}
</p>
{state.error === null ? null : <p className={css.error} role="alert">{t('welcome.error')}</p>}
<div className={css.footer}>
<Button
variant="primary"
className={css.primary}
disabled={state.status === 'saving'}
onClick={() => { void acknowledge() }}
>
{t('welcome.continue')}
</Button>
</div>
</section>
</OnboardingSurface>
)
}

View File

@@ -17,9 +17,6 @@ import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
// Type-only: pulls ctx.locale into this program.
import type {} from '@deepseek-ai/dsh-client-locale/client'
// Type-only: pulls the ctx.remote merge and the forwarded-event key face
// (the settings invalidation rides the allowlist) into this program.
import type {} from '@deepseek-ai/dsh-api-remotes/client'
import type {
SettingsOnboardingStep, SettingsRootInjected, SettingsSectionRow,
} from './shell-contract.ts'
@@ -29,10 +26,6 @@ import { GeneralSection } from './GeneralSection.tsx'
import { SettingsDocumentAction } from './SettingsDocumentAction.tsx'
import type { SettingsDocumentActionInjected } from './SettingsDocumentAction.tsx'
import { refreshDocumentIfLoaded, SettingsDocumentStore } from './settings-document-store.ts'
import type { WelcomeNoticeInjected } from './WelcomeNotice.tsx'
import { WelcomeNotice } from './WelcomeNotice.tsx'
import { refreshWelcomeIfLoaded, WelcomeNoticeStore } from './welcome-store.ts'
import { WELCOME_NOTICE_SETTINGS_NAMESPACE } from '../onboarding-copy.ts'
import { en, zh, type SettingsKey } from './locales.ts'
export type {
@@ -44,8 +37,6 @@ export type {
export type { SettingsDocumentActionInjected, SettingsDocumentActionProps } from './SettingsDocumentAction.tsx'
export type { SettingsDocumentState } from './settings-document-store.ts'
export { SettingsDocumentStore } from './settings-document-store.ts'
export type { WelcomeNoticeInjected, WelcomeNoticeProps } from './WelcomeNotice.tsx'
export type { WelcomeNoticeState } from './welcome-store.ts'
export type { SettingsKey } from './locales.ts'
declare module '@deepseek-ai/dsh-client-ui-slots' {
@@ -63,7 +54,7 @@ const NS = 'settings'
* ui-settings' apply, whose activation order relative to this one is NOT
* constrained; registrations depend on their slots through `slots.inject()`.
*/
export const inject = ['slots', 'locale', 'connection', 'remote']
export const inject = ['slots', 'locale', 'connection']
/**
* Register the `settings` dictionaries, the chrome content, and the General
@@ -87,27 +78,9 @@ export function apply(ctx: ClientContext): void {
const useSnapshot = bindSnapshotSelector(documentController.store)
return (): SettingsDocumentActionInjected => ({ controller: documentController, useSnapshot })
})()
const welcomeController = new WelcomeNoticeStore(connection.api, connection.isLoopback ? 'host' : 'memory')
const useWelcomeSnapshot = bindSnapshotSelector(welcomeController.store)
const welcomeInjected = (): WelcomeNoticeInjected => ({
controller: welcomeController,
useSnapshot: useWelcomeSnapshot,
})
ctx.effect(() => {
const refresh = (): void => { refreshWelcomeIfLoaded(welcomeController) }
const disposers = [
ctx.remote.$on('settings/document-updated', (ns) => {
if (ns !== WELCOME_NOTICE_SETTINGS_NAMESPACE) return
refresh()
}),
ctx.on('connection/reset', () => {
refresh()
refreshDocumentIfLoaded(documentController)
}),
]
return () => { for (const dispose of disposers) dispose() }
}, 'ui-settings-general: metadata invalidations')
ctx.effect(() => ctx.on('connection/reset', () => {
refreshDocumentIfLoaded(documentController)
}), 'ui-settings-general: metadata invalidations')
// The settings shell: this package occupies the sidebar-owned hole and
// declares the settings slots. Ledger → nav-row projection as an observable
// source (uSES contract: getSnapshot returns the cached rows until the
@@ -202,11 +175,4 @@ export function apply(ctx: ClientContext): void {
locale: NS,
children: { 'settings.general.item': { kind: 'list', scope: 'root' } },
}, GeneralSection))
ctx.slots.inject('settings.onboarding', () => ctx.slots.register({
name: 'settings.onboarding',
id: 'welcome-notice',
order: -100,
locale: NS,
inject: welcomeInjected,
}, WelcomeNotice))
}

View File

@@ -1,5 +1,4 @@
/** Shell chrome, General-nav, and welcome-notice dictionaries; feature rows own their copy. */
import { WELCOME_NOTICE_COPY } from '../onboarding-copy.ts'
/** Shell chrome and General-nav dictionaries; feature rows own their copy. */
/** Simplified Chinese dictionary (the key-set source of truth). */
export const zh = {
@@ -9,13 +8,6 @@ export const zh = {
'openDocument': '打开配置文件',
'openDocument.error': '无法打开配置文件',
'general.nav': '通用设置',
'welcome.title': WELCOME_NOTICE_COPY.zh.title,
'welcome.paragraph.0': WELCOME_NOTICE_COPY.zh.paragraphs[0],
'welcome.paragraph.1': WELCOME_NOTICE_COPY.zh.paragraphs[1],
'welcome.paragraph.2': WELCOME_NOTICE_COPY.zh.paragraphs[2],
'welcome.feedbackEmphasis': WELCOME_NOTICE_COPY.zh.feedbackEmphasis,
'welcome.continue': WELCOME_NOTICE_COPY.zh.continueLabel,
'welcome.error': '暂时无法保存确认状态,请重试。',
} satisfies Record<string, string>
/** The settings namespace key union. */
@@ -29,11 +21,4 @@ export const en = {
'openDocument': 'Open configuration file',
'openDocument.error': 'Could not open configuration file',
'general.nav': 'General',
'welcome.title': WELCOME_NOTICE_COPY.en.title,
'welcome.paragraph.0': WELCOME_NOTICE_COPY.en.paragraphs[0],
'welcome.paragraph.1': WELCOME_NOTICE_COPY.en.paragraphs[1],
'welcome.paragraph.2': WELCOME_NOTICE_COPY.en.paragraphs[2],
'welcome.feedbackEmphasis': WELCOME_NOTICE_COPY.en.feedbackEmphasis,
'welcome.continue': WELCOME_NOTICE_COPY.en.continueLabel,
'welcome.error': 'The acknowledgement could not be saved. Please try again.',
} satisfies Record<SettingsKey, string>

View File

@@ -1,128 +0,0 @@
/** Welcome-notice state, durable when the browser may use Host settings. */
import type { IApiClient, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
import {
WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_SETTINGS_NAMESPACE, WELCOME_NOTICE_VERSION,
} from '../onboarding-copy.ts'
/** State rendered by the welcome step. */
export interface WelcomeNoticeState {
status: 'idle' | 'loading' | 'ready' | 'saving' | 'error'
acknowledged: boolean
error: string | null
}
function messageOf(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}
function acknowledgementOf(view: SettingsNamespaceView): string | undefined {
if (typeof view.value !== 'object' || view.value === null) return undefined
const value = (view.value as Record<string, unknown>)[WELCOME_NOTICE_ACK_FIELD]
return typeof value === 'string' ? value : undefined
}
/** Coordinates durable Host acknowledgement or a process-local remote fallback. */
export class WelcomeNoticeStore {
/** uSES-safe state source shared by the registered welcome step. */
readonly store: SnapshotStore<WelcomeNoticeState> = createSnapshotStore({
status: 'idle', acknowledged: false, error: null,
})
private generation = 0
/**
* @param api - settings wire face used for durable reads and writes.
* @param persistence - remote browsers use memory because settings is loopback-only.
*/
constructor(
private readonly api: Pick<IApiClient, 'settings'>,
private readonly persistence: 'host' | 'memory' = 'host',
) {}
/** Load the acknowledgement from Host settings or initialize process-local state. */
async load(): Promise<void> {
const generation = ++this.generation
if (this.persistence === 'memory') {
this.store.update((state) => { state.status = 'ready'; state.error = null })
return
}
this.store.update((state) => { state.status = 'loading'; state.error = null })
try {
const response = await this.api.settings.describe({})
if (!response.result.ok) throw new Error(response.result.error.message)
const view = response.result.value.namespaces.find(
candidate => candidate.ns === WELCOME_NOTICE_SETTINGS_NAMESPACE,
)
if (view === undefined) throw new Error('welcome acknowledgement settings are unavailable')
if (generation !== this.generation) return
this.store.update((state) => {
state.status = 'ready'
state.acknowledged = acknowledgementOf(view) === WELCOME_NOTICE_VERSION
state.error = null
})
} catch (error) {
if (generation !== this.generation) return
this.store.update((state) => {
state.status = 'error'
state.acknowledged = false
state.error = messageOf(error)
})
}
}
/**
* Acknowledge this copy version. The Host path mutation is idempotent across
* tabs and preserves sibling settings; remote fallback changes only this store.
* @returns true when the selected persistence mode accepted the acknowledgement.
*/
async acknowledge(): Promise<boolean> {
const generation = ++this.generation
if (this.persistence === 'memory') {
this.store.update((state) => {
state.status = 'ready'
state.acknowledged = true
state.error = null
})
return true
}
this.store.update((state) => { state.status = 'saving'; state.error = null })
try {
const response = await this.api.settings.mutate({
ns: WELCOME_NOTICE_SETTINGS_NAMESPACE,
ops: [{ op: 'set', path: [WELCOME_NOTICE_ACK_FIELD], value: WELCOME_NOTICE_VERSION }],
})
if (!response.result.ok) throw new Error(response.result.error.message)
if (generation === this.generation) {
this.store.update((state) => {
state.status = 'ready'
state.acknowledged = true
state.error = null
})
}
return true
} catch (error) {
if (generation === this.generation) {
this.store.update((state) => {
state.status = 'error'
state.acknowledged = false
state.error = messageOf(error)
})
}
return false
}
}
}
/**
* Refresh only after welcome state has left idle. A memory-mode load retains
* acknowledgement so reconnect and settings-change refreshes do not reopen a
* process-local notice.
* @param controller - welcome state owner whose current status decides whether to load.
*/
export function refreshWelcomeIfLoaded(controller: WelcomeNoticeStore): void {
if (controller.store.getSnapshot().status === 'idle') return
void controller.load()
}

View File

@@ -3,28 +3,24 @@
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
import {
WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_SETTINGS_NAMESPACE,
} from './onboarding-copy.ts'
export {
WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_COPY, WELCOME_NOTICE_SETTINGS_NAMESPACE,
WELCOME_NOTICE_VERSION,
} from './onboarding-copy.ts'
/** Durable settings namespace for product-wide GUI onboarding facts. */
const ONBOARDING_SETTINGS_NAMESPACE = 'ui-onboarding'
interface OnboardingSettings {
/** Last welcome-notice version acknowledged by a since-removed first-run notice; kept so stored documents stay valid. */
welcomeNoticeVersion?: string
}
const OnboardingSettingsSchema: z<OnboardingSettings> = z.object({
[WELCOME_NOTICE_ACK_FIELD]: z.string(),
welcomeNoticeVersion: z.string(),
})
/** Register the durable GUI-onboarding section when a settings provider exists. */
export function apply(ctx: Context): void {
ctx.inject(['settings'], (settingsCtx) => {
settingsCtx.settings.register(
settingsNamespace(WELCOME_NOTICE_SETTINGS_NAMESPACE),
settingsNamespace(ONBOARDING_SETTINGS_NAMESPACE),
OnboardingSettingsSchema,
)
})

View File

@@ -16,7 +16,7 @@ export const inject = ['invariants']
/**
* No runtime invariant: the settings seam validates and publishes the durable
* welcome section, while slot conflicts fail loud in the slot core. The local
* onboarding section, while slot conflicts fail loud in the slot core. The local
* document action is browser state over typed RPC responses and is covered by
* store/component tests rather than a Cordis runtime relationship.
*/

View File

@@ -1,35 +0,0 @@
/** Durable settings namespace for product-wide GUI onboarding facts. */
export const WELCOME_NOTICE_SETTINGS_NAMESPACE = 'ui-onboarding'
/** Field storing the last welcome notice version the user acknowledged. */
export const WELCOME_NOTICE_ACK_FIELD = 'welcomeNoticeVersion'
/**
* Bump only when the notice changes materially and every user should see it
* again. The acknowledgement is compared for exact equality.
*/
export const WELCOME_NOTICE_VERSION = '2026-08-11.1'
/** The complete editable welcome notice in both supported GUI locales. */
export const WELCOME_NOTICE_COPY = {
zh: {
title: '内测声明',
paragraphs: [
'感谢您愿意拨冗试用 DeepSeek Harness。当前版本仍处于内部测试阶段功能仍待完善体验难免有些粗糙。',
'“如切如磋,如琢如磨。” 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中发现的问题,也可能促使我们重新审视,甚至推翻已有的设计。',
'内测版本默认不会上传 Session Log。如需在提交反馈时共享会话日志可以设置环境变量 DSH_TELEMETRY_MODE=FEEDBACK_ONLY如需持续上传可以设置 DSH_TELEMETRY_MODE=FULL但该模式同时会启用 dsh-sdk 命令遥测,上报匿名 ID、命令结果以及脱敏后的项目配置。另外如果您有任何反馈与建议请在企业微信群中留言告诉我们。每一条反馈都会帮助我们把它打磨得更好。',
],
feedbackEmphasis: '如果您有任何反馈与建议,请在企业微信群中留言告诉我们',
continueLabel: '继续',
},
en: {
title: '内测声明',
paragraphs: [
'感谢您愿意拨冗试用 DeepSeek Harness。当前版本仍处于内部测试阶段功能仍待完善体验难免有些粗糙。',
'“如切如磋,如琢如磨。” 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中发现的问题,也可能促使我们重新审视,甚至推翻已有的设计。',
'内测版本默认不会上传 Session Log。如需在提交反馈时共享会话日志可以设置环境变量 DSH_TELEMETRY_MODE=FEEDBACK_ONLY如需持续上传可以设置 DSH_TELEMETRY_MODE=FULL但该模式同时会启用 dsh-sdk 命令遥测,上报匿名 ID、命令结果以及脱敏后的项目配置。另外如果您有任何反馈与建议请在企业微信群中留言告诉我们。每一条反馈都会帮助我们把它打磨得更好。',
],
feedbackEmphasis: '如果您有任何反馈与建议,请在企业微信群中留言告诉我们',
continueLabel: '继续',
},
} as const