@lemoncat7/dsh-knowledge 0.6.0-alpha.7 → 0.6.0-alpha.8

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
@@ -1,4 +1,5 @@
1
1
  const API_BASE = document.querySelector('meta[name="dsh-knowledge-api"]')?.content || '/knowledge-api/v1'
2
+ const AUTH_MODE = document.querySelector('meta[name="dsh-knowledge-auth-mode"]')?.content || 'bearer'
2
3
  const TOKEN_KEY = 'dsh-knowledge.session-token'
3
4
  const TYPES = ['preference', 'fact', 'decision', 'procedure', 'lesson']
4
5
  const TYPE_LABELS = { preference: '偏好', fact: '事实', decision: '决策', procedure: '流程', lesson: '经验' }
@@ -51,6 +52,7 @@ const state = {
51
52
  candidates: [],
52
53
  candidateStatus: 'pending',
53
54
  tokens: [],
55
+ service: { publicApiEnabled: false, publicApiPrefix: '/knowledge-api/v1' },
54
56
  loading: false,
55
57
  error: '',
56
58
  }
@@ -132,6 +134,7 @@ function showToast(message, kind = '') {
132
134
 
133
135
  async function api(path, options = {}) {
134
136
  const headers = { accept: 'application/json', ...(options.body === undefined ? {} : { 'content-type': 'application/json' }) }
137
+ if (AUTH_MODE === 'same-origin') headers['x-dsh-knowledge-client'] = 'management-web'
135
138
  if (state.token) headers.authorization = `Bearer ${state.token}`
136
139
  const response = await fetch(`${API_BASE}/${path.replace(/^\/+/, '')}`, {
137
140
  method: options.method || 'GET',
@@ -153,6 +156,11 @@ async function api(path, options = {}) {
153
156
  }
154
157
 
155
158
  async function boot() {
159
+ if (AUTH_MODE === 'same-origin') {
160
+ state.token = ''
161
+ await navigate('overview')
162
+ return
163
+ }
156
164
  if (!state.token) {
157
165
  renderLogin()
158
166
  return
@@ -213,7 +221,8 @@ function renderLogin(message = '') {
213
221
  function signOut() {
214
222
  sessionStorage.removeItem(TOKEN_KEY)
215
223
  Object.assign(state, { token: '', stats: null, overview: null, knowledgeBases: [], mounts: [], resolvedMounts: [], entries: [], documents: [], candidates: [], tokens: [] })
216
- renderLogin()
224
+ if (AUTH_MODE === 'same-origin') void boot()
225
+ else renderLogin()
217
226
  }
218
227
 
219
228
  async function navigate(view) {
@@ -329,7 +338,9 @@ async function loadCandidates() {
329
338
  }
330
339
 
331
340
  async function loadTokens() {
332
- state.tokens = await api('tokens')
341
+ const [tokens, service] = await Promise.all([api('tokens'), api('service')])
342
+ state.tokens = tokens
343
+ state.service = service
333
344
  if (!state.stats) await refreshStats()
334
345
  }
335
346
 
@@ -448,7 +459,7 @@ function renderSidebar() {
448
459
  id === 'candidates' && pending ? element('span', { class: 'nav-count', 'aria-label': `${pending} 条待审核` }, pending) : null))),
449
460
  element('div', { class: 'sidebar-footer' },
450
461
  element('div', { class: 'connection' }, element('span', { class: 'status-dot', 'aria-hidden': 'true' }), '知识库已连接'),
451
- actionButton('退出当前会话', signOut, 'ghost small'),
462
+ AUTH_MODE === 'bearer' ? actionButton('退出当前会话', signOut, 'ghost small') : null,
452
463
  ),
453
464
  )
454
465
  }
@@ -1145,7 +1156,25 @@ function renderCandidateCard(candidate) {
1145
1156
  }
1146
1157
 
1147
1158
  function renderTokens() {
1159
+ const apiAddress = new URL(state.service.publicApiPrefix, location.origin).href.replace(/\/$/, '')
1148
1160
  return element('section', { 'aria-labelledby': 'tokens-heading' },
1161
+ element('div', { class: 'api-access-card' },
1162
+ element('div', { class: 'api-access-heading' },
1163
+ element('div', {}, element('strong', {}, '远程知识库 API'), element('p', {}, state.service.publicApiEnabled
1164
+ ? '其他 DSH 客户端可以使用下面的地址和客户端令牌连接。'
1165
+ : '当前仅能在本机管理;开启后其他 DSH 客户端才能连接。')),
1166
+ badge(state.service.publicApiEnabled ? '已开放' : '未开放', state.service.publicApiEnabled ? 'success' : 'warning'),
1167
+ ),
1168
+ element('label', { class: 'api-address-label', for: 'knowledge-public-api-address' }, '客户端填写的服务器地址'),
1169
+ element('div', { class: 'api-address-row' },
1170
+ Object.assign(element('input', { id: 'knowledge-public-api-address', class: 'input', readonly: true, value: apiAddress }), { value: apiAddress }),
1171
+ actionButton('复制地址', () => copyText(apiAddress, 'API 地址已复制。'), 'small'),
1172
+ ),
1173
+ element('div', { class: 'api-access-actions' },
1174
+ element('small', {}, state.service.publicApiEnabled ? '关闭后已有客户端会立即无法连接,本地管理不受影响。' : '开启远程 API 后,请为每台客户端创建独立令牌。'),
1175
+ actionButton(state.service.publicApiEnabled ? '关闭远程 API' : '开启远程 API', togglePublicApi, state.service.publicApiEnabled ? 'danger small' : 'primary small'),
1176
+ ),
1177
+ ),
1149
1178
  element('div', { class: 'section-heading' },
1150
1179
  element('div', {}, element('h2', { id: 'tokens-heading' }, '客户端访问令牌'), element('p', {}, '给其他 DSH 客户端分配最小必要权限;原始令牌只显示一次。')),
1151
1180
  actionButton('+ 创建令牌', openTokenCreator, 'primary'),
@@ -1164,10 +1193,32 @@ function renderTokenRow(token) {
1164
1193
  element('div', { class: 'permissions' }, token.permissions.map(permission => badge(permission))),
1165
1194
  element('small', {}, `创建于 ${formatDate(token.createdAt)}${token.lastUsedAt ? ` · 最近使用 ${formatDate(token.lastUsedAt)}` : ' · 尚未使用'}`),
1166
1195
  ),
1167
- revoked ? null : actionButton('撤销', () => confirmRevokeToken(token), 'danger small'),
1196
+ revoked
1197
+ ? actionButton('永久删除', () => confirmDeleteToken(token), 'danger small')
1198
+ : actionButton('撤销', () => confirmRevokeToken(token), 'danger small'),
1168
1199
  )
1169
1200
  }
1170
1201
 
1202
+ async function copyText(value, successMessage) {
1203
+ try { await navigator.clipboard.writeText(value); showToast(successMessage) } catch { showToast('复制失败,请手动选择内容。', 'error') }
1204
+ }
1205
+
1206
+ function togglePublicApi() {
1207
+ const enabling = !state.service.publicApiEnabled
1208
+ openConfirm({
1209
+ title: enabling ? '开启远程知识库 API?' : '关闭远程知识库 API?',
1210
+ message: enabling
1211
+ ? '开启后,持有有效令牌的其他 DSH 客户端可以连接这台中央知识库。'
1212
+ : '关闭后,所有远程客户端会立即断开;本地知识库和管理台不受影响。',
1213
+ confirmLabel: enabling ? '确认开启' : '确认关闭', danger: !enabling,
1214
+ onConfirm: async () => {
1215
+ state.service = await api('service', { method: 'PUT', body: { publicApiEnabled: enabling } })
1216
+ showToast(enabling ? '远程 API 已开启。' : '远程 API 已关闭。')
1217
+ renderShell()
1218
+ },
1219
+ })
1220
+ }
1221
+
1171
1222
  function emptyState(title, description, actionLabel, action) {
1172
1223
  return element('div', { class: 'empty-state' }, element('div', {}, element('strong', {}, title), element('p', {}, description), actionLabel ? actionButton(actionLabel, action, 'primary') : null))
1173
1224
  }
@@ -1605,6 +1656,15 @@ function confirmRevokeToken(token) {
1605
1656
  } })
1606
1657
  }
1607
1658
 
1659
+ function confirmDeleteToken(token) {
1660
+ openConfirm({ title: `永久删除“${token.name}”?`, message: '这条已撤销令牌记录会被永久删除,此操作无法撤销。', confirmLabel: '永久删除', danger: true, onConfirm: async () => {
1661
+ await api(`tokens/${encodeURIComponent(token.id)}`, { method: 'DELETE' })
1662
+ showToast('已撤销令牌已永久删除。')
1663
+ await loadTokens()
1664
+ renderShell()
1665
+ } })
1666
+ }
1667
+
1608
1668
  function openConfirm({ title, message, confirmLabel, danger, onConfirm }) {
1609
1669
  return openModal({ title, body: element('p', {}, message), primaryLabel: confirmLabel, primaryVariant: danger ? 'danger' : 'primary', onPrimary: async () => { await onConfirm(); return true } })
1610
1670
  }
package/web/index.html CHANGED
@@ -6,6 +6,7 @@
6
6
  <meta name="color-scheme" content="light dark">
7
7
  <meta name="theme-color" content="#f4f5f7">
8
8
  <meta name="dsh-knowledge-api" content="__DSH_KNOWLEDGE_API_PREFIX__">
9
+ <meta name="dsh-knowledge-auth-mode" content="__DSH_KNOWLEDGE_AUTH_MODE__">
9
10
  <meta name="dsh-knowledge-web" content="__DSH_KNOWLEDGE_WEB_PATH__">
10
11
  <title>DSH 知识库</title>
11
12
  <link rel="stylesheet" href="__DSH_KNOWLEDGE_WEB_PATH__/styles.css?v=__DSH_KNOWLEDGE_ASSET_VERSION__">
package/web/styles.css CHANGED
@@ -628,6 +628,15 @@ button:disabled { cursor: not-allowed; opacity: .56; }
628
628
  .token-row strong { display: block; }
629
629
  .token-row small { color: var(--text-tertiary); }
630
630
  .permissions { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; }
631
+ .api-access-card { display: grid; gap: 14px; margin-bottom: 20px; border: 1px solid var(--border); border-radius: var(--radius); padding: 18px; background: var(--surface); }
632
+ .api-access-heading, .api-access-actions { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
633
+ .api-access-heading strong { font-size: 15px; }
634
+ .api-access-heading p { margin: 4px 0 0; color: var(--text-tertiary); }
635
+ .api-address-label { color: var(--text-secondary); font-size: 12px; font-weight: 600; }
636
+ .api-address-row { display: flex; align-items: center; gap: 8px; }
637
+ .api-address-row .input { min-width: 0; flex: 1; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
638
+ .api-access-actions { align-items: center; border-top: 1px solid var(--border); padding-top: 13px; }
639
+ .api-access-actions small { color: var(--text-tertiary); }
631
640
 
632
641
  .dialog-backdrop { position: fixed; inset: 0; z-index: 100; display: grid; place-items: center; padding: 20px; background: rgb(8 10 15 / 52%); backdrop-filter: blur(4px); }
633
642
  .dialog {
@@ -755,6 +764,7 @@ button:disabled { cursor: not-allowed; opacity: .56; }
755
764
  }
756
765
 
757
766
  @media (max-width: 520px) {
767
+ .api-access-heading, .api-access-actions, .api-address-row { align-items: stretch; flex-direction: column; }
758
768
  body { font-size: 13px; }
759
769
  .login-page { padding: 16px; }
760
770
  .login-card { padding: 24px; }