22 * The Bailian page's controller: a hybrid form over two domains.
33 *
44 * The workspace, default-retrieval-service and default-chat-service ids live
5- * in the `bailian-kb` settings section the Host half registers, so while the
6- * settings scope is `ready` they ECHO: the page shows the resolved value and
7- * stages edits over it (clearing a field unsets the user layer, falling back
8- * to the entry config and then the credential store). When the scope is
9- * unavailable — a remote browser (memory mode) or a composition without a
10- * settings service — both fields degrade to the original write-only credential
11- * controls.
5+ * in the `bailian-kb` settings section the Host half registers. The Host
6+ * exposes them over a bridge route (`/bailian-kb/settings`) so the page can
7+ * read and write without riding the settings wire (which requires an apiproxy
8+ * allowlist entry the composition does not grant out-of-tree namespaces).
129 *
1310 * The API key always rides its credential reference (write-only by design:
1411 * the wire is structurally value-free), so that control starts blank and
1512 * reports only configured/unconfigured.
1613 */
1714
1815import type { IApiClient } from '@deepseek-ai/dsh-client-connection/client'
19- import { createSnapshotStore , type SettingsScope , type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
16+ import { createSnapshotStore , type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
2017
2118/** The credential references this page addresses, keyed by their ref names. */
2219export const BAILIAN_CARD_REFS = [
@@ -120,17 +117,15 @@ export function dirtyOf(state: BailianCardState): boolean {
120117 return BAILIAN_CARD_REFS . some ( key => staged ( state , key ) )
121118}
122119
123- /** Bridge the settings scope and the credentials domain onto the page. */
120+ /** Bridge the settings bridge route and the credentials domain onto the page. */
124121export class BailianCardController {
125122 private readonly store : SnapshotStore < BailianCardState >
126123
127124 /**
128125 * @param api - wire face used for the three credential references.
129- * @param scope - the bound `bailian-kb` settings scope (echo transport).
130126 */
131127 constructor (
132128 private readonly api : Pick < IApiClient , 'credentials' > ,
133- private readonly scope : SettingsScope < BailianKbSection > ,
134129 ) {
135130 this . store = createSnapshotStore < BailianCardState > ( {
136131 drafts : {
@@ -150,29 +145,35 @@ export class BailianCardController {
150145 clearing : false ,
151146 failed : false ,
152147 } )
153- this . syncSettings ( )
148+ void this . fetchSettings ( )
154149 void this . read ( )
155150 }
156151
157152 /**
158- * Mirror the scope snapshot into the page state. Called at construction and
159- * from the registration-side subscription (the scope self-refreshes on
160- * pushed document invalidations and connection resets).
153+ * Fetch the current settings from the Host bridge route. Called at
154+ * construction and after every mutation (save, clear).
161155 */
162- syncSettings ( ) : void {
163- const snapshot = this . scope . getSnapshot ( )
164- const value = snapshot . value
165- this . store . update ( draft => {
166- draft . settings = {
167- status : snapshot . status ,
168- writable : snapshot . writable ,
169- values : {
170- ...( value ?. workspaceId !== undefined ? { workspaceId : value . workspaceId } : { } ) ,
171- ...( value ?. defaultRetrieveAgentId !== undefined ? { defaultRetrieveAgentId : value . defaultRetrieveAgentId } : { } ) ,
172- ...( value ?. defaultChatAgentId !== undefined ? { defaultChatAgentId : value . defaultChatAgentId } : { } ) ,
173- } ,
174- }
175- } )
156+ async fetchSettings ( ) : Promise < void > {
157+ try {
158+ const resp = await fetch ( '/bailian-kb/settings' )
159+ if ( ! resp . ok ) throw new Error ( `HTTP ${ resp . status } ` )
160+ const value = await resp . json ( ) as Record < string , unknown >
161+ this . store . update ( draft => {
162+ draft . settings = {
163+ status : 'ready' ,
164+ writable : true ,
165+ values : {
166+ ...( typeof value . workspaceId === 'string' ? { workspaceId : value . workspaceId } : { } ) ,
167+ ...( typeof value . defaultRetrieveAgentId === 'string' ? { defaultRetrieveAgentId : value . defaultRetrieveAgentId } : { } ) ,
168+ ...( typeof value . defaultChatAgentId === 'string' ? { defaultChatAgentId : value . defaultChatAgentId } : { } ) ,
169+ } ,
170+ }
171+ } )
172+ } catch ( _fetchFailure ) {
173+ this . store . update ( draft => {
174+ draft . settings = { status : 'unavailable' , writable : false , values : { } }
175+ } )
176+ }
176177 }
177178
178179 /**
@@ -190,28 +191,31 @@ export class BailianCardController {
190191
191192 /**
192193 * Write every staged draft through its domain: echoing fields go to the
193- * settings user layer (blank = unset , falling back to entry config and the
194- * credential store), write-only fields go to `credentials.set`. A refused
195- * credential write keeps its draft; a refused settings write self-heals by
196- * the scope's own recovery read ( the control snaps back to the Host value) .
194+ * settings user layer via the bridge route (blank = removal , falling back
195+ * to entry config and the credential store), write-only fields go to
196+ * `credentials.set`. A refused credential write keeps its draft; a refused
197+ * settings write self-heals by re-fetching the Host value.
197198 */
198199 async save ( ) : Promise < void > {
199200 const state = this . store . getSnapshot ( )
200201 if ( state . saving || ! dirtyOf ( state ) ) return
201202 this . store . update ( draft => { draft . saving = true } )
202203 let failed = false
203- const writes : Promise < void > [ ] = [ ]
204+ const settingsPatch : Record < string , unknown > = { }
205+ let hasSettingsWrite = false
206+ const credentialWrites : Promise < void > [ ] = [ ]
204207 const settled : BailianFieldKey [ ] = [ ]
205208 for ( const key of BAILIAN_CARD_REFS ) {
206209 if ( ! staged ( state , key ) ) continue
207210 const text = state . drafts [ key ] as string
208211 const field = SETTINGS_FIELDS [ key ]
209212 if ( field !== undefined && state . settings . status === 'ready' ) {
210- writes . push ( text === '' ? this . scope . unset ( field ) : this . scope . set ( field , text ) )
213+ settingsPatch [ field ] = text === '' ? null : text
214+ hasSettingsWrite = true
211215 settled . push ( key )
212216 continue
213217 }
214- writes . push ( ( async ( ) => {
218+ credentialWrites . push ( ( async ( ) => {
215219 try {
216220 const response = await this . api . credentials . set ( { ref : key , value : text } )
217221 if ( response . result . ok ) settled . push ( key )
@@ -221,13 +225,20 @@ export class BailianCardController {
221225 }
222226 } ) ( ) )
223227 }
224- await Promise . all ( writes )
228+ if ( hasSettingsWrite ) {
229+ try {
230+ await this . saveSettings ( settingsPatch )
231+ } catch ( _settingsWriteFailure ) {
232+ failed = true
233+ }
234+ }
235+ await Promise . all ( credentialWrites )
225236 this . store . update ( draft => {
226237 draft . saving = false
227238 draft . failed = failed
228239 for ( const key of settled ) draft . drafts [ key ] = undefined
229240 } )
230- this . syncSettings ( )
241+ await this . fetchSettings ( )
231242 await this . read ( )
232243 }
233244
@@ -252,7 +263,13 @@ export class BailianCardController {
252263 if ( state . clearing ) return
253264 this . store . update ( draft => { draft . clearing = true } )
254265 let failed = false
255- if ( state . settings . status === 'ready' ) await this . scope . unset ( settingsField )
266+ if ( state . settings . status === 'ready' ) {
267+ try {
268+ await this . saveSettings ( { [ settingsField ] : null } )
269+ } catch ( _settingsWriteFailure ) {
270+ failed = true
271+ }
272+ }
256273 if ( state . credentials [ key ] . configured ) {
257274 try {
258275 const response = await this . api . credentials . unset ( { ref : key } )
@@ -266,7 +283,7 @@ export class BailianCardController {
266283 draft . failed = failed
267284 draft . drafts [ key ] = undefined
268285 } )
269- this . syncSettings ( )
286+ await this . fetchSettings ( )
270287 await this . read ( )
271288 }
272289
@@ -298,6 +315,24 @@ export class BailianCardController {
298315 }
299316 }
300317
318+ /**
319+ * Send a settings patch to the Host bridge route. `null`-valued keys are
320+ * removals (the field falls back to the entry config and then the
321+ * credential store); other values are merged into the user layer.
322+ * @param patch - the partial settings update.
323+ */
324+ private async saveSettings ( patch : Record < string , unknown > ) : Promise < void > {
325+ const resp = await fetch ( '/bailian-kb/settings' , {
326+ method : 'POST' ,
327+ headers : { 'Content-Type' : 'application/json' } ,
328+ body : JSON . stringify ( patch ) ,
329+ } )
330+ if ( ! resp . ok ) {
331+ const body = await resp . json ( ) . catch ( ( ) => ( { } ) ) as { error ?: string }
332+ throw new Error ( body . error ?? `HTTP ${ resp . status } ` )
333+ }
334+ }
335+
301336 /**
302337 * Ask the credentials domain about all three references and publish the
303338 * answer. A failed read keeps the last known state: the page stays usable
0 commit comments