dsh-smooth-stream 0.1.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +103 -0
- package/README.md +62 -47
- package/lib/client.js +545 -44
- package/lib/client.js.map +1 -1
- package/lib/index.js +229 -0
- package/package.json +25 -3
- package/src/client/SmoothStreamCard.module.css +253 -0
- package/src/client/SmoothStreamCard.tsx +122 -0
- package/src/client/TypewriterAssistantNodeView.tsx +11 -3
- package/src/client/index.ts +96 -3
- package/src/client/locales.ts +74 -0
- package/src/client/smooth-stream-card-controller.ts +162 -0
- package/src/client/smooth-stream-settings-api.ts +65 -0
- package/src/package-meta.ts +39 -0
- package/src/plugin.ts +101 -0
- package/src/profile-installation.ts +121 -0
- package/src/settings-api.ts +33 -0
- package/src/settings.ts +30 -0
- package/README.zh.md +0 -88
package/src/plugin.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis'
|
|
2
2
|
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
3
|
+
import type { ConnectionRpcHandler } from '@deepseek-ai/dsh-client-connection'
|
|
4
|
+
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
3
5
|
import Schema from '@deepseek-ai/schemastery'
|
|
4
6
|
import { DEFAULT_STREAM_CONFIG, type StreamConfig } from './config.ts'
|
|
5
7
|
import { injectStreamConfig } from './boot-config.ts'
|
|
8
|
+
import { STREAM_PACKAGE_NAME, STREAM_PACKAGE_VERSION } from './package-meta.ts'
|
|
9
|
+
import { inspectProfileInstallation, updateNpmProfilePackage } from './profile-installation.ts'
|
|
10
|
+
import { STREAM_SETTINGS_RPC, STREAM_SETTINGS_RPC_CHANNEL, type StreamSettingsView } from './settings-api.ts'
|
|
11
|
+
import { DEFAULT_STREAM_SETTINGS, STREAM_SETTINGS_NS, type StreamSettings } from './settings.ts'
|
|
6
12
|
|
|
7
13
|
/** Display name shown by the Host loader while the plugin is mounted. */
|
|
8
14
|
export const name = 'dsh-smooth-stream'
|
|
@@ -31,6 +37,14 @@ export const Config: Schema<Config> = Schema.object({
|
|
|
31
37
|
.default(DEFAULT_STREAM_CONFIG.maxScrollSpeedPxPerSec),
|
|
32
38
|
})
|
|
33
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Schema of the user-owned settings section. The Host keeps it in the durable
|
|
42
|
+
* settings provider while the browser edits it through the plugin RPC below.
|
|
43
|
+
*/
|
|
44
|
+
export const StreamSettingsSchema: Schema<StreamSettings> = Schema.object({
|
|
45
|
+
thinkAutoExpand: Schema.boolean().default(DEFAULT_STREAM_SETTINGS.thinkAutoExpand),
|
|
46
|
+
})
|
|
47
|
+
|
|
34
48
|
/**
|
|
35
49
|
* Host half: log the resolved configuration and bridge it to the browser
|
|
36
50
|
* half. The web boot graph carries no per-entry config, so the validated
|
|
@@ -51,4 +65,91 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
51
65
|
'dsh-smooth-stream: boot config bridge',
|
|
52
66
|
)
|
|
53
67
|
})
|
|
68
|
+
// The core settings RPC deliberately filters third-party namespaces. Keep
|
|
69
|
+
// the durable provider as the authority, but expose this one schema through
|
|
70
|
+
// the plugin's own loopback-only connection channel instead.
|
|
71
|
+
ctx.inject(['settings'], (settingsCtx) => {
|
|
72
|
+
const scope = settingsCtx.settings.register(
|
|
73
|
+
settingsNamespace(STREAM_SETTINGS_NS),
|
|
74
|
+
StreamSettingsSchema,
|
|
75
|
+
{ applies: 'live' },
|
|
76
|
+
)
|
|
77
|
+
settingsCtx.inject(['connection'], (connectionCtx) => {
|
|
78
|
+
let upgrade: Promise<void> | undefined
|
|
79
|
+
|
|
80
|
+
const view = (): StreamSettingsView => {
|
|
81
|
+
const installation = inspectProfileInstallation(connectionCtx.baseUrl, STREAM_PACKAGE_NAME)
|
|
82
|
+
return {
|
|
83
|
+
version: STREAM_PACKAGE_VERSION,
|
|
84
|
+
installation: installation.kind,
|
|
85
|
+
writable: connectionCtx.settings.writable,
|
|
86
|
+
thinkAutoExpand: scope.get().thinkAutoExpand,
|
|
87
|
+
canUpgrade: installation.kind === 'npm',
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const handle: ConnectionRpcHandler = async (endpoint, payload) => {
|
|
92
|
+
if (endpoint === STREAM_SETTINGS_RPC.read) return { ok: true, value: view() }
|
|
93
|
+
if (endpoint === STREAM_SETTINGS_RPC.write) {
|
|
94
|
+
if (typeof payload !== 'object' || payload === null
|
|
95
|
+
|| typeof (payload as { thinkAutoExpand?: unknown }).thinkAutoExpand !== 'boolean') {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
error: {
|
|
99
|
+
code: 'settings-rejected',
|
|
100
|
+
message: 'thinkAutoExpand must be a boolean',
|
|
101
|
+
details: { ns: STREAM_SETTINGS_NS },
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!connectionCtx.settings.writable) {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
error: {
|
|
109
|
+
code: 'settings-rejected',
|
|
110
|
+
message: 'smooth-stream settings are read-only',
|
|
111
|
+
details: { ns: STREAM_SETTINGS_NS },
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
await scope.update({ thinkAutoExpand: (payload as { thinkAutoExpand: boolean }).thinkAutoExpand })
|
|
117
|
+
} catch {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
error: {
|
|
121
|
+
code: 'settings-rejected',
|
|
122
|
+
message: 'smooth-stream settings update failed',
|
|
123
|
+
details: { ns: STREAM_SETTINGS_NS },
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { ok: true, value: view() }
|
|
128
|
+
}
|
|
129
|
+
if (endpoint === STREAM_SETTINGS_RPC.upgrade) {
|
|
130
|
+
const installation = inspectProfileInstallation(connectionCtx.baseUrl, STREAM_PACKAGE_NAME)
|
|
131
|
+
if (installation.kind !== 'npm') {
|
|
132
|
+
return { ok: false, error: { code: 'internal', message: 'smooth-stream is not an npm profile dependency', details: {} } }
|
|
133
|
+
}
|
|
134
|
+
if (upgrade !== undefined) {
|
|
135
|
+
return { ok: false, error: { code: 'internal', message: 'smooth-stream update is already running', details: {} } }
|
|
136
|
+
}
|
|
137
|
+
upgrade = updateNpmProfilePackage(installation.profileDir, STREAM_PACKAGE_NAME)
|
|
138
|
+
try {
|
|
139
|
+
await upgrade
|
|
140
|
+
} catch {
|
|
141
|
+
return { ok: false, error: { code: 'internal', message: 'smooth-stream update failed', details: {} } }
|
|
142
|
+
} finally {
|
|
143
|
+
upgrade = undefined
|
|
144
|
+
}
|
|
145
|
+
return { ok: true, value: { restartRequired: true } }
|
|
146
|
+
}
|
|
147
|
+
return { ok: false, error: { code: 'internal', message: `unknown smooth-stream endpoint ${JSON.stringify(endpoint)}`, details: {} } }
|
|
148
|
+
}
|
|
149
|
+
connectionCtx.effect(
|
|
150
|
+
() => connectionCtx.connection.rpc.handle(STREAM_SETTINGS_RPC_CHANNEL, handle, { authority: 'loopback' }),
|
|
151
|
+
'dsh-smooth-stream: settings RPC',
|
|
152
|
+
)
|
|
153
|
+
})
|
|
154
|
+
})
|
|
54
155
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/** Host-only profile source inspection and fixed npm update runner. */
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import { readFileSync } from 'node:fs'
|
|
5
|
+
import { basename, isAbsolute, join } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
/** A profile installation that can be updated through a fixed pnpm invocation. */
|
|
9
|
+
export interface NpmProfileInstallation {
|
|
10
|
+
kind: 'npm'
|
|
11
|
+
profileDir: string
|
|
12
|
+
profileName: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A local source is deliberately not eligible for one-click npm updates. */
|
|
16
|
+
export interface DevelopmentProfileInstallation {
|
|
17
|
+
kind: 'development'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The active context is not a recognized profile dependency. */
|
|
21
|
+
export interface UnmanagedProfileInstallation {
|
|
22
|
+
kind: 'unmanaged'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Installation source visible to the plugin's Host RPC. */
|
|
26
|
+
export type ProfileInstallation = NpmProfileInstallation | DevelopmentProfileInstallation | UnmanagedProfileInstallation
|
|
27
|
+
|
|
28
|
+
interface ProfileManifest {
|
|
29
|
+
dependencies?: unknown
|
|
30
|
+
dsh?: unknown
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
34
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
35
|
+
? value as Record<string, unknown>
|
|
36
|
+
: undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function profileDirectory(baseUrl: string | undefined): string | undefined {
|
|
40
|
+
if (baseUrl === undefined) return undefined
|
|
41
|
+
try {
|
|
42
|
+
const url = new URL(baseUrl)
|
|
43
|
+
return url.protocol === 'file:' ? fileURLToPath(url) : undefined
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function hasBundle(manifest: ProfileManifest, packageName: string): boolean {
|
|
50
|
+
const dsh = record(manifest.dsh)
|
|
51
|
+
const profile = record(dsh?.profile)
|
|
52
|
+
const bundles = profile?.bundles
|
|
53
|
+
return Array.isArray(bundles) && bundles.includes(packageName)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isLocalSpecifier(specifier: string): boolean {
|
|
57
|
+
return specifier.startsWith('link:')
|
|
58
|
+
|| specifier.startsWith('file:')
|
|
59
|
+
|| specifier.startsWith('.')
|
|
60
|
+
|| isAbsolute(specifier)
|
|
61
|
+
|| /^[A-Za-z]:[\\/]/.test(specifier)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isRegistrySpecifier(specifier: string): boolean {
|
|
65
|
+
// A registry range/tag contains neither a scheme nor a path. Treat every
|
|
66
|
+
// package-manager extension (catalog:, patch:, portal:, workspace:, etc.)
|
|
67
|
+
// as unconfirmed rather than guessing that an update may replace it.
|
|
68
|
+
return specifier.length > 0 && !specifier.includes(':') && !/[\\/]/.test(specifier)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isNpmSpecifier(specifier: string, packageName: string): boolean {
|
|
72
|
+
if (!specifier.startsWith('npm:')) return isRegistrySpecifier(specifier)
|
|
73
|
+
const aliased = specifier.slice('npm:'.length)
|
|
74
|
+
if (aliased === packageName) return true
|
|
75
|
+
const prefix = `${packageName}@`
|
|
76
|
+
return aliased.startsWith(prefix) && isRegistrySpecifier(aliased.slice(prefix.length))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Read only the profile manifest anchored by the current Cordis config tree.
|
|
81
|
+
* A malformed or unrelated tree never receives an update affordance.
|
|
82
|
+
*/
|
|
83
|
+
export function inspectProfileInstallation(
|
|
84
|
+
baseUrl: string | undefined,
|
|
85
|
+
packageName: string,
|
|
86
|
+
): ProfileInstallation {
|
|
87
|
+
const profileDir = profileDirectory(baseUrl)
|
|
88
|
+
if (profileDir === undefined) return { kind: 'unmanaged' }
|
|
89
|
+
let manifest: ProfileManifest
|
|
90
|
+
try {
|
|
91
|
+
manifest = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8')) as ProfileManifest
|
|
92
|
+
} catch {
|
|
93
|
+
return { kind: 'unmanaged' }
|
|
94
|
+
}
|
|
95
|
+
const dependencies = record(manifest.dependencies)
|
|
96
|
+
const specifier = dependencies?.[packageName]
|
|
97
|
+
if (typeof specifier !== 'string' || !hasBundle(manifest, packageName)) {
|
|
98
|
+
return { kind: 'unmanaged' }
|
|
99
|
+
}
|
|
100
|
+
if (isLocalSpecifier(specifier)) return { kind: 'development' }
|
|
101
|
+
if (!isNpmSpecifier(specifier, packageName)) return { kind: 'unmanaged' }
|
|
102
|
+
return { kind: 'npm', profileDir, profileName: basename(profileDir) }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Run the same fixed package update operation that a profile user would invoke. */
|
|
106
|
+
export function updateNpmProfilePackage(profileDir: string, packageName: string): Promise<void> {
|
|
107
|
+
const command = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const child = spawn(command, ['update', packageName], {
|
|
110
|
+
cwd: profileDir,
|
|
111
|
+
stdio: 'ignore',
|
|
112
|
+
shell: false,
|
|
113
|
+
windowsHide: true,
|
|
114
|
+
})
|
|
115
|
+
child.once('error', reject)
|
|
116
|
+
child.once('exit', (code, signal) => {
|
|
117
|
+
if (code === 0) resolve()
|
|
118
|
+
else reject(new Error(`dsh-smooth-stream: pnpm update failed (${signal ?? String(code)})`))
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Shared wire vocabulary for the plugin-owned settings RPC channel. */
|
|
2
|
+
|
|
3
|
+
/** Dedicated, loopback-only RPC channel registered by the Host half. */
|
|
4
|
+
export const STREAM_SETTINGS_RPC_CHANNEL = '/smooth-stream'
|
|
5
|
+
|
|
6
|
+
/** Endpoints accepted by {@link STREAM_SETTINGS_RPC_CHANNEL}. */
|
|
7
|
+
export const STREAM_SETTINGS_RPC = {
|
|
8
|
+
read: 'settings.read',
|
|
9
|
+
write: 'settings.write',
|
|
10
|
+
upgrade: 'plugin.upgrade',
|
|
11
|
+
} as const
|
|
12
|
+
|
|
13
|
+
/** How the active profile supplied this plugin. */
|
|
14
|
+
export type StreamInstallationKind = 'npm' | 'development' | 'unmanaged'
|
|
15
|
+
|
|
16
|
+
/** Redacted, plugin-owned configuration state returned to the browser. */
|
|
17
|
+
export interface StreamSettingsView {
|
|
18
|
+
/** Version of the package currently running in the Host process. */
|
|
19
|
+
version: string
|
|
20
|
+
/** Source class of this package in the active profile. */
|
|
21
|
+
installation: StreamInstallationKind
|
|
22
|
+
/** Whether the Host settings provider permits a write. */
|
|
23
|
+
writable: boolean
|
|
24
|
+
/** Current resolved preference. */
|
|
25
|
+
thinkAutoExpand: boolean
|
|
26
|
+
/** Whether a fixed npm update command is safe to offer. */
|
|
27
|
+
canUpgrade: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Successful package update acknowledgement; loading the new code needs a restart. */
|
|
31
|
+
export interface StreamUpgradeView {
|
|
32
|
+
restartRequired: true
|
|
33
|
+
}
|
package/src/settings.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-owned settings for the smooth-stream plugin, exposed to the Host
|
|
3
|
+
* settings service and edited from the Web Settings "plugin configuration"
|
|
4
|
+
* page. This is the runtime-editable complement to {@link StreamConfig}: that
|
|
5
|
+
* contract is composed at load and bridged once through the boot global, while
|
|
6
|
+
* these preferences live in the durable user-settings document and take effect
|
|
7
|
+
* live.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Settings namespace registered by the Host and served through the plugin RPC. */
|
|
11
|
+
export const STREAM_SETTINGS_NS = 'smooth-stream'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Preferences a user may set. Deliberately separate from {@link StreamConfig}
|
|
15
|
+
* because the two change at different times: composition-time values go
|
|
16
|
+
* through the boot global, a live UI edit goes through the protected plugin RPC.
|
|
17
|
+
*/
|
|
18
|
+
export interface StreamSettings {
|
|
19
|
+
/**
|
|
20
|
+
* Whether a reasoning ("Think") block auto-expands while it is the
|
|
21
|
+
* streaming tail. Off keeps the block collapsed — the user can still open
|
|
22
|
+
* it by hand — and stops the running state from re-owning the disclosure.
|
|
23
|
+
*/
|
|
24
|
+
thinkAutoExpand: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Defaults shared by the Host schema and the client-side fallback. */
|
|
28
|
+
export const DEFAULT_STREAM_SETTINGS: StreamSettings = {
|
|
29
|
+
thinkAutoExpand: true,
|
|
30
|
+
}
|
package/README.zh.md
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
# dsh-smooth-stream
|
|
2
|
-
|
|
3
|
-
[English](README.md) | 中文
|
|
4
|
-
|
|
5
|
-
[](https://whyihaveyou.github.io/dsh-suite/)
|
|
6
|
-
|
|
7
|
-
**dsh-smooth-stream** 是 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)的社区插件,给 Web 对话做**丝滑流式渲染**:字跟着模型走、换行滑入、不闪。不是官方发行的一部分。
|
|
8
|
-
|
|
9
|
-
项目主页:<https://laplace-bit.github.io/dsh-smooth-stream/>
|
|
10
|
-
|
|
11
|
-
## 效果
|
|
12
|
-
|
|
13
|
-
左:默认 Web UI。右:dsh-smooth-stream。
|
|
14
|
-
|
|
15
|
-

|
|
16
|
-
|
|
17
|
-
## 它做什么
|
|
18
|
-
|
|
19
|
-
- **揭示跟着模型走。** 助手文本按到达速率出现。突发不会整段倒出来,慢流也不会停住再猛地补上。
|
|
20
|
-
- **一直是 Markdown。** 代码、强调等在流式过程中就按格式渲染,没有先出纯文本再换成排版的交接。
|
|
21
|
-
- **换行是滑进来的。** 新的一行或正在变高的工具卡片会滑入视野,而不是把整段记录往上顶一格。
|
|
22
|
-
- **滚动条归你。** 往上翻看前文时 overlay 会松手。只有回到底部才会继续跟随——点「回到底部」也算。
|
|
23
|
-
- **思考仍是内置那一行。** 推理用原来的 disclosure。它是当前流式尾部时展开,思考一结束就收起;箭头仍可手动开关。
|
|
24
|
-
- **整轮一起动。** 运行中的工具卡片、模型重试、workflow run 共用同一套跟随,所以滑的是整轮回复,不只是助手正文。
|
|
25
|
-
- **该停的时候会停。** `prefers-reduced-motion` 会直接给出全文,也不接管跟随。帧率低于 30 fps 且回复在屏外时,揭示会暂停,画面恢复后再补上。
|
|
26
|
-
|
|
27
|
-
## 安装
|
|
28
|
-
|
|
29
|
-
在 DeepSeek Harness 源码仓库里:
|
|
30
|
-
|
|
31
|
-
```sh
|
|
32
|
-
pnpm dsh plugin --profile web add github:Laplace-bit/dsh-smooth-stream
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
如果 `PATH` 上已经有 `dsh`:
|
|
36
|
-
|
|
37
|
-
```sh
|
|
38
|
-
dsh plugin --profile web add github:Laplace-bit/dsh-smooth-stream
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
第一次 `add` 失败是正常的。git 安装需要跑这个包的 `prepare` 脚本,pnpm ≥10 在你授权之前会拦截。打开 `~/.dsh/profiles/web/pnpm-workspace.yaml`,把 pnpm 打印的那段加进去。当前 pnpm 打印的是:
|
|
42
|
-
|
|
43
|
-
```yaml
|
|
44
|
-
onlyBuiltDependencies:
|
|
45
|
-
- dsh-smooth-stream
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
然后重新执行同一条 `add`。
|
|
49
|
-
|
|
50
|
-
启动界面:
|
|
51
|
-
|
|
52
|
-
```sh
|
|
53
|
-
pnpm dsh web
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Host 日志里应出现 `[dsh-smooth-stream] plugin loaded!`。
|
|
57
|
-
|
|
58
|
-
卸载:`pnpm dsh plugin --profile web remove dsh-smooth-stream`(或 `dsh plugin --profile web remove dsh-smooth-stream`)。
|
|
59
|
-
|
|
60
|
-
## 配置
|
|
61
|
-
|
|
62
|
-
组合包默认 `preset: balanced`。要换节拍,在 profile 的 `cordis.patch.yml` 里改:
|
|
63
|
-
|
|
64
|
-
| `preset` | 手感 |
|
|
65
|
-
| --- | --- |
|
|
66
|
-
| `realtime` | 更贴模型到达 |
|
|
67
|
-
| `balanced` | 默认 |
|
|
68
|
-
| `silky` | 缓冲更大,追上更慢 |
|
|
69
|
-
|
|
70
|
-
`maxScrollSpeedPxPerSec`(默认 `1000`)是速度上限,避免第一次滞后过大时瞬移。
|
|
71
|
-
|
|
72
|
-
## 常见问题
|
|
73
|
-
|
|
74
|
-
**这是 DeepSeek 官方插件吗?**
|
|
75
|
-
不是。它是 DeepSeek Harness(`dsh`)Web UI 的社区插件,MIT 协议开源,不属于 DeepSeek 官方发行。
|
|
76
|
-
|
|
77
|
-
**dsh 插件怎么安装?**
|
|
78
|
-
用内置插件命令:在 dsh 源码目录运行 `dsh plugin --profile web add github:Laplace-bit/dsh-smooth-stream`(pnpm ≥10 的构建授权说明见[安装](#安装))。
|
|
79
|
-
|
|
80
|
-
**能用 npm 安装吗?**
|
|
81
|
-
暂时不能,目前只支持从 GitHub 安装。可以关注本仓库,npm 发布后会有通知。
|
|
82
|
-
|
|
83
|
-
**支持 `prefers-reduced-motion` 吗?**
|
|
84
|
-
支持。系统开启减少动态效果时直接显示完整文本、不接管跟随;帧率低于 30 fps 且回复在屏外时,揭示自动暂停、恢复后再补上。
|
|
85
|
-
|
|
86
|
-
## 许可证
|
|
87
|
-
|
|
88
|
-
[MIT](LICENSE)
|