dsh-opencode-usage-dashboard 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +254 -2
- package/THIRD_PARTY_NOTICES.md +10 -0
- package/client.js +3397 -0
- package/cordis.patch.yml +5 -0
- package/icon.svg +17 -0
- package/index.js +810 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +58 -4
- package/start.mjs +234 -0
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-opencode-usage-dashboard",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "OpenCode Go 用量面板:在 DSH 侧栏查看订阅额度、花费与用量趋势。OpenCode Go quota, spend and usage dashboard for DeepSeek Harness.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "zhxmt-xm",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/zhxmt-xm/dsh-opencode-usage-dashboard.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/zhxmt-xm/dsh-opencode-usage-dashboard#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/zhxmt-xm/dsh-opencode-usage-dashboard/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"deepseek-harness",
|
|
18
|
+
"dsh",
|
|
19
|
+
"dsh-plugin",
|
|
20
|
+
"opencode",
|
|
21
|
+
"opencode-go",
|
|
22
|
+
"quota",
|
|
23
|
+
"usage",
|
|
24
|
+
"dashboard",
|
|
25
|
+
"sidebar"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"start": "node start.mjs"
|
|
29
|
+
},
|
|
30
|
+
"exports": {
|
|
31
|
+
".": "./index.js",
|
|
32
|
+
"./client": "./client.js",
|
|
33
|
+
"./locale/*.json": "./locale/*.json",
|
|
34
|
+
"./icon": "./icon.svg",
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"index.js",
|
|
39
|
+
"client.js",
|
|
40
|
+
"cordis.patch.yml",
|
|
41
|
+
"locale/*.json",
|
|
42
|
+
"icon.svg",
|
|
43
|
+
"README.md",
|
|
44
|
+
"LICENSE",
|
|
45
|
+
"THIRD_PARTY_NOTICES.md",
|
|
46
|
+
"start.mjs"
|
|
47
|
+
],
|
|
48
|
+
"dsh": {
|
|
49
|
+
"bundle": {
|
|
50
|
+
"patch": "./cordis.patch.yml"
|
|
51
|
+
},
|
|
52
|
+
"client": {
|
|
53
|
+
"platform": "web",
|
|
54
|
+
"immediately": true,
|
|
55
|
+
"inject": [
|
|
56
|
+
"@deepseek-ai/dsh-client-locale"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
package/start.mjs
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 在 desktop profile 的本地 link 安装与 GitHub 安装之间切换。
|
|
4
|
+
*
|
|
5
|
+
* npm start # 交互选择
|
|
6
|
+
* node start.mjs link
|
|
7
|
+
* node start.mjs github
|
|
8
|
+
*
|
|
9
|
+
* 包名已从 @local/dsh-opencode-quota 迁移到 dsh-opencode-usage-dashboard;
|
|
10
|
+
* 脚本会顺手清理旧包名留下的依赖/junction。
|
|
11
|
+
*
|
|
12
|
+
* 为什么要先删目录:DSH 当前插件管理器卸载 `link:` 依赖时不会删除
|
|
13
|
+
* `node_modules` 里的 junction;直接 add 另一种来源会 EPERM。
|
|
14
|
+
*
|
|
15
|
+
* 环境变量:
|
|
16
|
+
* DSH_HOME 默认 ~/.dsh
|
|
17
|
+
* DSH_PROFILE 默认 desktop
|
|
18
|
+
* DSH_PROXY GitHub 安装代理,默认 http://127.0.0.1:7890
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { lstatSync, readdirSync, readFileSync, rmSync, rmdirSync } from 'node:fs'
|
|
22
|
+
import { homedir } from 'node:os'
|
|
23
|
+
import { dirname, join } from 'node:path'
|
|
24
|
+
import { createInterface } from 'node:readline/promises'
|
|
25
|
+
import { fileURLToPath } from 'node:url'
|
|
26
|
+
import { spawnSync } from 'node:child_process'
|
|
27
|
+
|
|
28
|
+
const PACKAGE_NAME = 'dsh-opencode-usage-dashboard'
|
|
29
|
+
const OLD_PACKAGE_NAME = '@local/dsh-opencode-quota'
|
|
30
|
+
const REPO_SPEC = 'github:zhxmt-xm/dsh-opencode-usage-dashboard'
|
|
31
|
+
const repoRoot = dirname(fileURLToPath(import.meta.url))
|
|
32
|
+
let source = process.argv[2]
|
|
33
|
+
|
|
34
|
+
if (source !== undefined && source !== 'link' && source !== 'github') {
|
|
35
|
+
console.error('用法:npm start,或 node start.mjs <link|github>')
|
|
36
|
+
process.exit(2)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const home = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
40
|
+
const profile = process.env.DSH_PROFILE || 'desktop'
|
|
41
|
+
const profileDir = join(home, 'profiles', profile)
|
|
42
|
+
const nodeModulesDir = join(profileDir, 'node_modules')
|
|
43
|
+
const pkgDir = join(nodeModulesDir, PACKAGE_NAME)
|
|
44
|
+
const oldLocalDir = join(nodeModulesDir, '@local')
|
|
45
|
+
const oldPkgDir = join(oldLocalDir, 'dsh-opencode-quota')
|
|
46
|
+
const proxy = process.env.DSH_PROXY || 'http://127.0.0.1:7890'
|
|
47
|
+
|
|
48
|
+
if (!lstatSync(profileDir, { throwIfNoEntry: false })) {
|
|
49
|
+
console.error(`找不到 profile:${profileDir}`)
|
|
50
|
+
process.exit(1)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 当前 node_modules 里装的是 link / github / none。 */
|
|
54
|
+
function pathState(path) {
|
|
55
|
+
const info = lstatSync(path, { throwIfNoEntry: false })
|
|
56
|
+
if (!info) return 'none'
|
|
57
|
+
return info.isSymbolicLink() ? 'link' : 'github'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function installedState() {
|
|
61
|
+
const next = pathState(pkgDir)
|
|
62
|
+
return next !== 'none' ? next : pathState(oldPkgDir)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function stateLabel(state) {
|
|
66
|
+
return state === 'link' ? '本地 link' : state === 'github' ? 'GitHub 版' : '未安装'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function currentLabel() {
|
|
70
|
+
const next = pathState(pkgDir)
|
|
71
|
+
if (next !== 'none') return stateLabel(next)
|
|
72
|
+
const old = pathState(oldPkgDir)
|
|
73
|
+
if (old !== 'none') return `${stateLabel(old)}(旧包名)`
|
|
74
|
+
return '未安装'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** profile 的 package.json 里是否还声明了这个依赖。 */
|
|
78
|
+
function hasDependency(name) {
|
|
79
|
+
try {
|
|
80
|
+
const pkg = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8'))
|
|
81
|
+
return Boolean(pkg.dependencies?.[name])
|
|
82
|
+
} catch {
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function dependencyNames() {
|
|
88
|
+
return [OLD_PACKAGE_NAME, PACKAGE_NAME].filter(hasDependency)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 没有命令行参数时,用 1/2 选择目标来源。 */
|
|
92
|
+
async function chooseSource() {
|
|
93
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
94
|
+
const state = installedState()
|
|
95
|
+
console.log(`当前安装:${currentLabel()}`)
|
|
96
|
+
console.log('切换前请先完全退出 DSH。')
|
|
97
|
+
console.log('')
|
|
98
|
+
console.log(` 1) 本地 link${state === 'link' ? '(当前)' : ''} ${repoRoot}`)
|
|
99
|
+
console.log(` 2) GitHub${state === 'github' ? '(当前)' : ''} ${REPO_SPEC}`)
|
|
100
|
+
console.log('')
|
|
101
|
+
let answer
|
|
102
|
+
try {
|
|
103
|
+
answer = (await rl.question('选择 1/2(Ctrl+C 退出):')).trim()
|
|
104
|
+
} finally {
|
|
105
|
+
rl.close()
|
|
106
|
+
}
|
|
107
|
+
if (answer === '1') return 'link'
|
|
108
|
+
if (answer === '2') return 'github'
|
|
109
|
+
console.error('无效选择')
|
|
110
|
+
process.exit(2)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!source) source = await chooseSource()
|
|
114
|
+
|
|
115
|
+
const state = installedState()
|
|
116
|
+
if (state === source) {
|
|
117
|
+
console.log(`当前已经是${stateLabel(state)},将重新安装。`)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (process.platform === 'win32') {
|
|
121
|
+
const check = spawnSync('tasklist', ['/FI', 'IMAGENAME eq DeepSeek Harness.exe', '/NH'], {
|
|
122
|
+
encoding: 'utf8',
|
|
123
|
+
windowsHide: true,
|
|
124
|
+
})
|
|
125
|
+
if (check.status === 0 && /DeepSeek Harness\.exe/i.test(check.stdout || '')) {
|
|
126
|
+
console.error('DSH 还在运行。请先完全退出 DeepSeek Harness,再运行本脚本。')
|
|
127
|
+
process.exit(1)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 用当前平台的 shell 调用 dsh plugin。 */
|
|
132
|
+
function runDsh(args, childEnv = process.env) {
|
|
133
|
+
if (process.platform === 'win32') {
|
|
134
|
+
const line = ['dsh', ...args.map((arg) => (/\s/.test(arg) ? `"${arg}"` : arg))].join(' ')
|
|
135
|
+
return spawnSync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', line], {
|
|
136
|
+
cwd: repoRoot,
|
|
137
|
+
env: childEnv,
|
|
138
|
+
stdio: 'inherit',
|
|
139
|
+
windowsHide: false,
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
return spawnSync('dsh', args, { cwd: repoRoot, env: childEnv, stdio: 'inherit' })
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 旧安装可能是 junction,也可能是 GitHub 装出来的普通目录。 */
|
|
146
|
+
function removeInstalled(path) {
|
|
147
|
+
const info = lstatSync(path, { throwIfNoEntry: false })
|
|
148
|
+
if (!info) return
|
|
149
|
+
if (info.isSymbolicLink()) {
|
|
150
|
+
console.log(`删除旧 junction:${path}`)
|
|
151
|
+
rmdirSync(path)
|
|
152
|
+
} else {
|
|
153
|
+
console.log(`删除旧包目录:${path}`)
|
|
154
|
+
rmSync(path, { recursive: true, force: true })
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** pnpm 上次失败可能留下 *_tmp_*。 */
|
|
159
|
+
function removeTemps(dir) {
|
|
160
|
+
if (!lstatSync(dir, { throwIfNoEntry: false })) return
|
|
161
|
+
for (const name of readdirSync(dir)) {
|
|
162
|
+
if (!name.startsWith('dsh-opencode-quota_tmp_') && !name.startsWith(`${PACKAGE_NAME}_tmp_`)) {
|
|
163
|
+
continue
|
|
164
|
+
}
|
|
165
|
+
const tmp = join(dir, name)
|
|
166
|
+
const info = lstatSync(tmp, { throwIfNoEntry: false })
|
|
167
|
+
if (!info) continue
|
|
168
|
+
console.log(`删除临时目录:${tmp}`)
|
|
169
|
+
if (info.isSymbolicLink()) rmdirSync(tmp)
|
|
170
|
+
else rmSync(tmp, { recursive: true, force: true })
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log('')
|
|
175
|
+
console.log(`目标:${source === 'github' ? 'GitHub 版' : '本地 link 版'}(${PACKAGE_NAME})`)
|
|
176
|
+
|
|
177
|
+
console.log('[1/3] 卸载当前版本…')
|
|
178
|
+
const deps = dependencyNames()
|
|
179
|
+
if (deps.length === 0) {
|
|
180
|
+
console.log('当前没有已安装的插件,跳过卸载。')
|
|
181
|
+
} else {
|
|
182
|
+
for (const name of deps) {
|
|
183
|
+
console.log(`卸载 ${name}…`)
|
|
184
|
+
const removed = runDsh(['plugin', '--profile', profile, 'remove', name])
|
|
185
|
+
if (removed.error) {
|
|
186
|
+
console.warn(`卸载命令失败:${removed.error.message},继续清理。`)
|
|
187
|
+
} else if (removed.status !== 0) {
|
|
188
|
+
console.warn(`卸载返回 ${removed.status},继续清理。`)
|
|
189
|
+
} else {
|
|
190
|
+
console.log('卸载完成。')
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
console.log('[2/3] 清理 node_modules 残留…')
|
|
196
|
+
if (pathState(oldPkgDir) !== 'none' && pathState(pkgDir) === 'none') {
|
|
197
|
+
console.log(`检测到旧包名 ${OLD_PACKAGE_NAME},清理后会安装 ${PACKAGE_NAME}。`)
|
|
198
|
+
}
|
|
199
|
+
removeInstalled(pkgDir)
|
|
200
|
+
removeInstalled(oldPkgDir)
|
|
201
|
+
removeTemps(nodeModulesDir)
|
|
202
|
+
removeTemps(oldLocalDir)
|
|
203
|
+
|
|
204
|
+
/** 中断的插件操作会留下 run.json,清掉避免下次被当成“操作进行中”。 */
|
|
205
|
+
console.log('清理 plugin-manager 运行标记…')
|
|
206
|
+
rmSync(join(profileDir, '.plugin-manager', 'run.json'), { force: true })
|
|
207
|
+
|
|
208
|
+
const spec =
|
|
209
|
+
source === 'github' ? REPO_SPEC : `link:${repoRoot.replaceAll('\\', '/')}`
|
|
210
|
+
|
|
211
|
+
const env = { ...process.env }
|
|
212
|
+
if (source === 'github' && proxy) {
|
|
213
|
+
env.HTTP_PROXY = proxy
|
|
214
|
+
env.HTTPS_PROXY = proxy
|
|
215
|
+
env.ALL_PROXY = proxy
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
console.log(`[3/3] 安装${source === 'github' ? 'GitHub 版' : '本地 link 版'}…`)
|
|
219
|
+
console.log(`dsh plugin --profile ${profile} add ${spec}`)
|
|
220
|
+
|
|
221
|
+
const result = runDsh(['plugin', '--profile', profile, 'add', spec], env)
|
|
222
|
+
|
|
223
|
+
if (result.error) {
|
|
224
|
+
console.error(`执行 dsh 失败:${result.error.message}`)
|
|
225
|
+
process.exit(1)
|
|
226
|
+
}
|
|
227
|
+
if (result.status !== 0) {
|
|
228
|
+
console.error(`dsh plugin 退出码 ${result.status}`)
|
|
229
|
+
process.exit(result.status || 1)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log('')
|
|
233
|
+
console.log(`完成:profile "${profile}" 现在使用 ${source === 'github' ? 'GitHub 版' : '本地 link 版'}(${PACKAGE_NAME})。`)
|
|
234
|
+
console.log('启动 DSH 即可。')
|