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.
- package/README.md +151 -0
- package/cordis.patch.yml +9 -0
- package/lib/client-registry.js +3006 -0
- package/lib/client.js +3006 -0
- package/lib/index.js +1756 -0
- package/lib/types/credential-store.d.ts +15 -0
- package/lib/types/index.d.ts +6 -0
- package/lib/types/keys.d.ts +39 -0
- package/lib/types/llm/adapter.d.ts +31 -0
- package/lib/types/router/index.d.ts +73 -0
- package/lib/types/router/types.d.ts +80 -0
- package/lib/types/shared.d.ts +138 -0
- package/lib/types/supplier-config.d.ts +28 -0
- package/lib/types/suppliers/contract.d.ts +98 -0
- package/lib/types/suppliers/loader.d.ts +41 -0
- package/lib/types/suppliers/registry.d.ts +33 -0
- package/package.json +86 -0
- package/src/client/CombosTab.tsx +458 -0
- package/src/client/EndpointTab.tsx +360 -0
- package/src/client/Modal.tsx +46 -0
- package/src/client/RouterView.tsx +186 -0
- package/src/client/SupplierDetail.tsx +943 -0
- package/src/client/index.tsx +18 -0
- package/src/client/router.css +2096 -0
- package/src/client/sidebar-entry.ts +167 -0
- package/src/client/workspace-controller.ts +26 -0
- package/src/client/workspace-mount.tsx +117 -0
- package/src/credential-store.ts +64 -0
- package/src/index.ts +468 -0
- package/src/keys.ts +143 -0
- package/src/llm/adapter.ts +273 -0
- package/src/router/index.ts +273 -0
- package/src/router/types.ts +85 -0
- package/src/shared.ts +152 -0
- package/src/supplier-config.ts +144 -0
- package/src/suppliers/contract.ts +73 -0
- package/src/suppliers/loader.ts +191 -0
- package/src/suppliers/nvidia/plugin.ts +268 -0
- package/src/suppliers/opencode/plugin.ts +200 -0
- package/src/suppliers/openrouter/plugin.ts +282 -0
- package/src/suppliers/registry.ts +365 -0
- 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
|
+
}
|