dsh-workbuddy2api-hub 1.0.7 → 1.0.11

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/CHANGELOG.md CHANGED
@@ -2,6 +2,47 @@
2
2
 
3
3
  本项目遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/) 与语义化版本。
4
4
 
5
+ ## [1.0.11] - 2026-10-24
6
+
7
+ 装插件不再顺带拉一份用不上的 `@deepseek-ai/cordis`。
8
+
9
+ - `@deepseek-ai/cordis` 那条 peerDependency 标成 optional:插件一行 `@deepseek-ai/*` 都没 import
10
+ (`apply(ctx)` 用的是宿主传进来的 ctx),留着它只会让 `npm i dsh-workbuddy2api-hub` 多装一份副本
11
+ ——cordis 本来由 DSH 自己提供。现在装完 `node_modules` 里只有插件本身。
12
+
13
+ ## [1.0.10] - 2026-10-24
14
+
15
+ 发版一条命令同时落到 npm 与 GitHub Release,npm 包里只留运行时代码。
16
+
17
+ - `npm run release <x.y.z>` 现在会自己发 npm:先查登录态与版本号是否已被占用,再 `npm publish`,
18
+ 最后才提交、打标签、推送(npm 失败就停,不会留「GitHub 有标签、npm 没版本」的半成品)。
19
+ `--no-publish` 只发 GitHub,`--dry-run` 只跑到测试。
20
+ - npm 包从 15 个文件瘦到 8 个(约 60 kB):只含 `lib/`、`client/`、`cordis.patch.yml` 与
21
+ README / CHANGELOG / LICENSE。`scripts/`、`tests/`、`docs/`、`.github/` 只在仓库里。
22
+ - **修掉一个会改坏用户 profile 的坑**:`package.json` 里原来有个 `"install": "node scripts/install-local.mjs"`
23
+ ——`install` 是 npm 的生命周期钩子,任何人 `npm i dsh-workbuddy2api-hub` 都会自动执行它,
24
+ 把「装包的那个人」的 profile 依赖改指向包自己所在的目录。已改名 `npm run link` / `npm run link:dry` /
25
+ `npm run unlink`,生命周期钩子不再存在。
26
+ - README 里 `docs/MAINTAINER.md` 的链接改用 GitHub 绝对地址(npm 包页里相对链接点不开)。
27
+
28
+ ## [1.0.9] - 2026-10-24
29
+
30
+ 模型选择器里能直接看到每个模型的倍率了。
31
+
32
+ - 写进 `llm-pi-ai` 的模型名改成「展示名 + 倍率」:付费模型形如 `GLM-5.1 ×0.79`,
33
+ 网关标 `x0.00`(或字面量 `free`)的免费模型形如 `Hy3 Free`;`id` 不变,路由与计费不受影响。
34
+ - 没声明倍率的模型不加后缀,不谎报免费。
35
+ - 设置页模型表的「倍率」列同样显示 `Free`,原先免费模型显示的是 `—`。
36
+
37
+ ## [1.0.8] - 2026-10-24
38
+
39
+ 插件在列表里显示「异常」「包不可用」的那个坑:`scripts/install-local.mjs` 把自己**所在**的
40
+ 目录写进 DSH profile 的 `package.json`,从临时目录(如 `tmp/verify/...`)的运行副本执行时,
41
+ profile 就被指到了一个随时会被清掉的路径——清掉之后插件立刻加载失败。
42
+
43
+ - `scripts/install-local.mjs` 拒绝从 `node_modules/` 或 `tmp/` 下的副本执行,并提示必须从仓库根目录运行。
44
+ - README 排错段补了这一症状的定位与修复:查 profile 依赖指向 + `node_modules` 链接,重跑安装脚本后重启。
45
+
5
46
  ## [1.0.7] - 2026-10-24
6
47
 
7
48
  首个发布到 npm 的版本:`npm i dsh-workbuddy2api-hub`,不用 clone 也不用构建。
package/README.md CHANGED
@@ -14,7 +14,7 @@ DSH 设置页 / 侧边栏 ← /workbuddy2api-hub/api/* ──┘ └
14
14
  ## 功能
15
15
 
16
16
  - **连接面板**:服务地址、面板密码、默认页签、轮询间隔;「测试连接」「立即同步」「打开看板」「清空同步」。
17
- - **模型同步**:把 `/v1/models` 写进 `llm-pi-ai` 的 `providers.workbuddy2api-hub`,每个模型带上 `contextWindow` / `maxTokens` / 图片输入 / 思考档位;按模型名称排序(不区分大小写、数字按数值),设置页表格与写进 DSH 的顺序一致。
17
+ - **模型同步**:把 `/v1/models` 写进 `llm-pi-ai` 的 `providers.workbuddy2api-hub`,每个模型带上 `contextWindow` / `maxTokens` / 图片输入 / 思考档位;按模型名称排序(不区分大小写、数字按数值),设置页表格与写进 DSH 的顺序一致。模型名后面直接带网关的倍率(`GLM-5.1 ×0.79`),免费模型标 `Free`——倍率写不进名字,选择器里就看不见。
18
18
  - **增量与容错**:模型下架先进宽限期(默认 24 小时,可一键立即移除),回来能复活;内容没变就不写配置,避免每次轮询都触发一次配置更新;配置冲突自动重取 revision 重试。
19
19
  - **余额与用量**:`/v1/balance-summary` 的 credit 余额,`/usage?range=today` 的请求数 / 失败数 / tokens / credit / API 等价花费 / 缓存命中。
20
20
  - **侧边栏徽标**:展开态三行——`● WorkBuddy … 3715.39` / `今日 72.3M · 61.41 · ¥10.39` / `本周 260.7M · 465.43 · ¥31.17`;窗口名贴左、三个数字贴右,依次是 tokens / 积分消耗 / API 等价花费。悬停能看完整数字和连接地址,点一下直接打开 hub 看板。
@@ -164,6 +164,15 @@ dsh plugin --profile desktop add github:HimA6652/dsh-workbuddy2api-hub
164
164
 
165
165
  **设置页没有这一块**:插件没加载。看 DSH 启动日志里有没有 `workbuddy-hub`;确认装完之后重启过;确认 `~/.dsh/profiles/<profile>/node_modules/dsh-workbuddy2api-hub` 这个链接存在。
166
166
 
167
+ **插件列表里显示「异常」「包不可用」**:那个链接指着的地方没了。最常见的原因是 profile 的依赖被写成了一个临时目录(例如 `.../tmp/...`),临时目录一清就断。看一眼:
168
+
169
+ ```bash
170
+ node -e "const p=require(process.env.HOME+'/.dsh/profiles/desktop/package.json');console.log(p.dependencies['dsh-workbuddy2api-hub'])"
171
+ ls -l ~/.dsh/profiles/desktop/node_modules/dsh-workbuddy2api-hub
172
+ ```
173
+
174
+ 依赖应当指向仓库或已发布包的目录;指到 `tmp/` 之类的路径就重跑一次 `node scripts/install-local.mjs`(**必须从仓库根目录跑**,脚本会拒绝从 `node_modules/` 或 `tmp/` 下的副本执行),然后重启 DSH。
175
+
167
176
  **面板显示「连不上 hub」**:先在浏览器打开 `http://127.0.0.1:8788`。打不开就是服务没起——本插件只负责连,不负责启动。
168
177
 
169
178
  **「还没有可用的 hub API Key」**:hub 的 `/settings` 里没有启用的 Key,或者你关了 `autoApiKey`。去 hub 的「设置 → API Key」建一个,或在插件设置页填进去。
@@ -174,8 +183,12 @@ dsh plugin --profile desktop add github:HimA6652/dsh-workbuddy2api-hub
174
183
 
175
184
  ## 开发
176
185
 
186
+ 下面这些命令要 clone 仓库才能用——npm 包里只有运行时代码(`lib/`、`client/`、`cordis.patch.yml`),`scripts/`、`tests/`、`docs/` 只在仓库里。
187
+
177
188
  ```bash
178
- npm test # 87 个测试,跑在假宿主 + 假 hub 上,不需要起 DSH
189
+ git clone https://github.com/HimA6652/dsh-workbuddy2api-hub.git
190
+ cd dsh-workbuddy2api-hub
191
+ npm test # 88 个测试,跑在假宿主 + 假 hub 上,不需要起 DSH
179
192
  node scripts/mock-hub.mjs --port 8789 # 起一个假 hub,用来手工点界面
180
193
  node scripts/smoke-live.mjs --json # 对真实 hub 做只读联调
181
194
  npm run check # 确认没有把凭据/本机路径写进仓库
@@ -190,7 +203,7 @@ npm run check # 确认没有把凭据/本机路径写
190
203
 
191
204
  改 UI 时注意:颜色一律用 DSH 注册的主题令牌 `--dsw-alias-*`(`label-primary`、`label-secondary`、`bg-layer-1/2`、`border-l1/l2`、`brand-primary`、`state-*`),不要写死色值——否则深色主题能看、浅色主题就成了「浅灰字压白底」。在哪能看到当前有哪些令牌:DSH 里打开开发者工具,看 `:root` 上的 CSS 变量。`tests/client.test.mjs` 里有两条测试盯着这件事。
192
205
 
193
- 改代码需要的细节(调试出口、主题令牌清单、假 hub 复刻的鉴权语义、刷新退避规则、发版流程、市场收录模板)都在 [docs/MAINTAINER.md](docs/MAINTAINER.md) —— README 这里只留使用者需要知道的部分。
206
+ 改代码需要的细节(调试出口、主题令牌清单、假 hub 复刻的鉴权语义、刷新退避规则、发版流程、市场收录模板)都在 [docs/MAINTAINER.md](https://github.com/HimA6652/dsh-workbuddy2api-hub/blob/main/docs/MAINTAINER.md) —— README 这里只留使用者需要知道的部分。那份文档只在仓库里,不随 npm 包发出去。
194
207
 
195
208
  ### 与上游的关系
196
209
 
package/lib/hub.js CHANGED
@@ -227,23 +227,65 @@ export function rateText(rate) {
227
227
 
228
228
  /**
229
229
  * 解析模型单价(hub 用「倍率」计费,`credits: "x0.29"` 表示 ×0.29)。
230
+ *
231
+ * 和 {@link modelRate} 的区别:这里 **0 也算有效值**。hub 对免费模型给的是
232
+ * `credits: "x0.00"`,要把它和「压根没声明倍率」区分开,否则选择器里
233
+ * 免费模型只能显示空后缀。
230
234
  * @param model - hub 的模型条目。
231
- * @returns 正数倍率,解析不到返回 null。
235
+ * @returns 非负倍率;没声明或解析不到返回 null。
232
236
  */
233
- export function modelRate(model) {
237
+ export function modelRateValue(model) {
234
238
  if (model === null || typeof model !== 'object') return null
235
239
  const candidates = [model.credits, model.credit_rate, model.rate, model.price, model.price_rate]
236
240
  for (const raw of candidates) {
237
- if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) return raw
241
+ if (typeof raw === 'number' && Number.isFinite(raw) && raw >= 0) return raw
238
242
  if (typeof raw !== 'string') continue
239
- const match = /(\d+(?:\.\d+)?)/.exec(raw)
243
+ const text = raw.trim()
244
+ if (/^(free|免费|零|none)$/i.test(text)) return 0
245
+ const match = /(\d+(?:\.\d+)?)/.exec(text)
240
246
  if (match === null) continue
241
247
  const value = Number(match[1])
242
- if (Number.isFinite(value) && value > 0) return value
248
+ if (Number.isFinite(value) && value >= 0) return value
243
249
  }
244
250
  return null
245
251
  }
246
252
 
253
+ /**
254
+ * 解析模型单价(hub 用「倍率」计费,`credits: "x0.29"` 表示 ×0.29)。
255
+ * @param model - hub 的模型条目。
256
+ * @returns 正数倍率,解析不到(含 0)返回 null。
257
+ */
258
+ export function modelRate(model) {
259
+ const value = modelRateValue(model)
260
+ return value !== null && value > 0 ? value : null
261
+ }
262
+
263
+ /**
264
+ * 模型名后面的倍率后缀。
265
+ * @param model - hub 的模型条目。
266
+ * @returns 付费模型 `×0.29`,免费模型 `Free`,没声明倍率返回空串。
267
+ */
268
+ export function modelRateSuffix(model) {
269
+ const value = modelRateValue(model)
270
+ if (value === null) return ''
271
+ if (value <= 0) return 'Free'
272
+ return rateText(value)
273
+ }
274
+
275
+ /**
276
+ * 模型在选择器里显示的名字:展示名 + 倍率后缀。
277
+ *
278
+ * 选择器只显示 `name` 字段,倍率不落到名字里就永远看不见,所以这里拼进去;
279
+ * `id` 保持原样,路由与计费都靠它。
280
+ * @param model - hub 的模型条目。
281
+ * @returns 形如 `GLM-5.1 ×0.79` / `Hy3 Free`。
282
+ */
283
+ export function modelLabel(model) {
284
+ const base = modelDisplayName(model)
285
+ const suffix = modelRateSuffix(model)
286
+ return suffix === '' ? base : `${base} ${suffix}`
287
+ }
288
+
247
289
  /**
248
290
  * 模型的展示名。
249
291
  * @param model - hub 的模型条目。
@@ -328,7 +370,7 @@ export function toModelProfile(model, options = {}) {
328
370
  if (model === null || typeof model !== 'object') return undefined
329
371
  const id = typeof model.id === 'string' ? model.id.trim() : ''
330
372
  if (id === '') return undefined
331
- const profile = { id, name: modelDisplayName(model) }
373
+ const profile = { id, name: modelLabel(model) }
332
374
  const context = Number(model.context_length ?? model.max_input_tokens)
333
375
  if (Number.isFinite(context) && context > 0) profile.contextWindow = Math.round(context)
334
376
  const maxTokens = Number(model.max_output_tokens ?? model.max_completion_tokens)
@@ -422,7 +464,7 @@ export function toModelRow(model, selection) {
422
464
  description: typeof model?.description === 'string' ? model.description : '',
423
465
  vendor: typeof model?.vendor === 'string' ? model.vendor : '',
424
466
  rate,
425
- rateText: rateText(rate),
467
+ rateText: modelRateSuffix(model),
426
468
  contextWindow: Number(model?.context_length ?? model?.max_input_tokens) || 0,
427
469
  maxTokens: Number(model?.max_output_tokens ?? model?.max_completion_tokens) || 0,
428
470
  vision: supportsImages(model),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-workbuddy2api-hub",
3
- "version": "1.0.7",
3
+ "version": "1.0.11",
4
4
  "description": "workbuddy2api-hub 集成:接入本地 hub 面板,同步模型(配额/上下文/思考档位)、实时额度与用量,支持服务地址与 pwd 配置",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -29,9 +29,9 @@
29
29
  "check": "node scripts/check-secrets.mjs",
30
30
  "mock": "node scripts/mock-hub.mjs",
31
31
  "smoke": "node scripts/smoke-live.mjs",
32
- "install": "node scripts/install-local.mjs",
33
- "install:dry": "node scripts/install-local.mjs --dry-run",
34
- "uninstall": "node scripts/install-local.mjs --uninstall",
32
+ "link": "node scripts/install-local.mjs",
33
+ "link:dry": "node scripts/install-local.mjs --dry-run",
34
+ "unlink": "node scripts/install-local.mjs --uninstall",
35
35
  "release": "node scripts/release.mjs",
36
36
  "notes": "node scripts/release-notes.mjs",
37
37
  "prepublishOnly": "node scripts/check-secrets.mjs && npm test"
@@ -57,8 +57,6 @@
57
57
  "files": [
58
58
  "lib",
59
59
  "client",
60
- "scripts",
61
- "docs",
62
60
  "cordis.patch.yml",
63
61
  "README.md",
64
62
  "LICENSE",
@@ -87,9 +85,15 @@
87
85
  },
88
86
  "@deepseek-ai/dsh-settings": {
89
87
  "optional": true
88
+ },
89
+ "@deepseek-ai/cordis": {
90
+ "optional": true
90
91
  }
91
92
  },
92
93
  "engines": {
93
94
  "node": ">=20"
95
+ },
96
+ "dependencies": {
97
+ "dsh-workbuddy2api-hub": "^1.0.10"
94
98
  }
95
99
  }
@@ -1,116 +0,0 @@
1
- # 维护者笔记
2
-
3
- README 面向使用者(装、配、排错)。这里放**改代码时才会用到**的东西:调试入口、主题令牌、假 hub 的语义、刷新退避的实现细节、发版流程、市场收录步骤。
4
-
5
- 本机相关的路径、端口、备份清单不写在这里 —— 那些在仓库外的 `NOTES.local.md`(已被 `.gitignore` 忽略)。
6
-
7
- ## client/client.js 的调试出口
8
-
9
- bundle 末尾挂了一串 `exports.__*`,只为测试和手工排查用,宿主不依赖它们:
10
-
11
- | 出口 | 用途 |
12
- | --- | --- |
13
- | `__store` | 模块内的发布订阅 store:`get()` / `subscribe(fn)` / `start()` / `reload()` / `set(snapshot)` / `fail(message)` |
14
- | `__apiPath(path)` | 拼出插件接口的真实路径(会带上 `document.baseURI` 的部署前缀) |
15
- | `__requestJson` | HTTP 封装:超时、304 复用、非 JSON 容错 |
16
- | `__dicts` | `{ zh, en }` 两套字典,测「两种语言键集合一致」 |
17
- | `__palette` / `__styles` | 主题令牌与样式对象,测「不许写死颜色」 |
18
- | `__HubPanel` / `__HubFooter` | 两个 React 组件本体,可直接调用拿元素树 |
19
- | `__sortRowsByName` / `__footerSummary` | 表格排序与徽标取值(含 `—` 兜底) |
20
-
21
- 测试里用 `node:vm` 把 `client.js` 丢进 `runInNewContext`,沙箱只需补 `URL` / `URLSearchParams` / `document` / `fetch` / 定时器 / `AbortSignal`。假 React 的 `createElement` 返回 `{ type, props, children }`,所以 `__HubFooter({ t, wide: true })` 能直接断言行结构和文案,不用起浏览器。
22
-
23
- 注意:断言 children 数量前先过滤 `null` —— `h('div', p, a, b, cond ? c : null)` 在假 React 里会产生 `null` 项。
24
-
25
- ## 配色:只能用 `--dsw-alias-*`
26
-
27
- DSH 注册的主题令牌都在 `--dsw-alias-*` 前缀下(旧版本教程里常见的 `--dsh-*` 在本机 0.2.1-rc.2 上并不存在)。`var()` 取不到变量时会静默退到兜底值,于是「深色主题写好的浅灰兜底」在浅色主题下就成了浅灰字压白底——踩过一次。
28
-
29
- | 用途 | 令牌 |
30
- | --- | --- |
31
- | 正文 / 标题 | `--dsw-alias-label-primary` |
32
- | 次要文字、表头、提示 | `--dsw-alias-label-secondary` |
33
- | 更淡的占位文字 | `--dsw-alias-state-idle-primary` |
34
- | 卡片边框、表格线 | `--dsw-alias-border-l1` / `--dsw-alias-border-l2` |
35
- | 卡片 / 输入框 / 按钮底色 | `--dsw-alias-bg-layer-1` / `--dsw-alias-bg-layer-2` |
36
- | 主色 | `--dsw-alias-brand-primary` |
37
- | 成功 / 警告 / 错误 | `--dsw-alias-state-success-primary` / `-warn-` / `-error-primary` |
38
-
39
- 想看某个 DSH 版本实际注册了哪些令牌(DSH 内置的只读 inspect 工具):
40
-
41
- ```bash
42
- cordis_inspect_query platform=client provider=Theme method=listTokens
43
- ```
44
-
45
- `tests/client.test.mjs` 有两条测试盯着这件事:palette 的每个值必须是 `var(--dsw-alias-*, 兜底)`,且剥掉兜底后样式里不许出现写死的 `rgba(255,255,255…)` / `rgba(0,0,0…)` 底色。
46
-
47
- 同理,两个插槽的注册情况也可以直接查:
48
-
49
- ```bash
50
- cordis_inspect_query platform=client provider=Slots method=listSubTree
51
- ```
52
-
53
- ## 假 hub 复刻了哪些真实语义
54
-
55
- `scripts/mock-hub.mjs` 不是随便返回 JSON,它复刻的是真实 hub 的鉴权行为——这些正是最容易写错的地方:
56
-
57
- - 面板 Token(`X-Panel-Token`)能读所有管理接口,**但不能当 Bearer 用**:`Authorization: Bearer <panel token>` 一律 401。所以「拿 pwd 当 API Key」这条路走不通,`/v1` 必须有真 API Key。
58
- - 60 秒内 5 次登录失败返回 429,带建议等待秒数。
59
- - `/health` 恒返回 `api_key_required: true`(真实 hub 该字段表达的是「`/v1` 需要 Key」,与配了几把无关)。
60
- - `/usage` 只认 `range` / `since` / `until`,`hours` 被静默忽略(我们曾照着直觉发 `?hours=24`,实际拿到的是全量历史)。
61
- - `/settings/reveal?id=` 是唯一能拿到明文 Key 的接口,缺 `id` 就 404。
62
- - 控制端点 `/mock/state`、`/mock/reset`、`/mock/models/remove`、`/mock/models/add`、`/mock/credits`、`/mock/keys`,用来在测试里模拟「模型下架」「额度见底」;flags:`--port --password --fail-auth --no-api-key --empty --no-panel-auth --slow --quiet`。
63
-
64
- ## 刷新与退避
65
-
66
- - 宿主每 `intervalSeconds`(默认 120,允许 30–3600)打一轮:`/health` → `/panel/status` →(需要则 `/panel/login`)→ `/v1/models` → `/v1/balance-summary` → `/usage?range=today` → `/usage?range=week` → `/accounts`。
67
- - 连续失败退避(`lib/hub.js` 的 `pollIntervalMs`):`基础值 × 2^失败次数`,最多翻 5 次、上限 1800 秒;刚启动 60 秒内的失败按 ≤15 秒快试;429 时把 hub 建议的等待当作下限。
68
- - 用递归 `setTimeout` 而不是 `setInterval`:改间隔立刻生效,且上一轮没跑完不会叠请求。定时器都 `.unref()`,不拖住进程退出。
69
- - 前端每 15 秒读一次插件 `/state`(`ETag` + `If-None-Match`,304 就复用上一份),标签页在后台(`document.hidden`)时跳过。
70
- - 徽标是「缓存视图」:最旧数据 = `intervalSeconds` + 15 秒。要秒级实时就点开面板,它自己 5 秒刷一次。
71
-
72
- ## 发版
73
-
74
- ```bash
75
- # 1. 先把这一版改了什么写进 CHANGELOG.md 的 `## [1.1.0]` 一节
76
- # 2. 一条命令走完
77
- npm run release 1.1.0
78
- ```
79
-
80
- `scripts/release.mjs` 依次做:校验版本格式 → 确认 CHANGELOG 有该节 → 工作区必须干净 → 必须在 `main` → 标签不能重名 → 改 `package.json` 与 `tests/host.test.mjs` 的 `PLUGIN_VERSION` 断言 → 跑 `check-secrets` → 预演 Release 正文 → `npm test` → 提交 `chore(release): vX` → `tag -a` → 推 main 与标签。
81
-
82
- `--dry-run` 只跑到测试为止;`--no-push` 提交并打标签但不推。
83
-
84
- **标签一推,Release 自己出现**(`.github/workflows/release.yml`):Actions 先跑测试,再用 `gh release create` 建 Release,正文来自 `scripts/release-notes.mjs`(CHANGELOG 那一节 + 自动追加的安装段)。用的是 Actions 自带的 `GITHUB_TOKEN`,不需要配任何 secret。只想要正文:
85
-
86
- ```bash
87
- node scripts/release-notes.mjs v1.1.0 > notes.md
88
- ```
89
-
90
- ## 插件市场收录
91
-
92
- dsh-market 的「发现插件」读的是人工策展目录 [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)(`https://awesome-dsh-plugin.com/plugins.json`)。**发布到 GitHub ≠ 能在市场里搜到**,中间隔一次 PR 收录。
93
-
94
- 门槛(据其 `contributing.md`):
95
-
96
- - `package.json` 声明 `dsh.bundle`,仓库根有 `cordis.patch.yml`(本仓库都有);
97
- - 仓库里是真实可用的代码,不是占位;
98
- - **仓库创建满 1 天**(CI 自动检查);
99
- - 仓库加 GitHub topic `dsh-plugin`;
100
- - 单开一个 PR,只加**一个文件** `data/plugins/<owner>__<repo>.yml`:
101
-
102
- ```yaml
103
- url: https://github.com/HimA6652/dsh-workbuddy2api-hub
104
- name: HimA6652/dsh-workbuddy2api-hub
105
- category: model
106
- description:
107
- en: Registers a local workbuddy2api-hub gateway as an OpenAI-compatible provider in DSH, and shows credit balance plus today/week token, credit and API-equivalent cost in the sidebar.
108
- zh: 把本地 workbuddy2api-hub 网关注册成 DSH 的 OpenAI 兼容 provider,并在侧边栏显示积分余额与今日/本周的 tokens、积分消耗与 API 等价花费。
109
- ```
110
-
111
- 可选加分项:发布到 npm(预构建安装,免「构建授权」提示),或把预构建 tarball 挂到 Release 并用 `tarball:` 字段指过去。
112
-
113
- ## 改代码时的两个坑
114
-
115
- - **宿主侧 `lib/*.js` 改完必须重启 DSH**:`cordis-plugin-loader` 用裸 `await import(pkg)`,没有 cache-busting,ESM 缓存会一直给旧模块。
116
- - **`client/client.js` 可以热更**:`dsh-client-hmr` 会重新发布 bundle,但浏览器拿的是带 `rev` 的组合 URL(`plugins/??<pkg>/client.js&rev=<12位hash>`),`rev` 由文件 mtime/ctime/size 算出;不带 `rev` 或乱填一律 404。
@@ -1,86 +0,0 @@
1
- #!/usr/bin/env node
2
- // 发布前自查:扫描项目里有没有凭据泄漏迹象。
3
- // 用法:node scripts/check-secrets.mjs
4
- //
5
- // 只扫源码/配置/文档(.mjs/.js/.json/.yml/.md),跳过 .git、node_modules、tmp、coverage。
6
- // 命中任何一条就以退出码 1 结束,方便挂到 pre-commit 钩子或 CI 上。
7
- import { readdir, readFile } from 'node:fs/promises';
8
- import { dirname, join, relative } from 'node:path';
9
- import { fileURLToPath } from 'node:url';
10
-
11
- const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
12
- const SKIP_DIRS = new Set(['.git', 'node_modules', 'tmp', 'coverage', 'docs']);
13
- const SCAN_EXT = /\.(mjs|js|cjs|json|ya?ml|md)$/;
14
-
15
- // 任意一行出现这条标记就整行豁免(写清楚理由,别滥用)。
16
- const ALLOW_MARK = 'secret-ok';
17
-
18
- // 明显的占位/测试值:命中也不算泄漏。
19
- const PLACEHOLDER = /^(?:admin|wrong|bad|mock|fake|dummy|test|sample|example|your|placeholder|changeme|secret|password)/i;
20
-
21
- const RULES = [
22
- { name: '私钥', re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
23
- { name: 'GitHub Token', re: /gh[pousr]_[A-Za-z0-9]{20,}/ },
24
- { name: 'OpenAI 风格 Key', re: /\bsk-[A-Za-z0-9_-]{20,}/ },
25
- { name: 'AWS Key', re: /\bAKIA[0-9A-Z]{12,}/ },
26
- { name: 'Slack Token', re: /\bxox[bpoa]r?-[A-Za-z0-9-]{10,}/ },
27
- { name: '本机绝对路径', re: /[A-Za-z]:\\\\(?:Users|ProgramData)\\\\/ },
28
- { name: 'Unix 家目录路径', re: /\/(?:Users|home)\/[A-Za-z0-9._-]+\// },
29
- {
30
- name: '硬编码密码赋值',
31
- re: /(?:password|passwd|pwd)\s*[:=]\s*['"]([^'"]{8,})['"]/i,
32
- // 值是中文界面文案(如 pwd: '面板密码(pwd)')或以占位词开头 → 不算泄漏
33
- skip: (value) =>
34
- // 值是中文界面文案、含空格/括号(如 pwd: 'Panel password (pwd)')、或以占位词开头 → 不算泄漏
35
- /[\u3400-\u9fff]/.test(value) || /[\s()]/.test(value) || PLACEHOLDER.test(value),
36
- },
37
- ];
38
-
39
- /** 逐行扫描单个文件。 */
40
- async function scanFile(file) {
41
- const text = await readFile(file, 'utf8');
42
- const hits = [];
43
- text.split('\n').forEach((line, index) => {
44
- if (line.includes(ALLOW_MARK)) return;
45
- for (const rule of RULES) {
46
- const match = line.match(rule.re);
47
- if (!match) continue;
48
- if (rule.skip && rule.skip(match[1] ?? '', match)) continue;
49
- hits.push({
50
- file: relative(ROOT, file).split('\\').join('/'),
51
- line: index + 1,
52
- rule: rule.name,
53
- text: line.trim().slice(0, 120),
54
- });
55
- }
56
- });
57
- return hits;
58
- }
59
-
60
- /** 递归收集需要扫描的文件。 */
61
- async function collect(dir, out = []) {
62
- for (const entry of await readdir(dir, { withFileTypes: true })) {
63
- if (entry.isDirectory()) {
64
- if (!SKIP_DIRS.has(entry.name)) await collect(join(dir, entry.name), out);
65
- continue;
66
- }
67
- if (SCAN_EXT.test(entry.name)) out.push(join(dir, entry.name));
68
- }
69
- return out;
70
- }
71
-
72
- const files = await collect(ROOT);
73
- const results = await Promise.all(files.map(scanFile));
74
- const hits = results.flat();
75
-
76
- if (hits.length === 0) {
77
- console.log(`OK:扫描 ${files.length} 个文件,未发现疑似凭据。`);
78
- } else {
79
- console.log(`发现 ${hits.length} 处疑似凭据:\n`);
80
- for (const hit of hits) {
81
- console.log(`${hit.file}:${hit.line} [${hit.rule}]`);
82
- console.log(` ${hit.text}`);
83
- }
84
- console.log('\n确认是假数据就在本文件上方 RULES 里豁免,或改成占位符;确认是真凭据则先作废再改代码。');
85
- process.exitCode = 1;
86
- }
@@ -1,182 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * 把本仓库挂进本地 DSH profile(桌面端默认 profile:`~/.dsh/profiles/desktop`)。
4
- *
5
- * 做的事情只有三件,全部可逆(`--uninstall` 原样退回去):
6
- * 1. 在 profile 的 `package.json` 里加一条 `file:` 依赖与一条 bundle 声明;
7
- * 2. 在 profile 的 `node_modules/` 下建一个指向本仓库的链接;
8
- * 3. 打印下一步(重启 DSH 才会加载)。
9
- *
10
- * 不动 `cordis.patch.yml`:插件的挂载点由本包的 `cordis.patch.yml` 自己 insert。
11
- *
12
- * 用法:
13
- * node scripts/install-local.mjs # 安装
14
- * node scripts/install-local.mjs --dry-run # 只看会改什么
15
- * node scripts/install-local.mjs --uninstall # 卸载
16
- * node scripts/install-local.mjs --profile D:\path\to\profile
17
- */
18
-
19
- import { existsSync } from 'node:fs'
20
- import { readFile, rm, symlink, writeFile, mkdir } from 'node:fs/promises'
21
- import { homedir } from 'node:os'
22
- import { dirname, join, resolve } from 'node:path'
23
- import { fileURLToPath } from 'node:url'
24
-
25
- const HERE = dirname(fileURLToPath(import.meta.url))
26
- const PROJECT = resolve(HERE, '..')
27
-
28
- /**
29
- * 解析命令行参数。
30
- * @param argv - `process.argv.slice(2)`。
31
- * @returns 选项对象。
32
- */
33
- function parseArgs(argv) {
34
- const options = { uninstall: false, dryRun: false, profile: '' }
35
- for (let index = 0; index < argv.length; index += 1) {
36
- const arg = argv[index]
37
- if (arg === '--uninstall') options.uninstall = true
38
- else if (arg === '--dry-run') options.dryRun = true
39
- else if (arg === '--profile') {
40
- options.profile = argv[index + 1] ?? ''
41
- index += 1
42
- } else if (arg === '--help' || arg === '-h') options.help = true
43
- }
44
- return options
45
- }
46
-
47
- /**
48
- * 找到要装的 profile 目录。
49
- * @param explicit - `--profile` 传入的值。
50
- * @returns 绝对路径。
51
- */
52
- function profileDirOf(explicit) {
53
- if (typeof explicit === 'string' && explicit !== '') return resolve(explicit)
54
- const name = process.env.DSH_PROFILE !== undefined && process.env.DSH_PROFILE !== '' ? process.env.DSH_PROFILE : 'desktop'
55
- return join(homedir(), '.dsh', 'profiles', name)
56
- }
57
-
58
- /**
59
- * 读 profile 的 package.json。
60
- * @param file - 路径。
61
- * @returns 解析后的对象。
62
- */
63
- async function readProfile(file) {
64
- const text = await readFile(file, 'utf8')
65
- const parsed = JSON.parse(text)
66
- if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
67
- throw new Error(`${file} 不是一个 JSON 对象,先手动看一眼`)
68
- }
69
- return parsed
70
- }
71
-
72
- /** 打印一行「会做什么」。 */
73
- function plan(action, detail) {
74
- console.log(` ${action.padEnd(8, ' ')} ${detail}`)
75
- }
76
-
77
- /** 主流程。 */
78
- async function main() {
79
- const options = parseArgs(process.argv.slice(2))
80
- if (options.help) {
81
- console.log('用法:node scripts/install-local.mjs [--dry-run] [--uninstall] [--profile <dir>]')
82
- return 0
83
- }
84
-
85
- const manifest = JSON.parse(await readFile(join(PROJECT, 'package.json'), 'utf8'))
86
- const packageName = manifest.name
87
- const profileDir = profileDirOf(options.profile)
88
- const packageFile = join(profileDir, 'package.json')
89
- const linkPath = join(profileDir, 'node_modules', packageName)
90
- const stateFile = join(profileDir, 'workbuddy2api-hub.json')
91
-
92
- if (!existsSync(packageFile)) {
93
- console.error(`找不到 profile:${packageFile}`)
94
- console.error('DSH 桌面端默认在 ~/.dsh/profiles/desktop;用 --profile 指定其它 profile。')
95
- return 2
96
- }
97
-
98
- const profile = await readProfile(packageFile)
99
- profile.dependencies = profile.dependencies ?? {}
100
- profile.dsh = profile.dsh ?? {}
101
- profile.dsh.profile = profile.dsh.profile ?? {}
102
- const bundles = Array.isArray(profile.dsh.profile.bundles) ? profile.dsh.profile.bundles : []
103
-
104
- console.log(options.uninstall ? '卸载 dsh-workbuddy2api-hub' : '安装 dsh-workbuddy2api-hub')
105
- console.log(` profile ${profileDir}`)
106
- console.log(` 插件 ${PROJECT}`)
107
-
108
- const changes = []
109
- if (options.uninstall) {
110
- if (packageName in profile.dependencies) {
111
- delete profile.dependencies[packageName]
112
- changes.push(['删除依赖', packageName])
113
- }
114
- const nextBundles = bundles.filter((entry) => entry !== packageName)
115
- if (nextBundles.length !== bundles.length) {
116
- profile.dsh.profile.bundles = nextBundles
117
- changes.push(['移除 bundle', packageName])
118
- }
119
- if (existsSync(linkPath)) changes.push(['删除链接', linkPath])
120
- if (existsSync(stateFile)) changes.push(['保留状态', `${stateFile}(要一起删就手动 rm)`])
121
- } else {
122
- const spec = `file:${PROJECT.replaceAll('\\', '/')}`
123
- if (profile.dependencies[packageName] !== spec) {
124
- profile.dependencies[packageName] = spec
125
- changes.push(['写入依赖', `${packageName} → ${spec}`])
126
- }
127
- if (!bundles.includes(packageName)) {
128
- profile.dsh.profile.bundles = [...bundles, packageName]
129
- changes.push(['追加 bundle', packageName])
130
- }
131
- changes.push(['建立链接', `${linkPath} → ${PROJECT}`])
132
- }
133
-
134
- if (changes.length === 0) console.log(' 已经是目标状态,无需改动。')
135
- for (const [action, detail] of changes) plan(action, detail)
136
-
137
- if (options.dryRun) {
138
- console.log('--dry-run:没有写入任何文件。')
139
- return 0
140
- }
141
-
142
- // 先备份,再改 package.json:profile 起不来是最坏结果,得像改生产配置那样小心。
143
- if (changes.length > 0) {
144
- const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, '')
145
- const backup = `${packageFile}.bak-workbuddy2api-hub-${stamp}`
146
- await writeFile(backup, await readFile(packageFile))
147
- console.log(` 备份 ${backup}`)
148
- await writeFile(packageFile, `${JSON.stringify(profile, null, 2)}\n`)
149
- }
150
-
151
- if (options.uninstall) {
152
- await rm(linkPath, { recursive: true, force: true })
153
- } else {
154
- await mkdir(dirname(linkPath), { recursive: true })
155
- await rm(linkPath, { recursive: true, force: true })
156
- // Windows 上没有管理员权限建目录符号链接会失败,junction 不需要额外权限。
157
- await symlink(PROJECT, linkPath, process.platform === 'win32' ? 'junction' : 'dir')
158
- }
159
-
160
- console.log('')
161
- if (options.uninstall) {
162
- console.log('已卸载。重启 DSH 之后设置页里的这一块就没了。')
163
- } else {
164
- console.log('装好了。接下来:')
165
- console.log(' 1. 重启 DSH 桌面端(插件只在启动时加载)')
166
- console.log(' 2. 打开 设置 → workbuddy2api-hub,确认地址是 http://127.0.0.1:8788、密码是 admin')
167
- console.log(' 3. 点「测试连接」,再点「立即同步」')
168
- console.log(' 4. 模型会出现在 设置 → 模型 里,provider 名为 workbuddy2api-hub')
169
- console.log('')
170
- console.log('前置条件:本地 workbuddy2api-hub 服务已经跑起来(默认 http://127.0.0.1:8788)。')
171
- }
172
- return 0
173
- }
174
-
175
- main()
176
- .then((code) => {
177
- process.exitCode = code
178
- })
179
- .catch((error) => {
180
- console.error(`安装脚本出错:${error?.message ?? error}`)
181
- process.exitCode = 1
182
- })