dsh-router-core 0.1.0

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.
Files changed (42) hide show
  1. package/README.md +151 -0
  2. package/cordis.patch.yml +9 -0
  3. package/lib/client-registry.js +3006 -0
  4. package/lib/client.js +3006 -0
  5. package/lib/index.js +1756 -0
  6. package/lib/types/credential-store.d.ts +15 -0
  7. package/lib/types/index.d.ts +6 -0
  8. package/lib/types/keys.d.ts +39 -0
  9. package/lib/types/llm/adapter.d.ts +31 -0
  10. package/lib/types/router/index.d.ts +73 -0
  11. package/lib/types/router/types.d.ts +80 -0
  12. package/lib/types/shared.d.ts +138 -0
  13. package/lib/types/supplier-config.d.ts +28 -0
  14. package/lib/types/suppliers/contract.d.ts +98 -0
  15. package/lib/types/suppliers/loader.d.ts +41 -0
  16. package/lib/types/suppliers/registry.d.ts +33 -0
  17. package/package.json +86 -0
  18. package/src/client/CombosTab.tsx +458 -0
  19. package/src/client/EndpointTab.tsx +360 -0
  20. package/src/client/Modal.tsx +46 -0
  21. package/src/client/RouterView.tsx +186 -0
  22. package/src/client/SupplierDetail.tsx +943 -0
  23. package/src/client/index.tsx +18 -0
  24. package/src/client/router.css +2096 -0
  25. package/src/client/sidebar-entry.ts +167 -0
  26. package/src/client/workspace-controller.ts +26 -0
  27. package/src/client/workspace-mount.tsx +117 -0
  28. package/src/credential-store.ts +64 -0
  29. package/src/index.ts +468 -0
  30. package/src/keys.ts +143 -0
  31. package/src/llm/adapter.ts +273 -0
  32. package/src/router/index.ts +273 -0
  33. package/src/router/types.ts +85 -0
  34. package/src/shared.ts +152 -0
  35. package/src/supplier-config.ts +144 -0
  36. package/src/suppliers/contract.ts +73 -0
  37. package/src/suppliers/loader.ts +191 -0
  38. package/src/suppliers/nvidia/plugin.ts +268 -0
  39. package/src/suppliers/opencode/plugin.ts +200 -0
  40. package/src/suppliers/openrouter/plugin.ts +282 -0
  41. package/src/suppliers/registry.ts +365 -0
  42. package/src/types.d.ts +45 -0
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Mount a self-healing sidebar entry for the 路由系统 (Routing System),
3
+ * placed directly ABOVE dsh-mnemon's 记忆系统 entry when that is present.
4
+ *
5
+ * The DOM pattern mirrors dsh-mnemon's sidebar-entry.ts: an official-style
6
+ * row under the New Session row, with a MutationObserver keeping it in place
7
+ * as the shell re-renders. We deliberately order the family selector so the
8
+ * router entry sits above the memory entry.
9
+ */
10
+ import { ROUTER_ENTRY_SELECTOR, ROUTER_TITLE } from '../shared.ts'
11
+ import type { RouterWorkspaceController } from './workspace-controller.ts'
12
+
13
+ /** Other plugin families that occupy the same sidebar region. */
14
+ const FAMILY_SELECTOR =
15
+ '[data-dsh-taskboard-entry], [data-dsh-ssh-entry], [data-dsh-mnemon-entry], [data-dsh-router-entry]'
16
+
17
+ /** The entry we want to sit above (dsh-mnemon's 记忆系统), when present. */
18
+ const ABOVE_TARGET_SELECTOR = '[data-dsh-mnemon-entry]'
19
+
20
+ function sidebarRoot(): HTMLElement | undefined {
21
+ const column = document.querySelector<HTMLElement>(
22
+ '[data-pane="sidebar"], [class*="sidebarCol"], .dshDesktopUpstreamSidebar',
23
+ )
24
+ if (column === null) return undefined
25
+ return column.querySelector<HTMLElement>('[class*="logoRow"]')?.parentElement
26
+ ?? (column.firstElementChild as HTMLElement | undefined)
27
+ }
28
+
29
+ function newSessionButton(root: HTMLElement): HTMLButtonElement | undefined {
30
+ const nested = root.querySelector<HTMLButtonElement>('button[class*="newSession"]')
31
+ if (nested !== null) return nested
32
+ for (const child of root.children) {
33
+ if (child.tagName === 'BUTTON') return child as HTMLButtonElement
34
+ }
35
+ return undefined
36
+ }
37
+
38
+ function createIcon(): SVGSVGElement {
39
+ const namespace = 'http://www.w3.org/2000/svg'
40
+ const icon = document.createElementNS(namespace, 'svg')
41
+ icon.setAttribute('viewBox', '0 0 16 16')
42
+ icon.setAttribute('width', '18')
43
+ icon.setAttribute('height', '18')
44
+ icon.setAttribute('fill', 'none')
45
+ icon.setAttribute('stroke', 'currentColor')
46
+ icon.setAttribute('stroke-width', '1.5')
47
+ icon.setAttribute('stroke-linecap', 'round')
48
+ icon.setAttribute('stroke-linejoin', 'round')
49
+ icon.setAttribute('aria-hidden', 'true')
50
+ // A simple node-graph "route" glyph: three dots joined by two paths.
51
+ const c1 = document.createElementNS(namespace, 'circle')
52
+ c1.setAttribute('cx', '3.5'); c1.setAttribute('cy', '3.5'); c1.setAttribute('r', '1.6')
53
+ const c2 = document.createElementNS(namespace, 'circle')
54
+ c2.setAttribute('cx', '12.5'); c2.setAttribute('cy', '3.5'); c2.setAttribute('r', '1.6')
55
+ const c3 = document.createElementNS(namespace, 'circle')
56
+ c3.setAttribute('cx', '8'); c3.setAttribute('cy', '12.5'); c3.setAttribute('r', '1.6')
57
+ const p1 = document.createElementNS(namespace, 'path')
58
+ p1.setAttribute('d', 'M5.1 3.5h6.8')
59
+ const p2 = document.createElementNS(namespace, 'path')
60
+ p2.setAttribute('d', 'M3.5 5.1v4.3c0 1.7 2 2.2 3 2.4')
61
+ const p3 = document.createElementNS(namespace, 'path')
62
+ p3.setAttribute('d', 'M12.5 5.1v4.3c0 1.7-2 2.2-3 2.4')
63
+ icon.append(c1, c2, c3, p1, p2, p3)
64
+ return icon
65
+ }
66
+
67
+ function createEntry(controller: RouterWorkspaceController): { entry: HTMLButtonElement; label: HTMLSpanElement } {
68
+ const entry = document.createElement('button')
69
+ entry.type = 'button'
70
+ entry.dataset.dshRouterEntry = ''
71
+ entry.className = 'dshr-entry'
72
+ const icon = document.createElement('span')
73
+ icon.className = 'dshr-entryIcon'
74
+ icon.append(createIcon())
75
+ const label = document.createElement('span')
76
+ label.className = 'dshr-entryLabel'
77
+ entry.append(icon, label)
78
+ entry.addEventListener('click', () => { controller.toggle() })
79
+ return { entry, label }
80
+ }
81
+
82
+ function placeEntry(root: HTMLElement, entry: HTMLButtonElement): boolean {
83
+ const button = newSessionButton(root)
84
+ if (button === undefined) return false
85
+ if (entry.parentElement === root) return true
86
+ const row = button.closest('[class*="logoRow"]')
87
+ const base = row !== null && row.parentElement === root ? row : button
88
+ const family = Array.from(root.children).filter(
89
+ (element): element is HTMLElement =>
90
+ element instanceof HTMLElement && element.matches(FAMILY_SELECTOR),
91
+ )
92
+ // Prefer to sit directly above dsh-mnemon's entry; otherwise after the
93
+ // last known family entry, else after the New Session row.
94
+ const aboveTarget = Array.from(root.children).find(
95
+ (element): element is HTMLElement =>
96
+ element instanceof HTMLElement && element.matches(ABOVE_TARGET_SELECTOR),
97
+ )
98
+ let anchor: Element | null
99
+ if (aboveTarget !== undefined) {
100
+ anchor = aboveTarget
101
+ } else {
102
+ anchor = family.at(-1)?.nextElementSibling ?? base.nextElementSibling
103
+ }
104
+ root.insertBefore(entry, anchor)
105
+ return true
106
+ }
107
+
108
+ /** Mount a self-healing 路由系统 entry above the 记忆系统 row. */
109
+ export function mountRouterSidebarEntry(
110
+ controller: RouterWorkspaceController,
111
+ ): () => void {
112
+ const { entry, label } = createEntry(controller)
113
+ let root: HTMLElement | undefined
114
+ let placed = false
115
+
116
+ const syncLabel = (): void => {
117
+ if (entry.getAttribute('aria-label') !== ROUTER_TITLE) entry.setAttribute('aria-label', ROUTER_TITLE)
118
+ if (entry.title !== ROUTER_TITLE) entry.title = ROUTER_TITLE
119
+ if (label.textContent !== ROUTER_TITLE) label.textContent = ROUTER_TITLE
120
+ }
121
+
122
+ const rootObserver = new MutationObserver(() => {
123
+ if (root === undefined || !root.isConnected) {
124
+ placed = false
125
+ tryPlace()
126
+ return
127
+ }
128
+ if (!root.contains(entry)) placed = placeEntry(root, entry)
129
+ })
130
+
131
+ const tryPlace = (): void => {
132
+ syncLabel()
133
+ if (root !== undefined && !root.isConnected) {
134
+ rootObserver.disconnect()
135
+ root = undefined
136
+ placed = false
137
+ }
138
+ if (placed && document.body.contains(entry)) return
139
+ if (placed) {
140
+ rootObserver.disconnect()
141
+ root = undefined
142
+ placed = false
143
+ }
144
+ root ??= sidebarRoot()
145
+ if (root === undefined) return
146
+ placed = placeEntry(root, entry)
147
+ if (placed) rootObserver.observe(root, { childList: true, subtree: true })
148
+ }
149
+
150
+ const waitObserver = new MutationObserver(tryPlace)
151
+ waitObserver.observe(document.body, { childList: true, subtree: true })
152
+
153
+ const syncActive = (): void => {
154
+ if (controller.getSnapshot().open) entry.dataset.active = 'true'
155
+ else delete entry.dataset.active
156
+ }
157
+ const unsubscribe = controller.subscribe(syncActive)
158
+ syncActive()
159
+ tryPlace()
160
+
161
+ return () => {
162
+ waitObserver.disconnect()
163
+ rootObserver.disconnect()
164
+ unsubscribe()
165
+ entry.remove()
166
+ }
167
+ }
@@ -0,0 +1,26 @@
1
+ export interface RouterWorkspaceSnapshot {
2
+ open: boolean
3
+ }
4
+
5
+ /** Small framework-neutral state holder shared by the sidebar row and panel. */
6
+ export class RouterWorkspaceController {
7
+ private snapshot: RouterWorkspaceSnapshot = { open: false }
8
+ private readonly listeners = new Set<() => void>()
9
+
10
+ getSnapshot = (): RouterWorkspaceSnapshot => this.snapshot
11
+
12
+ subscribe = (listener: () => void): (() => void) => {
13
+ this.listeners.add(listener)
14
+ return () => { this.listeners.delete(listener) }
15
+ }
16
+
17
+ open(): void { this.setOpen(true) }
18
+ close(): void { this.setOpen(false) }
19
+ toggle(): void { this.setOpen(!this.snapshot.open) }
20
+
21
+ private setOpen(open: boolean): void {
22
+ if (this.snapshot.open === open) return
23
+ this.snapshot = { open }
24
+ for (const listener of this.listeners) listener()
25
+ }
26
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Mount the 路由系统 center-column takeover panel, mirroring dsh-mnemon's
3
+ * workspace-mount.tsx: a React root appended to the conversation column,
4
+ * shown via `data-dsh-router-active` on <html>, and coordinated with the
5
+ * other sidebar panels (taskboard / ssh / mnemon) through the shared
6
+ * `dsh-panel-activate` event.
7
+ */
8
+ import { useEffect } from 'react'
9
+ import { createRoot, type Root } from 'react-dom/client'
10
+ import { ROUTER_ACTIVE_ATTR, ROUTER_TITLE, ROUTER_VIEW_SELECTOR } from '../shared.ts'
11
+ import { RouterView } from './RouterView.tsx'
12
+ import { mountRouterSidebarEntry } from './sidebar-entry.ts'
13
+ import { RouterWorkspaceController } from './workspace-controller.ts'
14
+
15
+ const CONVERSATION_COLUMN_SELECTOR =
16
+ '[data-pane="conversation"], [class*="centerCol"], .dshDesktopConversationSurface'
17
+ const ACTIVATE_EVENT = 'dsh-panel-activate'
18
+ const SIDEBAR_CONTEXT_SELECTOR =
19
+ '[class*="sessionRow"], [class*="projectRow"], [class*="searchResultRow"], [class*="searchResultWorkspace"], [class*="newSession"]'
20
+
21
+ function conversationColumn(): HTMLElement | undefined {
22
+ return document.querySelector<HTMLElement>(CONVERSATION_COLUMN_SELECTOR) ?? undefined
23
+ }
24
+
25
+ function mountPanel(controller: RouterWorkspaceController): () => void {
26
+ function Panel(): JSX.Element {
27
+ useEffect(() => {
28
+ // Keep the panel title in the browser tab for orientation, like the
29
+ // official panels do.
30
+ const original = document.title
31
+ document.title = ROUTER_TITLE
32
+ return () => { document.title = original }
33
+ }, [])
34
+ return <RouterView onBack={() => controller.close()} />
35
+ }
36
+
37
+ let root: Root | undefined
38
+ let container: HTMLDivElement | undefined
39
+
40
+ const ensure = (): void => {
41
+ if (container !== undefined && container.isConnected) return
42
+ if (container !== undefined) {
43
+ root?.unmount()
44
+ root = undefined
45
+ container = undefined
46
+ }
47
+ const column = conversationColumn()
48
+ if (column === undefined) return
49
+ container = document.createElement('div')
50
+ container.dataset.dshRouterView = ''
51
+ container.className = 'dshr-panelView'
52
+ column.append(container)
53
+ root = createRoot(container)
54
+ root.render(<Panel />)
55
+ }
56
+
57
+ const waitObserver = new MutationObserver(ensure)
58
+ waitObserver.observe(document.body, { childList: true, subtree: true })
59
+
60
+ let suppressCompatibilityClose = false
61
+ const applyActive = (): void => {
62
+ if (!controller.getSnapshot().open) {
63
+ document.documentElement.removeAttribute(ROUTER_ACTIVE_ATTR)
64
+ return
65
+ }
66
+ // Announce the other panels so they close; mirror dsh-mnemon's
67
+ // compatibility broadcast (taskboard/ssh listen to 'ssh'/'taskboard').
68
+ suppressCompatibilityClose = true
69
+ document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: 'ssh' }))
70
+ document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: 'taskboard' }))
71
+ suppressCompatibilityClose = false
72
+ document.documentElement.setAttribute(ROUTER_ACTIVE_ATTR, '')
73
+ document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: 'router' }))
74
+ }
75
+
76
+ const onOtherPanelActivate = (event: Event): void => {
77
+ if (suppressCompatibilityClose || !controller.getSnapshot().open) return
78
+ const detail = (event as CustomEvent<unknown>).detail
79
+ if (detail === 'taskboard' || detail === 'ssh' || detail === 'mnemon') controller.close()
80
+ }
81
+
82
+ const onSidebarContextClick = (event: MouseEvent): void => {
83
+ if (!controller.getSnapshot().open) return
84
+ const target = event.target
85
+ if (target instanceof Element && target.closest(SIDEBAR_CONTEXT_SELECTOR) !== null) controller.close()
86
+ }
87
+
88
+ document.addEventListener('click', onSidebarContextClick, true)
89
+ document.addEventListener(ACTIVATE_EVENT, onOtherPanelActivate)
90
+ const unsubscribe = controller.subscribe(applyActive)
91
+ applyActive()
92
+ ensure()
93
+
94
+ return () => {
95
+ document.removeEventListener('click', onSidebarContextClick, true)
96
+ document.removeEventListener(ACTIVATE_EVENT, onOtherPanelActivate)
97
+ waitObserver.disconnect()
98
+ unsubscribe()
99
+ document.documentElement.removeAttribute(ROUTER_ACTIVE_ATTR)
100
+ root?.unmount()
101
+ root = undefined
102
+ container?.remove()
103
+ container = undefined
104
+ }
105
+ }
106
+
107
+ /** Mount the sidebar row and its stateful center-column workspace as one unit. */
108
+ export function mountRouterWorkspace(): () => void {
109
+ if (typeof document === 'undefined' || typeof window === 'undefined') return () => {}
110
+ const controller = new RouterWorkspaceController()
111
+ const disposeEntry = mountRouterSidebarEntry(controller)
112
+ const disposePanel = mountPanel(controller)
113
+ return () => {
114
+ disposePanel()
115
+ disposeEntry()
116
+ }
117
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * 通用凭证存储 —— dsh-router 核心统一管账号凭证(SQLite,参考 9router 的
3
+ * providerConnections 表:凭证作为不透明 JSON blob 存单表 data 列)。
4
+ *
5
+ * 凭证是供应商特有的不透明数据(js 定义格式),核心只负责存储/增删/生命周期,
6
+ * 不解析内容。库文件:`{authDir}/credentials.sqlite`,表:
7
+ * credentials(supplier TEXT, uid TEXT, data TEXT, PRIMARY KEY(supplier, uid))
8
+ */
9
+ import { DatabaseSync } from 'node:sqlite'
10
+ import { mkdirSync } from 'node:fs'
11
+ import { join } from 'node:path'
12
+
13
+ /** 通用凭证存储。 */
14
+ export class CredentialStore {
15
+ private db: DatabaseSync
16
+
17
+ constructor(authDir: string) {
18
+ if (authDir !== '' && authDir !== '.') mkdirSync(authDir, { recursive: true })
19
+ const dbFile = join(authDir, 'credentials.sqlite')
20
+ this.db = new DatabaseSync(dbFile)
21
+ this.db.exec(`CREATE TABLE IF NOT EXISTS credentials (
22
+ supplier TEXT NOT NULL,
23
+ uid TEXT NOT NULL,
24
+ data TEXT NOT NULL,
25
+ PRIMARY KEY (supplier, uid)
26
+ )`)
27
+ }
28
+
29
+ /** 某供应商的所有凭证 uid。 */
30
+ list(supplierId: string): string[] {
31
+ const rows = this.db.prepare('SELECT uid FROM credentials WHERE supplier = ? ORDER BY rowid').all(supplierId) as Array<{ uid: string }>
32
+ return rows.map((r) => r.uid)
33
+ }
34
+
35
+ /** 读某凭证(不透明 blob)。不存在返回 undefined。 */
36
+ get<T = unknown>(supplierId: string, uid: string): T | undefined {
37
+ const row = this.db.prepare('SELECT data FROM credentials WHERE supplier = ? AND uid = ?').get(supplierId, uid) as { data: string } | undefined
38
+ if (row === undefined) return undefined
39
+ try {
40
+ return JSON.parse(row.data) as T
41
+ } catch {
42
+ return undefined
43
+ }
44
+ }
45
+
46
+ /** 存凭证(upsert)。 */
47
+ save(supplierId: string, uid: string, data: unknown): void {
48
+ this.db.prepare('INSERT INTO credentials (supplier, uid, data) VALUES (?, ?, ?) ON CONFLICT(supplier, uid) DO UPDATE SET data = excluded.data').run(
49
+ supplierId,
50
+ uid,
51
+ JSON.stringify(data),
52
+ )
53
+ }
54
+
55
+ /** 删凭证。 */
56
+ remove(supplierId: string, uid: string): void {
57
+ this.db.prepare('DELETE FROM credentials WHERE supplier = ? AND uid = ?').run(supplierId, uid)
58
+ }
59
+
60
+ /** 关闭连接(可选)。 */
61
+ close(): void {
62
+ this.db.close()
63
+ }
64
+ }