@lemoncat7/dsh-knowledge 1.0.11 → 1.0.14

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/web/app.js CHANGED
@@ -64,6 +64,7 @@ const state = {
64
64
  candidateTargets: new Map(),
65
65
  candidateStatus: 'pending',
66
66
  settings: { writebackPolicy: 'conservative', updatedAt: '' },
67
+ modelCatalog: null,
67
68
  settingsSaving: false,
68
69
  tokens: [],
69
70
  service: { publicApiEnabled: false, publicApiPrefix: '/knowledge-api/v1', remote: false },
@@ -348,16 +349,17 @@ async function ensureKnowledgeBases(force = false) {
348
349
  }
349
350
 
350
351
  async function loadKnowledgeBasesPage() {
351
- const requests = [api('knowledge-bases'), api('mounts'), api('settings')]
352
+ const requests = [api('knowledge-bases'), api('mounts'), api('settings'), api('service')]
352
353
  if (state.mountContext.sessionId) {
353
354
  const params = new URLSearchParams({ sessionId: state.mountContext.sessionId })
354
355
  if (state.mountContext.projectId) params.set('projectId', state.mountContext.projectId)
355
356
  requests.push(api(`mounts/resolve?${params}`))
356
357
  }
357
- const [bases, mounts, settings, resolved = []] = await Promise.all(requests)
358
+ const [bases, mounts, settings, service, resolved = []] = await Promise.all(requests)
358
359
  state.knowledgeBases = bases
359
360
  state.mounts = mounts
360
361
  state.settings = settings
362
+ state.service = service
361
363
  state.resolvedMounts = resolved
362
364
  await refreshStats()
363
365
  }
@@ -751,7 +753,7 @@ function renderKnowledgeBases() {
751
753
  const archivedBases = state.knowledgeBases.filter(base => base.status === 'archived')
752
754
  const contextAvailable = Boolean(state.mountContext.projectId || state.mountContext.sessionId)
753
755
  const query = state.knowledgeBaseQuery.trim().toLocaleLowerCase()
754
- const matchesQuery = base => !query || [base.name, base.description, base.defaultTags.join(' '), base.writebackProvider, base.writebackModel]
756
+ const matchesQuery = base => !query || [base.name, base.description, base.defaultTags.join(' ')]
755
757
  .some(value => String(value || '').toLocaleLowerCase().includes(query))
756
758
  const visibleActiveBases = activeBases.filter(matchesQuery)
757
759
  const visibleArchivedBases = archivedBases.filter(matchesQuery)
@@ -764,11 +766,11 @@ function renderKnowledgeBases() {
764
766
  type: 'button', role: 'tab', class: 'tab', 'aria-selected': String(state.knowledgeBaseView === id),
765
767
  onClick: () => { state.knowledgeBaseView = id; renderShell() },
766
768
  }, element('span', {}, label), element('span', { class: 'tab-count' }, count)))),
767
- actionButton('默认回写模型', () => openGlobalWritebackModelEditor(), 'ghost small'),
769
+ actionButton('本机回写模型', () => openGlobalWritebackModelEditor(), 'ghost small'),
768
770
  ),
769
- element('p', {}, state.settings.writebackProvider && state.settings.writebackModel
770
- ? `未单独指定模型的知识库使用 ${state.settings.writebackProvider} / ${state.settings.writebackModel}。`
771
- : '未单独指定模型的知识库跟随当前会话模型。'),
771
+ element('p', {}, state.service.writebackProvider && state.service.writebackModel
772
+ ? `仅当前客户端使用 ${state.service.writebackProvider} / ${state.service.writebackModel} 回写。`
773
+ : '当前客户端跟随每轮会话模型回写。'),
772
774
  )
773
775
  return element('div', { class: 'bases-page' },
774
776
  switcher,
@@ -780,7 +782,7 @@ function renderKnowledgeBases() {
780
782
  element('div', { class: 'knowledge-base-toolbar' },
781
783
  element('div', { class: 'search-box base-search' }, interfaceIcon('search', 'search-symbol'), element('input', {
782
784
  class: 'input', type: 'search', value: state.knowledgeBaseQuery,
783
- placeholder: '搜索名称、描述、标签或模型', 'aria-label': '搜索知识库',
785
+ placeholder: '搜索名称、描述或标签', 'aria-label': '搜索知识库',
784
786
  onInput: event => {
785
787
  state.knowledgeBaseQuery = event.target.value
786
788
  renderShell()
@@ -794,7 +796,7 @@ function renderKnowledgeBases() {
794
796
  visibleActiveBases.length
795
797
  ? element('div', { class: 'base-grid' }, visibleActiveBases.map(renderKnowledgeBaseCard))
796
798
  : query && visibleArchivedBases.length === 0
797
- ? emptyState('没有匹配的知识库', '尝试搜索名称、描述、标签或回写模型。')
799
+ ? emptyState('没有匹配的知识库', '尝试搜索名称、描述或标签。')
798
800
  : !query ? emptyState('还没有可用知识库', '使用右上角按钮创建第一个知识库。') : null,
799
801
  visibleArchivedBases.length ? element('details', { class: 'archived-bases', open: Boolean(query) },
800
802
  element('summary', {}, element('span', {}, '已归档知识库'), element('span', { class: 'summary-count' }, visibleArchivedBases.length)),
@@ -818,22 +820,22 @@ function renderKnowledgeBases() {
818
820
  )
819
821
  }
820
822
 
821
- function openGlobalWritebackModelEditor() {
822
- const custom = element('input', { type: 'checkbox', checked: Boolean(state.settings.writebackProvider && state.settings.writebackModel) })
823
- const provider = formField('模型提供方(Provider)', 'input', state.settings.writebackProvider || '', { maxlength: 100 })
824
- const model = formField('模型名称(Model)', 'input', state.settings.writebackModel || '', { maxlength: 200 })
823
+ async function openGlobalWritebackModelEditor() {
824
+ await loadModelCatalog()
825
+ const custom = element('input', { type: 'checkbox', checked: Boolean(state.service.writebackProvider && state.service.writebackModel) })
826
+ const route = modelRouteFields(state.service.writebackProvider || '', state.service.writebackModel || '')
825
827
  const form = element('form', { class: 'form-grid' },
826
- element('label', { class: 'check-option span-2' }, custom, element('span', {}, element('strong', {}, '使用全局默认模型'), element('small', {}, '关闭后,未单独配置的知识库跟随当前会话模型。'))),
827
- provider.wrapper, model.wrapper,
828
+ element('label', { class: 'check-option span-2' }, custom, element('span', {}, element('strong', {}, '当前客户端使用专用模型'), element('small', {}, '关闭后,跟随当前客户端每轮会话实际使用的模型。'))),
829
+ route.provider.wrapper, route.model.wrapper,
828
830
  )
829
- const sync = () => { provider.input.disabled = model.input.disabled = !custom.checked; provider.input.required = model.input.required = custom.checked }
831
+ const sync = () => { route.provider.input.disabled = route.model.input.disabled = !custom.checked; route.provider.input.required = route.model.input.required = custom.checked }
830
832
  custom.addEventListener('change', sync); sync()
831
- openSheet({ title: '默认回写模型', description: '知识库自己的模型优先于这里;这里留空则跟随当前会话。', body: form, primaryLabel: '保存', onPrimary: async () => {
833
+ openSheet({ title: '本机回写模型', description: '此设置只保存在当前 DSH 客户端,不会写入中央知识库或影响其他设备。', body: form, primaryLabel: '保存', onPrimary: async () => {
832
834
  if (!form.reportValidity()) return false
833
- state.settings = await api('settings', { method: 'PUT', body: { patch: custom.checked
834
- ? { writebackProvider: provider.input.value.trim(), writebackModel: model.input.value.trim() }
835
- : { writebackProvider: null, writebackModel: null } } })
836
- showToast('默认回写模型已更新。'); renderShell(); return true
835
+ state.service = await api('service', { method: 'PUT', body: custom.checked
836
+ ? { writebackProvider: route.provider.input.value, writebackModel: route.model.input.value }
837
+ : { writebackProvider: null, writebackModel: null } })
838
+ showToast('当前客户端的回写模型已更新。'); renderShell(); return true
837
839
  } })
838
840
  }
839
841
 
@@ -860,7 +862,7 @@ function renderKnowledgeBaseCard(base) {
860
862
  hiddenTagCount ? element('span', { class: 'tag' }, `+${hiddenTagCount}`) : null),
861
863
  element('div', { class: 'base-card-meta' },
862
864
  element('span', {}, element('strong', {}, '回写策略'), base.writebackPolicy === 'proactive' ? '主动' : '严谨'),
863
- element('span', {}, element('strong', {}, '回写模型'), base.writebackProvider && base.writebackModel ? `${base.writebackProvider} / ${base.writebackModel}` : state.settings.writebackProvider && state.settings.writebackModel ? `默认 · ${state.settings.writebackProvider} / ${state.settings.writebackModel}` : '跟随当前会话'),
865
+ element('span', {}, element('strong', {}, '回写模型'), state.service.writebackProvider && state.service.writebackModel ? `本机 · ${state.service.writebackProvider} / ${state.service.writebackModel}` : '跟随当前会话'),
864
866
  base.extractionInstructions ? element('span', {}, element('strong', {}, '提取规则'), '已设置') : null,
865
867
  ),
866
868
  element('div', { class: 'base-card-actions' },
@@ -920,15 +922,14 @@ function renderMountManager(activeBases) {
920
922
  const query = manager.query.trim().toLowerCase()
921
923
  const rows = activeBases.map(base => ({ base, view: mountView(base, manager.targetKind, targetId) }))
922
924
  const visibleRows = rows.filter(({ base, view }) => {
923
- const model = base.writebackProvider && base.writebackModel ? `${base.writebackProvider} ${base.writebackModel}` : '跟随会话'
924
- const searchable = `${base.name} ${base.description} ${base.defaultTags.join(' ')} ${model}`.toLowerCase()
925
+ const searchable = `${base.name} ${base.description} ${base.defaultTags.join(' ')}`.toLowerCase()
925
926
  return (!query || searchable.includes(query)) && (manager.filter === 'all' || manager.filter === view.statusKey)
926
927
  })
927
928
  const visibleIds = visibleRows.map(({ base }) => base.id)
928
929
  const selectedCount = manager.selectedIds.size
929
930
  const searchInput = element('input', {
930
931
  class: 'input', type: 'search', value: manager.query,
931
- placeholder: '搜索名称、描述、标签或模型', 'aria-label': '搜索可挂载知识库',
932
+ placeholder: '搜索名称、描述或标签', 'aria-label': '搜索可挂载知识库',
932
933
  onInput: (event) => { manager.query = event.target.value; renderShell() },
933
934
  })
934
935
  const filter = selectControl('筛选挂载状态', [
@@ -982,8 +983,8 @@ function renderMountListRow(base, view, targetKind, targetId) {
982
983
  renderShell()
983
984
  },
984
985
  })
985
- const modelLabel = base.writebackProvider && base.writebackModel
986
- ? `${base.writebackProvider} / ${base.writebackModel}`
986
+ const modelLabel = state.service.writebackProvider && state.service.writebackModel
987
+ ? `本机 · ${state.service.writebackProvider} / ${state.service.writebackModel}`
987
988
  : '跟随当前会话模型'
988
989
  return element('article', { class: `mount-list-row${selected ? ' is-selected' : ''}`, role: 'listitem' },
989
990
  element('label', { class: 'mount-select' }, checkbox, element('span', { class: 'visually-hidden' }, `选择 ${base.name}`)),
@@ -1410,26 +1411,9 @@ function openKnowledgeBaseEditor(base) {
1410
1411
  { value: 'conservative', label: '严谨(高置信度直写)' },
1411
1412
  { value: 'proactive', label: '主动(更积极沉淀)' },
1412
1413
  ], source.writebackPolicy || 'conservative')
1413
- const customModel = element('input', { type: 'checkbox', checked: Boolean(source.writebackProvider && source.writebackModel) })
1414
- const provider = formField('模型提供方(Provider)', 'input', source.writebackProvider || '', { maxlength: 100, placeholder: '例如:cli' })
1415
- const model = formField('模型名称(Model)', 'input', source.writebackModel || '', { maxlength: 200, placeholder: '例如:kimi-k2.7-code' })
1416
- const modelHint = element('div', { class: 'field-hint span-2' }, '默认跟随当前会话模型。指定后,仅这个知识库使用专用模型回写。')
1417
- const updateModelFields = () => {
1418
- provider.input.disabled = !customModel.checked
1419
- model.input.disabled = !customModel.checked
1420
- provider.input.required = customModel.checked
1421
- model.input.required = customModel.checked
1422
- }
1423
- customModel.addEventListener('change', updateModelFields)
1424
- updateModelFields()
1425
1414
  for (const field of [name, description, tags, instructions]) field.wrapper.classList.add('span-2')
1426
1415
  form.append(
1427
1416
  name.wrapper, description.wrapper, tags.wrapper, instructions.wrapper, policy.wrapper,
1428
- element('label', { class: 'check-option span-2' }, customModel, element('span', {},
1429
- element('strong', {}, '为这个知识库指定回写模型'),
1430
- element('small', {}, '关闭时自动使用当前会话正在使用的模型。'),
1431
- )),
1432
- provider.wrapper, model.wrapper, modelHint,
1433
1417
  )
1434
1418
  openSheet({
1435
1419
  title: base ? '编辑知识库' : '创建知识库',
@@ -1444,8 +1428,6 @@ function openKnowledgeBaseEditor(base) {
1444
1428
  defaultTags: parseTags(tags.input.value),
1445
1429
  extractionInstructions: instructions.input.value.trim(),
1446
1430
  writebackPolicy: policy.input.value,
1447
- writebackProvider: customModel.checked ? provider.input.value.trim() : '',
1448
- writebackModel: customModel.checked ? model.input.value.trim() : '',
1449
1431
  }
1450
1432
  if (base) await api(`knowledge-bases/${encodeURIComponent(base.id)}`, { method: 'PUT', body: { draft } })
1451
1433
  else await api('knowledge-bases', { method: 'POST', body: { draft } })
@@ -1741,6 +1723,41 @@ function selectField(label, options, value) {
1741
1723
  return { input, wrapper: element('div', { class: 'field' }, element('label', {}, label), input) }
1742
1724
  }
1743
1725
 
1726
+ async function loadModelCatalog() {
1727
+ if (state.modelCatalog !== null) return state.modelCatalog
1728
+ try {
1729
+ const response = await fetch('/knowledge-control/v1/models', { headers: { accept: 'application/json' } })
1730
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
1731
+ const payload = await response.json()
1732
+ state.modelCatalog = Array.isArray(payload.providers) ? payload.providers : []
1733
+ } catch (error) {
1734
+ state.modelCatalog = []
1735
+ showToast(`无法读取当前 DSH 的模型目录:${error instanceof Error ? error.message : String(error)}`, 'error')
1736
+ }
1737
+ return state.modelCatalog
1738
+ }
1739
+
1740
+ function modelRouteFields(currentProvider, currentModel) {
1741
+ const providers = [...(state.modelCatalog || [])]
1742
+ if (currentProvider && !providers.some(item => item.id === currentProvider)) {
1743
+ providers.push({ id: currentProvider, name: `${currentProvider}(当前配置)`, models: [] })
1744
+ }
1745
+ const provider = selectField('模型提供方', providers.map(item => ({ value: item.id, label: item.name || item.id })), currentProvider || providers[0]?.id || '')
1746
+ const model = selectField('回写模型', [], '')
1747
+ const refreshModels = preferred => {
1748
+ const selected = providers.find(item => item.id === provider.input.value)
1749
+ const models = [...(selected?.models || [])]
1750
+ if (preferred && !models.some(item => item.id === preferred)) models.push({ id: preferred, name: `${preferred}(当前配置)` })
1751
+ model.input.replaceChildren(...models.map(item => element('option', {
1752
+ value: item.id, selected: item.id === preferred,
1753
+ }, item.name && item.name !== item.id ? `${item.name} · ${item.id}` : item.id)))
1754
+ if (preferred && models.some(item => item.id === preferred)) model.input.value = preferred
1755
+ }
1756
+ provider.input.addEventListener('change', () => refreshModels(''))
1757
+ refreshModels(currentModel)
1758
+ return { provider, model }
1759
+ }
1760
+
1744
1761
  async function reviewCandidate(candidate, decision, resolution) {
1745
1762
  const approve = decision === 'approve'
1746
1763
  openConfirm({