dsh-diary-memory 0.1.0 → 0.1.2
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.md +71 -15
- package/cordis.patch.yml +0 -1
- package/lib/bailian-client.js +8 -3
- package/lib/bailian-client.js.map +1 -1
- package/lib/client/choices.d.ts +53 -0
- package/lib/client/choices.js +51 -0
- package/lib/client/choices.js.map +1 -0
- package/lib/client/controller.d.ts +40 -0
- package/lib/client/controller.js +130 -0
- package/lib/client/controller.js.map +1 -0
- package/lib/client/index.d.ts +5 -0
- package/lib/client/index.js +19 -0
- package/lib/client/index.js.map +1 -0
- package/lib/client/page.d.ts +5 -0
- package/lib/client/page.js +24 -0
- package/lib/client/page.js.map +1 -0
- package/lib/client.js +430 -0
- package/lib/client.js.map +7 -0
- package/lib/config.d.ts +63 -1
- package/lib/config.js +33 -4
- package/lib/config.js.map +1 -1
- package/lib/diary-runtime.d.ts +10 -0
- package/lib/diary-runtime.js +65 -0
- package/lib/diary-runtime.js.map +1 -0
- package/lib/index.d.ts +3 -3
- package/lib/index.js +7 -2
- package/lib/index.js.map +1 -1
- package/lib/memory-store.d.ts +6 -3
- package/lib/memory-store.js +6 -3
- package/lib/memory-store.js.map +1 -1
- package/lib/model-parameters.d.ts +7 -0
- package/lib/model-parameters.js +38 -0
- package/lib/model-parameters.js.map +1 -0
- package/lib/orchestrator.js +4 -66
- package/lib/orchestrator.js.map +1 -1
- package/lib/retrieval-service.d.ts +12 -3
- package/lib/retrieval-service.js +25 -13
- package/lib/retrieval-service.js.map +1 -1
- package/lib/session-memory.d.ts +20 -0
- package/lib/session-memory.js +161 -0
- package/lib/session-memory.js.map +1 -0
- package/lib/session-selection.d.ts +1 -0
- package/lib/session-selection.js +6 -0
- package/lib/session-selection.js.map +1 -0
- package/lib/sqlite.d.ts +25 -0
- package/lib/sqlite.js +64 -0
- package/lib/sqlite.js.map +1 -0
- package/lib/tools.d.ts +1 -0
- package/lib/tools.js +3 -0
- package/lib/tools.js.map +1 -1
- package/package.json +140 -26
- package/src/bailian-client.ts +7 -3
- package/src/client/choices.ts +66 -0
- package/src/client/controller.ts +108 -0
- package/src/client/index.ts +26 -0
- package/src/client/page.tsx +100 -0
- package/src/config.ts +41 -4
- package/src/diary-runtime.ts +62 -0
- package/src/index.ts +11 -5
- package/src/memory-store.ts +10 -6
- package/src/model-parameters.ts +32 -0
- package/src/orchestrator.ts +4 -57
- package/src/retrieval-service.ts +27 -14
- package/src/session-memory.ts +151 -0
- package/src/session-selection.ts +4 -0
- package/src/sqlite.ts +78 -0
- package/src/tools.ts +4 -0
package/package.json
CHANGED
|
@@ -1,15 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-diary-memory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "为 DSH 的陪伴与个人数字分身提供有温度、可演化的日记记忆、百炼检索和安全压缩。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"types": "./lib/index.d.ts",
|
|
8
8
|
"license": "MIT",
|
|
9
|
-
"
|
|
10
|
-
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/cyhddyx/dsh-diary-memory.git"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/cyhddyx/dsh-diary-memory#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/cyhddyx/dsh-diary-memory/issues"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22.15",
|
|
19
|
+
"dsh": ">=0.2.1-rc.2 <0.3.0"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"lib",
|
|
23
|
+
"src",
|
|
24
|
+
"locale",
|
|
25
|
+
"icon.svg",
|
|
26
|
+
"cordis.patch.yml",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
11
30
|
"exports": {
|
|
12
|
-
"
|
|
31
|
+
"./client": {
|
|
32
|
+
"types": "./lib/client/index.d.ts",
|
|
33
|
+
"default": "./lib/client.js"
|
|
34
|
+
},
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./lib/index.d.ts",
|
|
37
|
+
"default": "./lib/index.js"
|
|
38
|
+
},
|
|
13
39
|
"./compaction": "./lib/orchestrator.js",
|
|
14
40
|
"./compaction/locale/*.json": "./locale/compaction/*.json",
|
|
15
41
|
"./compaction/icon": "./icon.svg",
|
|
@@ -23,51 +49,139 @@
|
|
|
23
49
|
"./package.json": "./package.json"
|
|
24
50
|
},
|
|
25
51
|
"scripts": {
|
|
26
|
-
"build": "tsc -p tsconfig.json",
|
|
52
|
+
"build": "tsc -p tsconfig.json && node scripts/build-client.mjs",
|
|
27
53
|
"check": "tsc -p tsconfig.json --noEmit",
|
|
28
|
-
"test": "
|
|
54
|
+
"test": "npm run build && node --test test/*.test.mjs",
|
|
29
55
|
"prepack": "npm run build"
|
|
30
56
|
},
|
|
31
|
-
"dependencies": {
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"zod": "4.4.3"
|
|
59
|
+
},
|
|
32
60
|
"peerDependencies": {
|
|
33
61
|
"@deepseek-ai/cordis": "~4.0.5",
|
|
62
|
+
"@deepseek-ai/dsh-agent": ">=0.2.1-rc.2 <0.3.0",
|
|
63
|
+
"@deepseek-ai/dsh-compaction": ">=0.2.1-rc.2 <0.3.0",
|
|
64
|
+
"@deepseek-ai/dsh-compaction-basic": ">=0.2.1-rc.2 <0.3.0",
|
|
65
|
+
"@deepseek-ai/dsh-llm": ">=0.2.1-rc.2 <0.3.0",
|
|
66
|
+
"@deepseek-ai/dsh-session": ">=0.2.1-rc.2 <0.3.0",
|
|
67
|
+
"@deepseek-ai/dsh-session-projection": ">=0.2.1-rc.2 <0.3.0",
|
|
68
|
+
"@deepseek-ai/dsh-settings": ">=0.2.1-rc.2 <0.3.0",
|
|
69
|
+
"@deepseek-ai/dsh-tools": ">=0.2.1-rc.2 <0.3.0",
|
|
34
70
|
"@deepseek-ai/schemastery": "~3.18.5",
|
|
35
|
-
"
|
|
36
|
-
"@deepseek-ai/dsh-
|
|
37
|
-
"@deepseek-ai/dsh-
|
|
38
|
-
"@deepseek-ai/dsh-llm": "0.2.1-rc.3",
|
|
39
|
-
"@deepseek-ai/dsh-session": "0.2.1-rc.3",
|
|
40
|
-
"@deepseek-ai/dsh-session-projection": "0.2.1-rc.3",
|
|
41
|
-
"@deepseek-ai/dsh-tools": "0.2.1-rc.3"
|
|
71
|
+
"better-sqlite3": "^12.4.1",
|
|
72
|
+
"@deepseek-ai/dsh-agent-preset-registry": ">=0.2.1-rc.2 <0.3.0",
|
|
73
|
+
"@deepseek-ai/dsh-scope": ">=0.2.1-rc.2 <0.3.0"
|
|
42
74
|
},
|
|
43
75
|
"peerDependenciesMeta": {
|
|
44
|
-
"@deepseek-ai/cordis": {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"@deepseek-ai/
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"@deepseek-ai/dsh-
|
|
51
|
-
|
|
52
|
-
|
|
76
|
+
"@deepseek-ai/cordis": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"@deepseek-ai/schemastery": {
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
82
|
+
"@deepseek-ai/dsh-agent": {
|
|
83
|
+
"optional": true
|
|
84
|
+
},
|
|
85
|
+
"@deepseek-ai/dsh-compaction": {
|
|
86
|
+
"optional": true
|
|
87
|
+
},
|
|
88
|
+
"@deepseek-ai/dsh-compaction-basic": {
|
|
89
|
+
"optional": true
|
|
90
|
+
},
|
|
91
|
+
"@deepseek-ai/dsh-llm": {
|
|
92
|
+
"optional": true
|
|
93
|
+
},
|
|
94
|
+
"@deepseek-ai/dsh-session": {
|
|
95
|
+
"optional": true
|
|
96
|
+
},
|
|
97
|
+
"@deepseek-ai/dsh-session-projection": {
|
|
98
|
+
"optional": true
|
|
99
|
+
},
|
|
100
|
+
"@deepseek-ai/dsh-tools": {
|
|
101
|
+
"optional": true
|
|
102
|
+
},
|
|
103
|
+
"@deepseek-ai/dsh-settings": {
|
|
104
|
+
"optional": true
|
|
105
|
+
},
|
|
106
|
+
"better-sqlite3": {
|
|
107
|
+
"optional": true
|
|
108
|
+
},
|
|
109
|
+
"@deepseek-ai/dsh-agent-preset-registry": {
|
|
110
|
+
"optional": true
|
|
111
|
+
},
|
|
112
|
+
"@deepseek-ai/dsh-scope": {
|
|
113
|
+
"optional": true
|
|
114
|
+
}
|
|
53
115
|
},
|
|
54
116
|
"devDependencies": {
|
|
55
117
|
"@deepseek-ai/cordis": "4.0.5",
|
|
56
|
-
"@deepseek-ai/
|
|
118
|
+
"@deepseek-ai/cosmokit": "1.8.6",
|
|
57
119
|
"@deepseek-ai/dsh-agent": "0.2.1-rc.3",
|
|
120
|
+
"@deepseek-ai/dsh-agent-loop": "0.2.1-rc.3",
|
|
121
|
+
"@deepseek-ai/dsh-agent-preset": "0.2.1-rc.3",
|
|
122
|
+
"@deepseek-ai/dsh-agent-preset-registry": "0.2.1-rc.3",
|
|
123
|
+
"@deepseek-ai/dsh-api-remotes": "0.2.1-rc.3",
|
|
124
|
+
"@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.3",
|
|
125
|
+
"@deepseek-ai/dsh-app-boot": "0.2.1-rc.3",
|
|
126
|
+
"@deepseek-ai/dsh-client-store": "0.2.1-rc.3",
|
|
127
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.3",
|
|
128
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.3",
|
|
129
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.2.1-rc.3",
|
|
130
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.3",
|
|
58
131
|
"@deepseek-ai/dsh-compaction": "0.2.1-rc.3",
|
|
59
132
|
"@deepseek-ai/dsh-compaction-basic": "0.2.1-rc.3",
|
|
133
|
+
"@deepseek-ai/dsh-config-editor": "0.2.1-rc.3",
|
|
60
134
|
"@deepseek-ai/dsh-llm": "0.2.1-rc.3",
|
|
135
|
+
"@deepseek-ai/dsh-scope": "0.2.1-rc.3",
|
|
61
136
|
"@deepseek-ai/dsh-session": "0.2.1-rc.3",
|
|
62
137
|
"@deepseek-ai/dsh-session-projection": "0.2.1-rc.3",
|
|
138
|
+
"@deepseek-ai/dsh-settings": "0.2.1-rc.3",
|
|
63
139
|
"@deepseek-ai/dsh-token-meter": "0.2.1-rc.3",
|
|
64
140
|
"@deepseek-ai/dsh-tools": "0.2.1-rc.3",
|
|
141
|
+
"@deepseek-ai/schemastery": "3.18.5",
|
|
142
|
+
"@shikijs/langs": "4.4.3",
|
|
65
143
|
"@types/better-sqlite3": "^7.6.13",
|
|
66
144
|
"@types/node": "^24.0.0",
|
|
145
|
+
"@types/react": "18.3.31",
|
|
146
|
+
"@types/react-dom": "18.3.7",
|
|
147
|
+
"anser": "2.3.5",
|
|
148
|
+
"clsx": "2.1.1",
|
|
149
|
+
"diff": "9.0.0",
|
|
150
|
+
"esbuild": "0.28.2",
|
|
151
|
+
"immer": "10.2.0",
|
|
152
|
+
"katex": "0.18.7",
|
|
153
|
+
"mdast-util-from-markdown": "2.1.0",
|
|
154
|
+
"mdast-util-gfm": "3.1.0",
|
|
155
|
+
"mdast-util-math": "3.0.0",
|
|
156
|
+
"micromark-core-commonmark": "2.0.4",
|
|
157
|
+
"micromark-extension-gfm": "3.0.0",
|
|
158
|
+
"micromark-extension-math": "3.1.0",
|
|
159
|
+
"micromark-factory-space": "2.1.0",
|
|
160
|
+
"micromark-util-character": "2.1.1",
|
|
161
|
+
"micromark-util-classify-character": "2.0.1",
|
|
162
|
+
"micromark-util-sanitize-uri": "2.0.1",
|
|
163
|
+
"micromark-util-symbol": "2.0.1",
|
|
164
|
+
"micromark-util-types": "2.0.3",
|
|
165
|
+
"react": "18.3.1",
|
|
166
|
+
"react-dom": "18.3.1",
|
|
167
|
+
"semver": "7.8.5",
|
|
168
|
+
"shiki": "4.4.3",
|
|
169
|
+
"simple-icons": "16.31.0",
|
|
67
170
|
"typescript": "^5.9.3",
|
|
68
|
-
"yaml": "^2.8.1"
|
|
171
|
+
"yaml": "^2.8.1",
|
|
172
|
+
"zustand": "5.0.16",
|
|
173
|
+
"better-sqlite3": "^12.4.1"
|
|
69
174
|
},
|
|
70
175
|
"dsh": {
|
|
71
|
-
"
|
|
176
|
+
"client": {
|
|
177
|
+
"inject": [
|
|
178
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
179
|
+
"@deepseek-ai/dsh-client-ui-renderer"
|
|
180
|
+
],
|
|
181
|
+
"platform": "web"
|
|
182
|
+
},
|
|
183
|
+
"bundle": {
|
|
184
|
+
"patch": "./cordis.patch.yml"
|
|
185
|
+
}
|
|
72
186
|
}
|
|
73
187
|
}
|
package/src/bailian-client.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { setTimeout as delay } from 'node:timers/promises'
|
|
2
2
|
import type { ResolvedConfig } from './config.js'
|
|
3
|
+
import { stableParameters } from './model-parameters.js'
|
|
3
4
|
|
|
4
5
|
interface BailianFailure {
|
|
5
6
|
status?: number
|
|
@@ -38,7 +39,10 @@ export class BailianClient {
|
|
|
38
39
|
this.fetch = fetchImpl
|
|
39
40
|
this.signal = signal
|
|
40
41
|
// Model names alone do not identify the vector space across endpoints/dimensions.
|
|
41
|
-
|
|
42
|
+
const space: unknown[] = [config.baseUrl, config.embeddingModel, config.embeddingDimensions]
|
|
43
|
+
// Keep the original vector-space key when no extra parameters were chosen.
|
|
44
|
+
if (Object.keys(config.embeddingParameters).length) space.push(stableParameters(config.embeddingParameters))
|
|
45
|
+
this.embeddingKey = JSON.stringify(space)
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
async request(path: string, body: unknown, callerSignal?: AbortSignal): Promise<unknown> {
|
|
@@ -95,7 +99,7 @@ export class BailianClient {
|
|
|
95
99
|
const output = await this.request('/api/v1/services/embeddings/text-embedding/text-embedding', {
|
|
96
100
|
model: embeddingModel,
|
|
97
101
|
input: { texts: [text] },
|
|
98
|
-
parameters: { text_type: textType, dimension: embeddingDimensions },
|
|
102
|
+
parameters: { ...this.config.embeddingParameters, text_type: textType, dimension: embeddingDimensions },
|
|
99
103
|
}, signal) as { embeddings?: { text_index?: unknown; embedding?: unknown }[] }
|
|
100
104
|
if (output.embeddings?.length !== 1 || output.embeddings[0].text_index !== 0) throw new BailianError('百炼 embedding 的 text_index 无效')
|
|
101
105
|
return validateVector(output.embeddings[0].embedding, embeddingDimensions)
|
|
@@ -108,7 +112,7 @@ export class BailianClient {
|
|
|
108
112
|
const output = await this.request('/api/v1/services/rerank/text-rerank/text-rerank', {
|
|
109
113
|
model: this.config.rerankModel,
|
|
110
114
|
input: { query, documents },
|
|
111
|
-
parameters: {
|
|
115
|
+
parameters: { return_documents: false, ...this.config.rerankParameters, top_n: topN },
|
|
112
116
|
}, signal) as { results?: RerankResult[] }
|
|
113
117
|
if (!Array.isArray(output.results) || output.results.length !== topN) throw new BailianError('百炼 rerank 结果数量无效')
|
|
114
118
|
const seen = new Set()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
|
|
2
|
+
import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
|
|
3
|
+
|
|
4
|
+
export interface SessionChoice { id: string; title: string }
|
|
5
|
+
export interface WriterChoice { provider: string; model: string; label: string }
|
|
6
|
+
export interface ChoicesState {
|
|
7
|
+
sessions: SessionChoice[]
|
|
8
|
+
sessionsReady: boolean
|
|
9
|
+
writers: WriterChoice[]
|
|
10
|
+
modelsStatus: 'loading' | 'ready' | 'error'
|
|
11
|
+
}
|
|
12
|
+
export interface SessionCatalog {
|
|
13
|
+
phase: string
|
|
14
|
+
ids: readonly string[]
|
|
15
|
+
byId: Record<string, { id: string; displayTitle: string; origin?: string }>
|
|
16
|
+
}
|
|
17
|
+
export interface ModelCatalogResult {
|
|
18
|
+
ok: boolean
|
|
19
|
+
value?: { groups: readonly { id: string; name: string; models: readonly { id: string; name: string }[] }[] }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Read only the native metadata catalog; never open or upload conversation bodies. */
|
|
23
|
+
export class DiaryChoices {
|
|
24
|
+
readonly store = createSnapshotStore<ChoicesState>({ sessions: [], sessionsReady: false, writers: [], modelsStatus: 'loading' })
|
|
25
|
+
private readonly unsubscribe?: () => void
|
|
26
|
+
private generation = 0
|
|
27
|
+
private disposed = false
|
|
28
|
+
|
|
29
|
+
constructor(private readonly catalog?: ObservableSnapshot<SessionCatalog>, private readonly loadModels?: () => Promise<ModelCatalogResult>, private readonly refreshSessions?: () => Promise<void>) {
|
|
30
|
+
this.unsubscribe = catalog?.subscribe(() => this.readSessions())
|
|
31
|
+
this.readSessions()
|
|
32
|
+
void this.refreshModels()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private readSessions(): void {
|
|
36
|
+
const value = this.catalog?.getSnapshot()
|
|
37
|
+
this.store.update(state => {
|
|
38
|
+
state.sessionsReady = value?.phase === 'ready'
|
|
39
|
+
state.sessions = value ? value.ids.flatMap(id => {
|
|
40
|
+
const row = value.byId[id]
|
|
41
|
+
return row && row.origin !== 'subagent' ? [{ id, title: row.displayTitle }] : []
|
|
42
|
+
}) : []
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async refreshModels(): Promise<void> {
|
|
47
|
+
const generation = ++this.generation
|
|
48
|
+
this.store.update(state => { state.modelsStatus = 'loading' })
|
|
49
|
+
try {
|
|
50
|
+
const response = await this.loadModels?.()
|
|
51
|
+
if (this.disposed || generation !== this.generation) return
|
|
52
|
+
this.store.update(state => {
|
|
53
|
+
state.writers = response?.ok && response.value ? response.value.groups.flatMap(group => group.models.map(model => ({ provider: group.id, model: model.id, label: `${group.name} / ${model.name}` }))) : []
|
|
54
|
+
state.modelsStatus = response?.ok ? 'ready' : 'error'
|
|
55
|
+
})
|
|
56
|
+
} catch {
|
|
57
|
+
if (!this.disposed && generation === this.generation) this.store.update(state => { state.modelsStatus = 'error' })
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async refresh(): Promise<void> {
|
|
62
|
+
await Promise.allSettled([this.refreshSessions?.(), this.refreshModels()])
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
dispose(): void { this.disposed = true; this.generation++; this.unsubscribe?.() }
|
|
66
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { SettingsFormModel, settingsTextField } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
2
|
+
import type { SettingsFieldSpec, SettingsFieldState, SettingsFormScope, SettingsFormShell } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
3
|
+
import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
|
4
|
+
import type { DiaryConfig } from '../config.js'
|
|
5
|
+
import { validateModelParameters } from '../model-parameters.js'
|
|
6
|
+
import { validateSessionIds } from '../session-selection.js'
|
|
7
|
+
import { DiaryChoices } from './choices.js'
|
|
8
|
+
import type { ChoicesState } from './choices.js'
|
|
9
|
+
|
|
10
|
+
export const editableFields = ['apiKey', 'baseUrl', 'embeddingModel', 'embeddingDimensions', 'embeddingParameters', 'rerankModel', 'rerankParameters', 'candidateCount', 'recallCount', 'compressionRounds', 'retainRounds', 'autoRecall', 'sessionIds', 'diaryProvider', 'diaryModel'] as const
|
|
11
|
+
export type EditableField = typeof editableFields[number]
|
|
12
|
+
export type FormValues = Partial<Pick<DiaryConfig, EditableField>>
|
|
13
|
+
export interface DiarySettingsState extends SettingsFormShell, ChoicesState {
|
|
14
|
+
fields: Record<EditableField, SettingsFieldState>
|
|
15
|
+
keyConfigured: boolean
|
|
16
|
+
error: string | null
|
|
17
|
+
selectedSessions: string[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function numberField(field: string, min: number, max: number): SettingsFieldSpec {
|
|
21
|
+
return {
|
|
22
|
+
field, format: value => typeof value === 'number' ? String(value) : '',
|
|
23
|
+
parse: text => !text.trim() ? { kind: 'clear' }
|
|
24
|
+
: Number.isSafeInteger(Number(text)) && Number(text) >= min && Number(text) <= max ? { kind: 'set', value: Number(text) } : undefined,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function parametersField(field: string, kind: 'embedding' | 'rerank'): SettingsFieldSpec {
|
|
28
|
+
return {
|
|
29
|
+
field, format: value => value && typeof value === 'object' ? JSON.stringify(value, null, 2) : '{}',
|
|
30
|
+
parse: text => {
|
|
31
|
+
if (!text.trim()) return { kind: 'clear' }
|
|
32
|
+
try { return { kind: 'set', value: validateModelParameters(JSON.parse(text), kind) } } catch { return undefined }
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const specs: SettingsFieldSpec[] = [
|
|
37
|
+
...['apiKey', 'baseUrl', 'embeddingModel', 'rerankModel', 'diaryProvider', 'diaryModel'].map(settingsTextField),
|
|
38
|
+
{ field: 'sessionIds', format: value => JSON.stringify(value ?? []), parse: text => {
|
|
39
|
+
try { return { kind: 'set', value: validateSessionIds(JSON.parse(text || '[]')) } } catch { return undefined }
|
|
40
|
+
} },
|
|
41
|
+
numberField('embeddingDimensions', 1, 4096), numberField('candidateCount', 1, 100), numberField('recallCount', 1, 100),
|
|
42
|
+
numberField('compressionRounds', 2, 1000000), numberField('retainRounds', 1, 1000000),
|
|
43
|
+
parametersField('embeddingParameters', 'embedding'), parametersField('rerankParameters', 'rerank'),
|
|
44
|
+
{ field: 'autoRecall', format: value => String(value ?? true), parse: text => text === 'true' || text === 'false' ? { kind: 'set', value: text === 'true' } : undefined },
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
/** Native staged form: only edited paths are sent, preserving undisclosed secrets. */
|
|
48
|
+
export class DiarySettingsController {
|
|
49
|
+
readonly form: SettingsFormModel<FormValues>
|
|
50
|
+
readonly store: SnapshotStore<DiarySettingsState>
|
|
51
|
+
private readonly unsubscribeChoices: () => void
|
|
52
|
+
|
|
53
|
+
constructor(private readonly scope: SettingsFormScope<FormValues>, private readonly keyConfigured: () => boolean = () => false, readonly choices = new DiaryChoices()) {
|
|
54
|
+
this.form = new SettingsFormModel(scope, specs)
|
|
55
|
+
this.store = this.form.bind(() => this.projection())
|
|
56
|
+
this.unsubscribeChoices = choices.store.subscribe(() => this.store.set(this.projection()))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private projection(): DiarySettingsState {
|
|
60
|
+
const fields = Object.fromEntries(editableFields.map(field => [field, this.form.field(field)])) as DiarySettingsState['fields']
|
|
61
|
+
const base = this.scope.getSnapshot().base as FormValues | undefined
|
|
62
|
+
const number = (field: 'compressionRounds' | 'retainRounds' | 'recallCount' | 'candidateCount', fallback: number) => fields[field].text.trim() ? Number(fields[field].text) : base?.[field] ?? fallback
|
|
63
|
+
let error: string | null = null
|
|
64
|
+
let selectedSessions: string[] = []
|
|
65
|
+
try { selectedSessions = validateSessionIds(JSON.parse(fields.sessionIds.text || '[]')) } catch (failure) { error = (failure as Error).message }
|
|
66
|
+
const writerProvider = fields.diaryProvider.text.trim()
|
|
67
|
+
const writerModel = fields.diaryModel.text.trim()
|
|
68
|
+
if (Boolean(writerProvider) !== Boolean(writerModel)) error = '写日记的模型提供方与模型名必须同时填写,或同时留空跟随聊天模型。'
|
|
69
|
+
if (writerProvider.length > 200 || writerModel.length > 200) error = '写日记的提供方与模型名不能超过 200 字符。'
|
|
70
|
+
if (number('compressionRounds', 20) <= number('retainRounds', 2)) error = '压缩轮次必须大于保留轮次。'
|
|
71
|
+
if (number('recallCount', 6) > number('candidateCount', 30)) error = '召回条数不能大于候选数量。'
|
|
72
|
+
for (const kind of ['embedding', 'rerank'] as const) {
|
|
73
|
+
const text = fields[`${kind}Parameters`].text.trim()
|
|
74
|
+
try { validateModelParameters(JSON.parse(text || '{}'), kind) }
|
|
75
|
+
catch (failure) { error = failure instanceof SyntaxError ? `${kind} 参数需要合法的 JSON 对象。` : (failure as Error).message }
|
|
76
|
+
}
|
|
77
|
+
const endpoint = fields.baseUrl.text.trim()
|
|
78
|
+
try {
|
|
79
|
+
if (endpoint) {
|
|
80
|
+
const url = new URL(endpoint)
|
|
81
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== '/' || url.protocol !== 'https:' && !(url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))) throw new Error()
|
|
82
|
+
}
|
|
83
|
+
} catch { fields.baseUrl.invalid = true; error = '百炼端点需要 HTTPS origin,例如 https://dashscope.aliyuncs.com。' }
|
|
84
|
+
for (const field of ['embeddingModel', 'rerankModel'] as const) {
|
|
85
|
+
if (fields[field].text.trim().length > 200) { fields[field].invalid = true; error = '模型名不能超过 200 字符。' }
|
|
86
|
+
}
|
|
87
|
+
const shell = this.form.shell()
|
|
88
|
+
return { ...shell, ...this.choices.store.getSnapshot(), selectedSessions, invalid: shell.invalid || Boolean(error) || !shell.writable, fields, keyConfigured: this.keyConfigured(), error }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async save(): Promise<void> { if (!this.projection().invalid) await this.form.save() }
|
|
92
|
+
inject() {
|
|
93
|
+
return {
|
|
94
|
+
hooks: { diarySettings: this.store }, ...this.form.actions(), save: () => { void this.save() },
|
|
95
|
+
toggleSession: (id: string, enabled: boolean) => {
|
|
96
|
+
const ids = this.projection().selectedSessions
|
|
97
|
+
this.form.actions().edit('sessionIds', JSON.stringify(enabled ? validateSessionIds([...ids, id]) : ids.filter(item => item !== id)))
|
|
98
|
+
},
|
|
99
|
+
chooseWriter: (selection: string) => {
|
|
100
|
+
const [provider, model] = selection ? JSON.parse(selection) as [string, string] : ['', '']
|
|
101
|
+
this.form.actions().edit('diaryProvider', provider)
|
|
102
|
+
this.form.actions().edit('diaryModel', model)
|
|
103
|
+
},
|
|
104
|
+
refreshChoices: () => { void this.choices.refresh() },
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
dispose(): void { this.unsubscribeChoices(); this.choices.dispose(); this.form.dispose() }
|
|
108
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
2
|
+
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
3
|
+
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
|
4
|
+
import { DiarySettingsController } from './controller.js'
|
|
5
|
+
import type { FormValues } from './controller.js'
|
|
6
|
+
import { DiarySettingsPage } from './page.js'
|
|
7
|
+
import { DiaryChoices } from './choices.js'
|
|
8
|
+
import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client'
|
|
9
|
+
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
|
10
|
+
|
|
11
|
+
export { DiarySettingsController } from './controller.js'
|
|
12
|
+
export { DiarySettingsPage } from './page.js'
|
|
13
|
+
export const inject = ['slots', 'configForms', 'sessions', 'remote', 'remote.session']
|
|
14
|
+
|
|
15
|
+
export function apply(ctx: Context): void {
|
|
16
|
+
const sessions = ctx.get('sessions') as unknown as ISessions
|
|
17
|
+
const choices = new DiaryChoices(sessions.list, () => ctx.remote.session.modelCatalog(), () => sessions.refresh())
|
|
18
|
+
const controller = new DiarySettingsController(ctx.configForms.get<FormValues>('diary-memory'), () => {
|
|
19
|
+
const entry = ctx.configForms.describe().getSnapshot().view?.namespaces.find(item => item.ns === 'diary-memory')
|
|
20
|
+
return entry?.secrets?.some(item => item.path.length === 1 && item.path[0] === 'apiKey' && item.set) ?? false
|
|
21
|
+
}, choices)
|
|
22
|
+
ctx.effect(() => () => controller.dispose(), 'diary-memory: settings form')
|
|
23
|
+
ctx.effect(() => ctx.configForms.whileServed(['diary-memory'], () => ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
24
|
+
name: 'settings.section', id: 'diary-memory', order: 65, label: '陪伴日记', inject: () => controller.inject(),
|
|
25
|
+
}, DiarySettingsPage))), 'diary-memory: settings page')
|
|
26
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { SettingsForm, SettingsSecretField, SettingsValueField, Switch } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
2
|
+
import type { CSSProperties, ReactNode } from 'react'
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import type { InjectFace } from '@deepseek-ai/dsh-client-ui-slots'
|
|
5
|
+
import type { DiarySettingsController, EditableField } from './controller.js'
|
|
6
|
+
|
|
7
|
+
type PageProps = InjectFace<ReturnType<DiarySettingsController['inject']>>
|
|
8
|
+
const card: CSSProperties = { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 14, padding: '16px 20px', marginTop: 16, minWidth: 0 }
|
|
9
|
+
const hint: CSSProperties = { fontSize: 12, lineHeight: 1.6, color: 'var(--dsw-alias-label-tertiary)', margin: '8px 0 0' }
|
|
10
|
+
const textarea: CSSProperties = { display: 'block', width: '100%', boxSizing: 'border-box', minHeight: 110, padding: 12, borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-3)', color: 'var(--dsw-alias-label-primary)', fontFamily: 'var(--dsw-font-mono, monospace)', fontSize: 12, lineHeight: 1.6, resize: 'vertical' }
|
|
11
|
+
|
|
12
|
+
function Group({ title, children }: { title: string; children: ReactNode }) {
|
|
13
|
+
return <section aria-label={title} style={card}><h3 style={{ fontSize: 14, margin: '0 0 8px' }}>{title}</h3>{children}</section>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function DiarySettingsPage(props: PageProps) {
|
|
17
|
+
const state = props.useDiarySettings(value => value)
|
|
18
|
+
const disabled = !state.writable || state.saving
|
|
19
|
+
const [sessionFilter, setSessionFilter] = useState('')
|
|
20
|
+
const sessions = [...state.sessions, ...state.selectedSessions.filter(id => !state.sessions.some(item => item.id === id)).map(id => ({ id, title: '已选会话(不在当前列表)' }))]
|
|
21
|
+
.filter(item => `${item.title}\n${item.id}`.toLowerCase().includes(sessionFilter.toLowerCase()))
|
|
22
|
+
const writer = state.fields.diaryProvider.text && state.fields.diaryModel.text ? JSON.stringify([state.fields.diaryProvider.text, state.fields.diaryModel.text]) : ''
|
|
23
|
+
const valueField = (field: EditableField, label: string, description: string, numeric = false) => <SettingsValueField
|
|
24
|
+
key={field} id={`diary-${field}`} label={label} hint={description} numeric={numeric}
|
|
25
|
+
{...state.fields[field]} disabled={disabled} overriddenLabel="已自定义" resetLabel="恢复默认"
|
|
26
|
+
invalidLabel={numeric ? '请填写范围内的整数。' : '请检查填写的内容。'}
|
|
27
|
+
onEdit={text => props.edit(field, text)} onReset={() => props.resetField(field)}
|
|
28
|
+
/>
|
|
29
|
+
const parameters = (field: 'embeddingParameters' | 'rerankParameters', description: string) => <div style={{ padding: '12px 0' }}>
|
|
30
|
+
<label htmlFor={`diary-${field}`} style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 8 }}>额外参数(JSON)</label>
|
|
31
|
+
<textarea id={`diary-${field}`} aria-describedby={`diary-${field}-hint`} aria-invalid={state.fields[field].invalid || undefined}
|
|
32
|
+
value={state.fields[field].text} onChange={event => props.edit(field, event.target.value)} disabled={disabled}
|
|
33
|
+
spellCheck={false} rows={5} style={textarea} />
|
|
34
|
+
<p id={`diary-${field}-hint`} style={hint}>{description}</p>
|
|
35
|
+
</div>
|
|
36
|
+
return <div data-diary-settings style={{ color: 'var(--dsw-alias-label-primary)', paddingBottom: 16 }}>
|
|
37
|
+
<h2 style={{ fontSize: 18, margin: '0 0 8px' }}>陪伴日记</h2>
|
|
38
|
+
<p style={hint}>设置百炼连接、回忆模型与日记压缩方式。填写后点击保存。</p>
|
|
39
|
+
<SettingsForm state={state} onSave={props.save} onDiscard={props.discard} labels={{
|
|
40
|
+
unavailable: '正在加载配置;若持续不可用,请检查陪伴日记插件是否已启用。', readOnly: '当前配置只读。',
|
|
41
|
+
saveFailed: '保存失败或配置已在其他窗口修改。输入已保留,请检查后重试。', save: '保存配置', saving: '正在保存…',
|
|
42
|
+
}}>
|
|
43
|
+
<Group title="指定已有会话">
|
|
44
|
+
<p style={hint}>勾选后保存,在原会话继续聊天即可。保留原人设、模型与工具;未选会话保持原来的压缩方式。“陪伴日记”预设本身已启用记忆。</p>
|
|
45
|
+
<label htmlFor="diary-session-filter" style={{ display: 'block', fontSize: 13, marginTop: 12 }}>查找会话</label>
|
|
46
|
+
<input id="diary-session-filter" type="search" value={sessionFilter} onChange={event => setSessionFilter(event.target.value)}
|
|
47
|
+
placeholder="按会话标题或 ID 搜索" style={{ ...textarea, minHeight: 0, height: 36, marginTop: 6 }} />
|
|
48
|
+
<div aria-label="已有会话列表" style={{ maxHeight: 300, overflowY: 'auto', marginTop: 10 }}>
|
|
49
|
+
{sessions.map(item => <div key={item.id} style={{ display: 'flex', gap: 12, alignItems: 'center', padding: '10px 0', borderBottom: '1px solid var(--dsw-alias-border-l2)' }}>
|
|
50
|
+
<div style={{ minWidth: 0, flex: 1 }}><div style={{ fontSize: 13, overflowWrap: 'anywhere' }}>{item.title}</div><div style={{ ...hint, margin: 0, overflowWrap: 'anywhere' }}>{item.id}</div></div>
|
|
51
|
+
<Switch label={`为 ${item.title} 启用记忆`} checked={state.selectedSessions.includes(item.id)} disabled={disabled} onChange={value => props.toggleSession(item.id, value)} />
|
|
52
|
+
</div>)}
|
|
53
|
+
{!sessions.length ? <p style={hint}>{state.sessionsReady ? '没有匹配的会话。' : '正在读取会话列表…'}</p> : null}
|
|
54
|
+
</div>
|
|
55
|
+
<p style={hint}>已选 {state.selectedSessions.length} 个会话。同一记忆空间中的已选会话共享日记;取消勾选不会删除已保存的记忆。</p>
|
|
56
|
+
<button type="button" onClick={props.refreshChoices} disabled={state.saving} style={{ marginTop: 10 }}>刷新会话与模型列表</button>
|
|
57
|
+
</Group>
|
|
58
|
+
<Group title="专用写日记模型">
|
|
59
|
+
<label htmlFor="diary-writer" style={{ display: 'block', fontSize: 13, marginBottom: 6 }}>选择已配置的 DSH 模型</label>
|
|
60
|
+
<select id="diary-writer" value={writer} disabled={disabled} onChange={event => props.chooseWriter(event.target.value)} style={{ ...textarea, minHeight: 0, height: 40, padding: '0 10px', fontFamily: 'inherit' }}>
|
|
61
|
+
<option value="">跟随当前会话的聊天模型</option>
|
|
62
|
+
{writer && !state.writers.some(item => JSON.stringify([item.provider, item.model]) === writer) ? <option value={writer}>当前手填模型:{state.fields.diaryProvider.text} / {state.fields.diaryModel.text}</option> : null}
|
|
63
|
+
{state.writers.map(item => <option key={JSON.stringify([item.provider, item.model])} value={JSON.stringify([item.provider, item.model])}>{item.label}</option>)}
|
|
64
|
+
</select>
|
|
65
|
+
<p style={hint}>专用模型只负责写日记,不改变聊天模型。先在 DSH 的模型设置里配置提供方,也可以在下面手填其路由名与模型名。</p>
|
|
66
|
+
{state.modelsStatus === 'error' ? <p style={hint}>模型列表暂不可用,已保存的模型仍保留,可手填或刷新重试。</p> : null}
|
|
67
|
+
{valueField('diaryProvider', '写日记的模型提供方', '填写 DSH 已注册的 provider 路由名;与模型名同时留空则跟随聊天模型。')}
|
|
68
|
+
{valueField('diaryModel', '写日记的模型名', '填写该提供方支持的生成模型。这里只设置模型,不增加生成参数。')}
|
|
69
|
+
</Group>
|
|
70
|
+
<Group title="百炼连接">
|
|
71
|
+
<SettingsSecretField id="diary-apiKey" label="百炼 API Key" hint="密钥不会回显;留空保留已保存的 Key。也可通过 DSH 的环境变量提供。"
|
|
72
|
+
text={state.fields.apiKey.text} configured={state.keyConfigured} stateLabel={state.keyConfigured ? '已保存(不回显)' : '尚未保存'}
|
|
73
|
+
disabled={disabled} onEdit={text => props.edit('apiKey', text)} />
|
|
74
|
+
{valueField('baseUrl', '百炼 API 地址', '需要与 API Key 的地域一致,例如 https://dashscope.aliyuncs.com。')}
|
|
75
|
+
</Group>
|
|
76
|
+
<Group title="Embedding · 向量召回">
|
|
77
|
+
{valueField('embeddingModel', 'Embedding 模型名', '可手动填写百炼支持的文本向量模型,例如 text-embedding-v4。')}
|
|
78
|
+
{valueField('embeddingDimensions', '向量维度', '1–4096;请填写所选模型支持的维度。', true)}
|
|
79
|
+
{parameters('embeddingParameters', '填写该模型支持的 parameters,例如 {"output_type":"dense"}。dimension 在上方填写,text_type 由入库与召回自动设置。')}
|
|
80
|
+
<p style={hint}>更改 Embedding 模型、维度或参数后,旧向量不会参与新配置的检索。</p>
|
|
81
|
+
</Group>
|
|
82
|
+
<Group title="Rerank · 回忆排序">
|
|
83
|
+
{valueField('rerankModel', 'Rerank 模型名', '可手动填写百炼支持的重排模型,例如 gte-rerank-v2。')}
|
|
84
|
+
{valueField('candidateCount', '候选数量', '1–100;先由向量检索选出这些日记交给 Rerank。', true)}
|
|
85
|
+
{valueField('recallCount', '召回条数(top_n)', '1–100,且不大于候选数量。', true)}
|
|
86
|
+
{parameters('rerankParameters', '填写该模型支持的 parameters,例如 {"return_documents":false}。top_n 由上方的召回条数设置。')}
|
|
87
|
+
</Group>
|
|
88
|
+
<Group title="日记与陪伴">
|
|
89
|
+
{valueField('compressionRounds', '触发压缩的轮次', '完成多少轮后保存日记并安全压缩;须大于保留轮次。', true)}
|
|
90
|
+
{valueField('retainRounds', '保留最近轮次', '至少保留最近多少个用户输入及后续消息。', true)}
|
|
91
|
+
<div style={{ display: 'flex', gap: 10, justifyContent: 'space-between', alignItems: 'center', padding: '12px 0', fontSize: 13 }}>
|
|
92
|
+
<span>自动回想相关日记</span>
|
|
93
|
+
<Switch label="自动回想相关日记" checked={state.fields.autoRecall.text === 'true'} disabled={disabled}
|
|
94
|
+
onChange={value => props.edit('autoRecall', String(value))} />
|
|
95
|
+
</div>
|
|
96
|
+
</Group>
|
|
97
|
+
{state.error ? <p role="alert" style={{ ...hint, color: 'var(--dsw-alias-label-error)' }}>{state.error}</p> : null}
|
|
98
|
+
</SettingsForm>
|
|
99
|
+
</div>
|
|
100
|
+
}
|