cortico-world-workbuddy 0.1.6 → 0.1.7
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 +22 -1
- package/README.md +25 -4
- package/describe.json +11 -6
- package/dist/webui.js +53 -7
- package/package.json +1 -1
- package/scripts/verify-ui.mjs +17 -0
- package/src/ENV_PROMPT.md +1 -0
- package/src/config.js +13 -0
- package/src/safety.js +168 -0
- package/src/upstream.js +2 -0
- package/src/webui/client.js +61 -5
- package/src/world.js +91 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
+
## 0.1.7
|
|
4
|
+
|
|
5
|
+
按 WorkBuddy 官方对话给出的合规口径,给桥接加「安全清单」闸门(仍只在本机,未推送 / 未发布)。
|
|
6
|
+
|
|
7
|
+
- **新增 `src/safety.js`**:把 WorkBuddy 的筛选原则固化成 `SAFE_MODEL_IDS`(21 个公开对话模型)、
|
|
8
|
+
分组表 `SAFE_MODEL_GROUPS`、谨慎档 `CAUTION_MODEL_IDS`(hy3 系列与 preview)与 `classifyModel()`。
|
|
9
|
+
`classifyModel` 把目录里每个模型判成 `safe / caution / alias / disabled / media / code / retired /
|
|
10
|
+
format / experimental / unknown` 十档,附中文理由;`allow` 只对前两档为真。
|
|
11
|
+
- **闸门默认开启**(`worlds.workbuddy.safeOnly = true`):`wb_chat`、`wb_select_model` 与面板的
|
|
12
|
+
「使用」按钮都会拒绝安全清单外的模型,并给出「判定档位 + 理由 + 如何解除」的说明;
|
|
13
|
+
关掉这个配置才能调用 disabled / 内部别名 / 预览等模型,风险自负。
|
|
14
|
+
- **`wb_list_models` 默认只列安全清单**(按 DeepSeek / GLM / 混元 / Kimi / 其他分组,带此刻倍率),
|
|
15
|
+
传 `all=true` 才追加其余模型及其排除理由。
|
|
16
|
+
- **被排除的典型项**:`wb-lite`(目录 `disabled:true`)、`auto` / `fast-model` / `balanced-model` /
|
|
17
|
+
`deep-model`(内部路由别名)、`hunyuan-image-alpha*` / `kling-video-v3` / `minimax-video-h3`(媒体)、
|
|
18
|
+
`codewise-*`(代码补全)、`hy4-preview*`(实验预览)、19 个已下架(11102)、`glm-5.0-turbo`(11150)。
|
|
19
|
+
- **模型目录面板**新增「合规」列(安全清单 / 安全清单·谨慎 / 各排除档位,悬停看理由),
|
|
20
|
+
默认只显示安全清单,勾选「显示全部模型(含不建议项)」才展开;不安全项的选择按钮置灰。
|
|
21
|
+
- `envPromptVars()` 增加 `workbuddy.safeOnly`;`ENV_PROMPT.md` 增加一条:闸门开着时不要绕过拒绝,
|
|
22
|
+
应该告诉用户原因并改推荐安全清单里的模型。
|
|
23
|
+
- `describe.json` 的 `envPrompt` 与 `wb_list_models` 描述同步(多了 `all` 参数)。
|
|
24
|
+
|
|
3
25
|
## 0.1.6
|
|
4
26
|
|
|
5
27
|
新增「用量」面板与真实的时段优惠计算(只在本机验证,未发布)。
|
|
@@ -22,7 +44,6 @@
|
|
|
22
44
|
- `wb_list_models` 标注此刻生效倍率(含原价对比、时段)与优惠徽标;`wb_select_model` 提示此刻倍率。
|
|
23
45
|
- 环境提示词新增 `{{workbuddy.todayCredit}}` / `{{workbuddy.todayTokens}}`,并在倍率旁说明已含生效中的限时优惠。
|
|
24
46
|
- 详情页徽标增加「今日积分」。
|
|
25
|
-
- 仓库补 `package-lock.json`(锁定 esbuild 0.24.2),CI 改为 `npm ci`:`dist` 的构建产物必须逐字节稳定,否则「dist 未同步」的检查会误报。
|
|
26
47
|
|
|
27
48
|
## 0.1.5
|
|
28
49
|
|
package/README.md
CHANGED
|
@@ -139,11 +139,31 @@ Coopanion (Coo)
|
|
|
139
139
|
|
|
140
140
|
| 面板 | 内容 |
|
|
141
141
|
|---|---|
|
|
142
|
-
| 模型目录 | 刷新目录;逐模型一行:名称/id
|
|
142
|
+
| 模型目录 | 刷新目录;逐模型一行:名称/id、**合规档位**、**此刻倍率**(含限时折扣时显示 `x0.5 ← x0.79 23:00–07:50`,x0 标「免费」)、上下文、推理档、「选用」按钮;默认只列安全清单,勾选「显示全部模型(含不建议项)」才展开其余项 |
|
|
143
143
|
| 账号 | 登录态来源(本机发现 / 手动)、掩码 token、积分余额查询、重新检测、手动 token 存取 |
|
|
144
144
|
| 是否启用 WorkBuddy | 「中转站优先」开关 + 主进程状态,与开始页按钮同步 |
|
|
145
145
|
| 用量 | 区间概览(今天 / 近 7 天 / 近 30 天)、按模型聚合、积分包明细、限时优惠清单、最近调用;带「刷新余额」「清空本地账本」 |
|
|
146
146
|
|
|
147
|
+
### 安全清单(合规闸门)
|
|
148
|
+
|
|
149
|
+
WorkBuddy 官方对话给出的口径是:只调用「官方公开目录里、当前可正常对话」的模型,
|
|
150
|
+
`disabled`、内部路由别名、媒体/代码补全与状态不明的模型都不要经外部软件调用,否则有风控/封号风险。
|
|
151
|
+
这里把它落成代码:
|
|
152
|
+
|
|
153
|
+
- 白名单 21 个(`src/safety.js` 的 `SAFE_MODEL_IDS`,按 DeepSeek / GLM / 混元 / Kimi / 其他分组):
|
|
154
|
+
`deepseek-v4.1-flash`、`deepseek-v4-flash`、`deepseek-v4-pro`、`deepseek-v3-2-volc`、
|
|
155
|
+
`glm-5.3-flash`、`glm-5.3`、`glm-5.2`、`hy3`、`hy3-b`、`hy3-c`、`hy3-x`、`hunyuan-chat`、
|
|
156
|
+
`kimi-k2.5`、`kimi-k2.8-preview`、`kimi-k3-1`、`kimi-k3.1-pro`、`minimax-m2.7`、
|
|
157
|
+
`minimax-m3.1-flash`、`mimo-v2.6-pro`、`space-bunny`、`step-5-preview`。
|
|
158
|
+
其中 `hy3*` 与 `*preview` 标为「谨慎」(免费试用/预览,别做高频自动化)。
|
|
159
|
+
- 其余全部**拒绝**:`wb-lite`(目录 `disabled:true`)、`auto` / `fast-model` / `balanced-model` /
|
|
160
|
+
`deep-model`(内部路由别名)、4 个媒体模型、`codewise-*`、`hy4-preview*`、19 个已下架的、
|
|
161
|
+
`glm-5.0-turbo`(11150 请求格式不符)。
|
|
162
|
+
- 闸门是 `worlds.workbuddy.safeOnly`(默认 `true`)。开着时 `wb_chat` / `wb_select_model` / 面板「选用」
|
|
163
|
+
三处一致拒绝,并说明「判定档位 + 理由 + 怎么解除」;关掉才放行,属于明示的风险自负。
|
|
164
|
+
- 清单是按 2026-11 的官方目录定的,官方加新模型不会被自动信任——这正是「未列入安全清单」这一档的意义。
|
|
165
|
+
|
|
166
|
+
|
|
147
167
|
### 倍率与限时优惠怎么算
|
|
148
168
|
|
|
149
169
|
- 目录来自 `GET /v3/config`(应用身份 UA):一次同时拿到 `models`、`modelPromotions`、`modelTiers`。
|
|
@@ -167,9 +187,9 @@ Coopanion (Coo)
|
|
|
167
187
|
|
|
168
188
|
| 工具 | 作用 |
|
|
169
189
|
|---|---|
|
|
170
|
-
| `wb_list_models` |
|
|
171
|
-
| `wb_select_model` | 按 id 选择 `wb_chat`
|
|
172
|
-
| `wb_chat` | 用选中模型回答一个单轮问题(**真实消耗积分**,需主进程在跑);回执附输入/输出/缓存 token
|
|
190
|
+
| `wb_list_models` | 列出**安全清单内**的模型及此刻倍率(含限时优惠与时段),并单列优惠清单;传 `all=true` 才带上被排除项与理由(只读) |
|
|
191
|
+
| `wb_select_model` | 按 id 选择 `wb_chat` 的默认模型,持久化;提示此刻倍率;安全清单外会被拒 |
|
|
192
|
+
| `wb_chat` | 用选中模型回答一个单轮问题(**真实消耗积分**,需主进程在跑);回执附输入/输出/缓存 token 与本次积分;安全清单外会被拒 |
|
|
173
193
|
| `wb_credits` | 只读查询积分余额与本周期已用(需主进程在跑) |
|
|
174
194
|
|
|
175
195
|
## 配置(`worlds.workbuddy.*`)
|
|
@@ -184,6 +204,7 @@ Coopanion (Coo)
|
|
|
184
204
|
| `envPrompt` | `true` | 是否把桥接状态写进 system 前缀(关掉后「中转站优先」也不生效) |
|
|
185
205
|
| `prefer` | `false` | 「中转站优先」,由开始页按钮 / 详情页开关写入 |
|
|
186
206
|
| `hostInject` | `true` | 是否注入「开始」页按钮(关掉后不碰宿主文件,只用详情页开关) |
|
|
207
|
+
| `safeOnly` | `true` | 只允许调用安全清单内的公开对话模型;关掉会放行 disabled / 别名 / 预览等项,风险自负 |
|
|
187
208
|
|
|
188
209
|
## 开发与校验
|
|
189
210
|
|
package/describe.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"tools": [
|
|
3
3
|
{
|
|
4
4
|
"name": "wb_list_models",
|
|
5
|
-
"description": "List the WorkBuddy models
|
|
5
|
+
"description": "List the WorkBuddy chat models that are safe to call (official public catalog only), each with its credit multiplier (e.g. x0.79, already including time-limited discounts), context window and reasoning efforts. Read-only; costs nothing. Pass all=true to also list models that are disabled, internal routing aliases, media/code models or delisted ones — those are not callable through this bridge while worlds.workbuddy.safeOnly is on.",
|
|
6
6
|
"summary": {
|
|
7
|
-
"zh": "
|
|
8
|
-
"en": "Lists WorkBuddy models
|
|
7
|
+
"zh": "列出 WorkBuddy 公开对话模型(安全清单)及各自的积分倍率、上下文与推理档;all=true 时附上被排除的项与原因。",
|
|
8
|
+
"en": "Lists the safe WorkBuddy chat models with multiplier, context and reasoning efforts; all=true also lists excluded ones with reasons."
|
|
9
9
|
},
|
|
10
10
|
"tags": [
|
|
11
11
|
"read"
|
|
@@ -13,12 +13,17 @@
|
|
|
13
13
|
"parameters": {
|
|
14
14
|
"type": "object",
|
|
15
15
|
"additionalProperties": false,
|
|
16
|
-
"properties": {
|
|
16
|
+
"properties": {
|
|
17
|
+
"all": {
|
|
18
|
+
"type": "boolean",
|
|
19
|
+
"description": "Also list models outside the safe list (shown with their risk reason)."
|
|
20
|
+
}
|
|
21
|
+
}
|
|
17
22
|
}
|
|
18
23
|
},
|
|
19
24
|
{
|
|
20
25
|
"name": "wb_select_model",
|
|
21
|
-
"description": "Select which WorkBuddy model wb_chat should use by default. The id must come from wb_list_models. Persists across restarts.",
|
|
26
|
+
"description": "Select which WorkBuddy model wb_chat should use by default. The id must come from wb_list_models. Refused when worlds.workbuddy.safeOnly is on and the model is outside the safe list. Persists across restarts.",
|
|
22
27
|
"summary": {
|
|
23
28
|
"zh": "选择 wb_chat 默认调用的 WorkBuddy 模型。",
|
|
24
29
|
"en": "Selects the default WorkBuddy model for wb_chat."
|
|
@@ -98,5 +103,5 @@
|
|
|
98
103
|
}
|
|
99
104
|
}
|
|
100
105
|
],
|
|
101
|
-
"envPrompt": "## WorkBuddy bridge\n\nThis machine has a WorkBuddy desktop sign-in bridged in (region: {{workbuddy.region}}; signed in: {{workbuddy.authed}}).\nThe model catalog holds {{workbuddy.catalogSize}} models. The currently selected model is `{{workbuddy.model}}` with default credit multiplier {{workbuddy.multiplier}}.\nWorkBuddy desktop main process: {{workbuddy.process}}. Relay-first switch: {{workbuddy.priority}}.\nSpent today through the bridge: {{workbuddy.todayCredit}} credits / {{workbuddy.todayTokens}} tokens (locally recorded).\nThe multiplier above already includes any time-limited discount or free window that is active right now; `wb_list_models` also marks which offers are active and their hours.\n\n- Use `wb_list_models` to see every model with its multiplier before choosing; prefer low-multiplier models for casual tasks.\n- Use `wb_select_model` when the user asks to switch the WorkBuddy model.\n- `wb_chat` only works while the WorkBuddy desktop main process is running; otherwise it returns「未检测到 WorkBuddy 主进程」. Every call spends the user's own credits at that model's multiplier.\n- `wb_credits` is read-only and costs nothing (also requires the desktop process).\n\n### Relay first: {{workbuddy.priority}}\n\n- Switch `on` **and** process `running`: every user turn that expects a conversational answer (question, chat, writing, reasoning, rewrite, translation, summary) must be answered by a WorkBuddy model first — call `wb_chat` (omit `model` to use `{{workbuddy.model}}`), then speak the returned answer back in your own voice and name the model that answered. Only fall back to your own model if the call fails.\n- Switch `off` (default): do not call `wb_chat`; answer with your own model. If the user explicitly asks for WorkBuddy, `wb_chat` is still allowed — the desktop process must be running.\n- Tool errors and empty answers are not a reason to flip the switch; just say what happened.\n"
|
|
106
|
+
"envPrompt": "## WorkBuddy bridge\n\nThis machine has a WorkBuddy desktop sign-in bridged in (region: {{workbuddy.region}}; signed in: {{workbuddy.authed}}).\nThe model catalog holds {{workbuddy.catalogSize}} models. The currently selected model is `{{workbuddy.model}}` with default credit multiplier {{workbuddy.multiplier}}.\nWorkBuddy desktop main process: {{workbuddy.process}}. Relay-first switch: {{workbuddy.priority}}.\nSpent today through the bridge: {{workbuddy.todayCredit}} credits / {{workbuddy.todayTokens}} tokens (locally recorded).\nThe multiplier above already includes any time-limited discount or free window that is active right now; `wb_list_models` also marks which offers are active and their hours.\n\n- Use `wb_list_models` to see every model with its multiplier before choosing; prefer low-multiplier models for casual tasks.\n- Safe-list gate: {{workbuddy.safeOnly}}. When it is `on`, only the models WorkBuddy publishes as public chat models are callable — `disabled` entries, internal routing aliases (`auto`, `fast-model`, `balanced-model`, `deep-model`), media/code models and delisted ones are refused. Do not try to work around a refusal; tell the user why and offer a safe model instead. `wb_list_models` with `all=true` shows the refused ones with reasons.\n- Use `wb_select_model` when the user asks to switch the WorkBuddy model.\n- `wb_chat` only works while the WorkBuddy desktop main process is running; otherwise it returns「未检测到 WorkBuddy 主进程」. Every call spends the user's own credits at that model's multiplier.\n- `wb_credits` is read-only and costs nothing (also requires the desktop process).\n\n### Relay first: {{workbuddy.priority}}\n\n- Switch `on` **and** process `running`: every user turn that expects a conversational answer (question, chat, writing, reasoning, rewrite, translation, summary) must be answered by a WorkBuddy model first — call `wb_chat` (omit `model` to use `{{workbuddy.model}}`), then speak the returned answer back in your own voice and name the model that answered. Only fall back to your own model if the call fails.\n- Switch `off` (default): do not call `wb_chat`; answer with your own model. If the user explicitly asks for WorkBuddy, `wb_chat` is still allowed — the desktop process must be running.\n- Tool errors and empty answers are not a reason to flip the switch; just say what happened.\n"
|
|
102
107
|
}
|
package/dist/webui.js
CHANGED
|
@@ -77,7 +77,14 @@ var TEXT = {
|
|
|
77
77
|
noData: "\u6682\u65E0\u6570\u636E",
|
|
78
78
|
totalCredit: "\u5408\u8BA1\u79EF\u5206",
|
|
79
79
|
todayCredit: "\u4ECA\u65E5\u79EF\u5206",
|
|
80
|
-
todayTokens: "\u4ECA\u65E5 token"
|
|
80
|
+
todayTokens: "\u4ECA\u65E5 token",
|
|
81
|
+
colSafety: "\u5408\u89C4",
|
|
82
|
+
safetySafe: "\u5B89\u5168\u6E05\u5355",
|
|
83
|
+
safetyCaution: "\u5B89\u5168\u6E05\u5355\xB7\u8C28\u614E",
|
|
84
|
+
showAllModels: "\u663E\u793A\u5168\u90E8\u6A21\u578B\uFF08\u542B\u4E0D\u5EFA\u8BAE\u9879\uFF09",
|
|
85
|
+
unsafePick: "\u8BE5\u6A21\u578B\u4E0D\u5728\u5B89\u5168\u6E05\u5355\u5185\uFF0C\u5DF2\u62D2\u7EDD\u9009\u62E9",
|
|
86
|
+
safeOnlyOn: "\u4EC5\u8C03\u7528\u5B89\u5168\u6E05\u5355\u6A21\u578B",
|
|
87
|
+
safeOnlyOff: "safeOnly \u5DF2\u5173\u95ED\uFF1A\u5168\u90E8\u6A21\u578B\u53EF\u8C03\u7528\uFF08\u542B WorkBuddy \u4E0D\u5EFA\u8BAE\u5916\u90E8\u8C03\u7528\u7684\u9879\uFF09"
|
|
81
88
|
},
|
|
82
89
|
en: {
|
|
83
90
|
refresh: "Refresh from WorkBuddy",
|
|
@@ -156,7 +163,14 @@ var TEXT = {
|
|
|
156
163
|
noData: "No data",
|
|
157
164
|
totalCredit: "Total credits",
|
|
158
165
|
todayCredit: "Credits today",
|
|
159
|
-
todayTokens: "Tokens today"
|
|
166
|
+
todayTokens: "Tokens today",
|
|
167
|
+
colSafety: "Policy",
|
|
168
|
+
safetySafe: "Safe list",
|
|
169
|
+
safetyCaution: "Safe list \xB7 caution",
|
|
170
|
+
showAllModels: "Show all models (including discouraged ones)",
|
|
171
|
+
unsafePick: "This model is outside the safe list and cannot be selected",
|
|
172
|
+
safeOnlyOn: "Safe-list only",
|
|
173
|
+
safeOnlyOff: "safeOnly is off: every model is callable (including ones WorkBuddy discourages)"
|
|
160
174
|
}
|
|
161
175
|
};
|
|
162
176
|
function strings(ctx) {
|
|
@@ -190,33 +204,42 @@ function mountModels(ctx) {
|
|
|
190
204
|
const { ui } = ctx;
|
|
191
205
|
const sheet = ui.sheet({
|
|
192
206
|
title: s.colModel === "\u6A21\u578B" ? "WorkBuddy \u6A21\u578B\u76EE\u5F55" : "WorkBuddy model catalog",
|
|
193
|
-
desc: s.
|
|
207
|
+
desc: s.colModel === "\u6A21\u578B" ? "\u9ED8\u8BA4\u53EA\u5217 WorkBuddy \u516C\u5F00\u5BF9\u8BDD\u6A21\u578B\uFF08\u5B89\u5168\u6E05\u5355\uFF09\uFF1B\u52FE\u9009\u300C\u663E\u793A\u5168\u90E8\u6A21\u578B\u300D\u53EF\u67E5\u770B\u88AB\u6392\u9664\u9879\u53CA\u5176\u7406\u7531\u3002" : 'Only WorkBuddy public chat models are listed by default; tick "Show all models" to see excluded ones and why.'
|
|
194
208
|
});
|
|
195
209
|
ctx.root.append(sheet.el);
|
|
196
210
|
const head = ui.rowbar();
|
|
197
211
|
const refreshBtn = ui.button(s.refresh, { variant: "primary" });
|
|
198
212
|
const stateChip = ui.chip("", "plain");
|
|
199
213
|
const currentChip = ui.chip("", "accent");
|
|
200
|
-
|
|
214
|
+
const allBox = ui.checkbox(s.showAllModels, { checked: false });
|
|
215
|
+
head.append(refreshBtn, stateChip, currentChip, allBox.el);
|
|
201
216
|
sheet.body.append(head);
|
|
202
217
|
const errLine = ui.msgline();
|
|
203
218
|
sheet.body.append(errLine);
|
|
204
219
|
const table = ui.table({
|
|
205
|
-
head: [s.colModel, s.colMultiplier, s.colContext, s.colReasoning, s.colAction],
|
|
220
|
+
head: [s.colModel, s.colSafety, s.colMultiplier, s.colContext, s.colReasoning, s.colAction],
|
|
206
221
|
maxHeight: "calc(100vh - 320px)"
|
|
207
222
|
});
|
|
208
223
|
sheet.body.append(table.el);
|
|
209
224
|
let state = null;
|
|
210
225
|
function renderChips() {
|
|
211
|
-
|
|
226
|
+
const models = state?.models ?? [];
|
|
227
|
+
const safeCount = models.filter((m) => m.safety?.allow).length;
|
|
228
|
+
stateChip.textContent = `${s.lastRefresh}: ${fmtTs(state?.catalogAt, s)} \xB7 ${safeCount}/${models.length} ${s.models} \xB7 ${state?.safeOnly === false ? s.safeOnlyOff : s.safeOnlyOn}`;
|
|
212
229
|
currentChip.textContent = `${s.currentModel}: ${state?.model || "\u2014"}`;
|
|
213
230
|
}
|
|
231
|
+
function pickable(m) {
|
|
232
|
+
if (state?.safeOnly === false) return true;
|
|
233
|
+
return m.safety?.allow !== false;
|
|
234
|
+
}
|
|
214
235
|
function renderTable() {
|
|
215
|
-
const
|
|
236
|
+
const all = state?.models ?? [];
|
|
237
|
+
const models = allBox.checked ? all : all.filter((m) => m.safety?.allow);
|
|
216
238
|
table.clear(models.length ? void 0 : s.empty);
|
|
217
239
|
for (const m of models) {
|
|
218
240
|
const mul = fmtMultiplier(m, s);
|
|
219
241
|
const isCurrent = state.model === m.id;
|
|
242
|
+
const safety = m.safety ?? {};
|
|
220
243
|
const nameCell = ui.h("div");
|
|
221
244
|
nameCell.append(ui.h("div", null, m.name || m.id));
|
|
222
245
|
const sub = ui.h("div", "dim", m.id);
|
|
@@ -224,12 +247,26 @@ function mountModels(ctx) {
|
|
|
224
247
|
sub.style.opacity = "0.65";
|
|
225
248
|
nameCell.append(sub);
|
|
226
249
|
if (m.description) nameCell.title = m.description;
|
|
250
|
+
const safePill = ui.pill(
|
|
251
|
+
safety.label ?? "\u2014",
|
|
252
|
+
safety.allow ? safety.caution ? "plain" : "on" : "plain"
|
|
253
|
+
);
|
|
254
|
+
if (safety.reason) safePill.title = safety.reason;
|
|
255
|
+
if (!safety.allow) {
|
|
256
|
+
safePill.style.opacity = "0.6";
|
|
257
|
+
nameCell.style.opacity = "0.6";
|
|
258
|
+
}
|
|
227
259
|
const mulPill = ui.pill(mul.text, mul.free ? "on" : "plain");
|
|
228
260
|
if (mul.promo) mulPill.title = mul.promo;
|
|
261
|
+
const allowed = pickable(m);
|
|
229
262
|
const btn = ui.button(isCurrent ? s.selected : s.select, {
|
|
230
263
|
size: "sm",
|
|
231
264
|
variant: isCurrent ? "plain" : "primary",
|
|
232
265
|
onClick: async () => {
|
|
266
|
+
if (!allowed) {
|
|
267
|
+
ui.toast(`${s.unsafePick}${safety.reason ? `\uFF1A${safety.reason}` : ""}`, "bad");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
233
270
|
const off = ui.disable(btn);
|
|
234
271
|
try {
|
|
235
272
|
const r = await ctx.invoke("select", [m.id]);
|
|
@@ -249,8 +286,13 @@ function mountModels(ctx) {
|
|
|
249
286
|
}
|
|
250
287
|
});
|
|
251
288
|
if (isCurrent) btn.disabled = true;
|
|
289
|
+
if (!allowed) {
|
|
290
|
+
btn.disabled = true;
|
|
291
|
+
btn.title = safety.reason ?? "";
|
|
292
|
+
}
|
|
252
293
|
table.addRow([
|
|
253
294
|
nameCell,
|
|
295
|
+
safePill,
|
|
254
296
|
mulPill,
|
|
255
297
|
m.contextWindow ? String(m.contextWindow) : "\u2014",
|
|
256
298
|
m.reasoning?.supportedEfforts?.join(" / ") ?? "\u2014",
|
|
@@ -258,6 +300,10 @@ function mountModels(ctx) {
|
|
|
258
300
|
]);
|
|
259
301
|
}
|
|
260
302
|
}
|
|
303
|
+
allBox.input.addEventListener("change", () => {
|
|
304
|
+
renderChips();
|
|
305
|
+
renderTable();
|
|
306
|
+
});
|
|
261
307
|
async function load() {
|
|
262
308
|
state = await ctx.invoke("state");
|
|
263
309
|
renderChips();
|
package/package.json
CHANGED
package/scripts/verify-ui.mjs
CHANGED
|
@@ -356,6 +356,23 @@ for (let run = 1; run <= RUNS; run += 1) {
|
|
|
356
356
|
row.checks.usageRows = usage.rows;
|
|
357
357
|
row.checks.usageChip = usage.chip;
|
|
358
358
|
if (SHOTS && run === 1) { await sleep(300); await shot('world-usage'); }
|
|
359
|
+
// --- 模型目录:合规列 + 默认只列安全清单 ---
|
|
360
|
+
await evaluate(clickByText('模型目录'));
|
|
361
|
+
const modelsView = await waitFor('模型目录渲染', async () => {
|
|
362
|
+
const v = await evaluate(`(() => {
|
|
363
|
+
const th = [...document.querySelectorAll('th')].map((n) => n.textContent.trim());
|
|
364
|
+
const rows = document.querySelectorAll('table tr').length;
|
|
365
|
+
const safePills = [...document.querySelectorAll('td span')].filter((n) => /安全清单/.test(n.textContent || '')).length;
|
|
366
|
+
const allBox = [...document.querySelectorAll('input[type="checkbox"]')].length;
|
|
367
|
+
return { hasPolicy: th.includes('合规'), headers: th.join('|'), rows, safePills, checkboxes: allBox };
|
|
368
|
+
})()`);
|
|
369
|
+
return v.hasPolicy && v.rows > 10 ? v : false;
|
|
370
|
+
});
|
|
371
|
+
row.checks.modelsPolicyColumn = modelsView.hasPolicy;
|
|
372
|
+
row.checks.modelsRows = modelsView.rows;
|
|
373
|
+
row.checks.modelsSafePills = modelsView.safePills;
|
|
374
|
+
row.checks.modelsHasShowAll = modelsView.checkboxes >= 1;
|
|
375
|
+
if (SHOTS && run === 1) { await sleep(300); await shot('world-models'); }
|
|
359
376
|
if (KEEP) await shot(`run${run}-world`);
|
|
360
377
|
void pane;
|
|
361
378
|
} catch (err) {
|
package/src/ENV_PROMPT.md
CHANGED
|
@@ -7,6 +7,7 @@ Spent today through the bridge: {{workbuddy.todayCredit}} credits / {{workbuddy.
|
|
|
7
7
|
The multiplier above already includes any time-limited discount or free window that is active right now; `wb_list_models` also marks which offers are active and their hours.
|
|
8
8
|
|
|
9
9
|
- Use `wb_list_models` to see every model with its multiplier before choosing; prefer low-multiplier models for casual tasks.
|
|
10
|
+
- Safe-list gate: {{workbuddy.safeOnly}}. When it is `on`, only the models WorkBuddy publishes as public chat models are callable — `disabled` entries, internal routing aliases (`auto`, `fast-model`, `balanced-model`, `deep-model`), media/code models and delisted ones are refused. Do not try to work around a refusal; tell the user why and offer a safe model instead. `wb_list_models` with `all=true` shows the refused ones with reasons.
|
|
10
11
|
- Use `wb_select_model` when the user asks to switch the WorkBuddy model.
|
|
11
12
|
- `wb_chat` only works while the WorkBuddy desktop main process is running; otherwise it returns「未检测到 WorkBuddy 主进程」. Every call spends the user's own credits at that model's multiplier.
|
|
12
13
|
- `wb_credits` is read-only and costs nothing (also requires the desktop process).
|
package/src/config.js
CHANGED
|
@@ -29,6 +29,12 @@ export const WB_DEFAULTS = {
|
|
|
29
29
|
prefer: false,
|
|
30
30
|
/** 是否允许改写宿主控制台 index.html 注入「中转站优先」按钮;关闭后不注入也不还原。 */
|
|
31
31
|
hostInject: true,
|
|
32
|
+
/**
|
|
33
|
+
* 只允许调用 WorkBuddy 公开对话模型(安全清单)。
|
|
34
|
+
* 关掉后能用 disabled / 内部别名 / 预览等模型,但那属于「已知会违反对方使用建议」的操作,
|
|
35
|
+
* 默认必须是 true。
|
|
36
|
+
*/
|
|
37
|
+
safeOnly: true,
|
|
32
38
|
};
|
|
33
39
|
|
|
34
40
|
/** World 配置组(WebUI 通用渲染的旋钮)。模型选择走自定义面板,不在这里。 */
|
|
@@ -84,6 +90,13 @@ export const WB_CONFIG_GROUP = {
|
|
|
84
90
|
description: '在「开始」页加一个「中转站优先」按钮(会改写 Coopanion 的 build/console/index.html,停用或卸载时自动还原)。关掉后只能用 World 详情页的开关。',
|
|
85
91
|
'x-hot': true,
|
|
86
92
|
},
|
|
93
|
+
'worlds.workbuddy.safeOnly': {
|
|
94
|
+
type: 'boolean',
|
|
95
|
+
title: '只调用安全清单模型',
|
|
96
|
+
description:
|
|
97
|
+
'默认开:wb_chat / wb_select_model 只接受 WorkBuddy 公开目录里的对话模型(21 个),disabled、内部路由别名、媒体与代码补全模型一律拒绝。关掉可绕过限制,但那属于违反对方使用建议的操作,风险自负。',
|
|
98
|
+
'x-hot': true,
|
|
99
|
+
},
|
|
87
100
|
},
|
|
88
101
|
},
|
|
89
102
|
};
|
package/src/safety.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模型安全清单(合规口径)。
|
|
3
|
+
*
|
|
4
|
+
* 依据:WorkBuddy 官方对话给出的「避免违反用户协议/触发风控」筛选原则——
|
|
5
|
+
* 只调用「官方公开目录里、当前可正常对话」的模型;凡是 disabled、内部路由别名、
|
|
6
|
+
* 状态不明(请求格式报错)、媒体/代码补全模型,一律不通过外部软件调用。
|
|
7
|
+
*
|
|
8
|
+
* 这里把结论固化成三件事:
|
|
9
|
+
* 1. SAFE_MODEL_IDS:可直接用 wb_chat 调用的白名单(WorkBuddy 给的 21 个);
|
|
10
|
+
* 2. classifyModel():给目录里每个模型打等级与理由,供面板展示与拦截;
|
|
11
|
+
* 3. DEFAULT_SAFE_MODEL:当前选择失效时的回落项(同样是白名单内的 flash 档)。
|
|
12
|
+
*
|
|
13
|
+
* 清单会随官方目录变化而过时——这正是 classifyModel 返回 safe=false 时给出
|
|
14
|
+
* 「不在安全清单内」的原因,而不是断言模型不可用。用户可以显式关掉 safeOnly
|
|
15
|
+
* 自行承担风险,扩展不做单方面放行。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** WorkBuddy 建议的公开对话模型白名单。 */
|
|
19
|
+
export const SAFE_MODEL_IDS = [
|
|
20
|
+
// DeepSeek
|
|
21
|
+
'deepseek-v4.1-flash',
|
|
22
|
+
'deepseek-v4-flash',
|
|
23
|
+
'deepseek-v4-pro',
|
|
24
|
+
'deepseek-v3-2-volc',
|
|
25
|
+
// GLM
|
|
26
|
+
'glm-5.3-flash',
|
|
27
|
+
'glm-5.3',
|
|
28
|
+
'glm-5.2',
|
|
29
|
+
// 混元
|
|
30
|
+
'hy3',
|
|
31
|
+
'hy3-b',
|
|
32
|
+
'hy3-c',
|
|
33
|
+
'hy3-x',
|
|
34
|
+
'hunyuan-chat',
|
|
35
|
+
// Kimi
|
|
36
|
+
'kimi-k2.5',
|
|
37
|
+
'kimi-k2.8-preview',
|
|
38
|
+
'kimi-k3-1',
|
|
39
|
+
'kimi-k3.1-pro',
|
|
40
|
+
// 其他
|
|
41
|
+
'minimax-m2.7',
|
|
42
|
+
'minimax-m3.1-flash',
|
|
43
|
+
'mimo-v2.6-pro',
|
|
44
|
+
'space-bunny',
|
|
45
|
+
'step-5-preview',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/** 白名单分组(面板与 wb_list_models 用同一份顺序)。 */
|
|
49
|
+
export const SAFE_MODEL_GROUPS = [
|
|
50
|
+
{ label: 'DeepSeek', ids: ['deepseek-v4.1-flash', 'deepseek-v4-flash', 'deepseek-v4-pro', 'deepseek-v3-2-volc'] },
|
|
51
|
+
{ label: 'GLM', ids: ['glm-5.3-flash', 'glm-5.3', 'glm-5.2'] },
|
|
52
|
+
{ label: '混元', ids: ['hy3', 'hy3-b', 'hy3-c', 'hy3-x', 'hunyuan-chat'] },
|
|
53
|
+
{ label: 'Kimi', ids: ['kimi-k2.5', 'kimi-k2.8-preview', 'kimi-k3-1', 'kimi-k3.1-pro'] },
|
|
54
|
+
{ label: '其他', ids: ['minimax-m2.7', 'minimax-m3.1-flash', 'mimo-v2.6-pro', 'space-bunny', 'step-5-preview'] },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
/** 白名单里属于「免费试用/预览」的模型:能用,但不适合高频自动化调用。 */
|
|
58
|
+
export const CAUTION_MODEL_IDS = ['hy3', 'hy3-b', 'hy3-c', 'kimi-k2.8-preview', 'step-5-preview'];
|
|
59
|
+
|
|
60
|
+
/** 当前选择失效时的回落模型。 */
|
|
61
|
+
export const DEFAULT_SAFE_MODEL = 'deepseek-v4.1-flash';
|
|
62
|
+
|
|
63
|
+
/** 内部路由别名:疑似仅限 WorkBuddy 应用内 UI 使用。 */
|
|
64
|
+
const ALIAS_IDS = ['auto', 'fast-model', 'balanced-model', 'deep-model'];
|
|
65
|
+
|
|
66
|
+
/** 媒体生成模型:不属于 /v2/chat/completions 的对话范围。 */
|
|
67
|
+
const MEDIA_IDS = ['hunyuan-image-alpha', 'hunyuan-image-alpha-edit', 'kling-video-v3', 'minimax-video-h3'];
|
|
68
|
+
|
|
69
|
+
/** 代码补全模型:走补全接口,对话接口只会回空。 */
|
|
70
|
+
const CODE_IDS = ['codewise-rewrite', 'codewise-nes-a4-027-aide', 'codewise-jump', 'codewise-completions'];
|
|
71
|
+
|
|
72
|
+
/** 实测已下架(11102 service info not found)的模型,仅用于展示时置灰。 */
|
|
73
|
+
const RETIRED_IDS = [
|
|
74
|
+
'hy4-preview-x',
|
|
75
|
+
'minimax-m2.5',
|
|
76
|
+
'glm-4.6v',
|
|
77
|
+
'kimi-k2-thinking',
|
|
78
|
+
'glm-4.6',
|
|
79
|
+
'deepseek-v3-1-volc',
|
|
80
|
+
'deepseek-v3-1',
|
|
81
|
+
'deepseek-v3-0324-lkeap',
|
|
82
|
+
'deepseek-r1-0528-lkeap',
|
|
83
|
+
'kimi-k2-instruct-taiji',
|
|
84
|
+
'completion-gf',
|
|
85
|
+
'default-1.1',
|
|
86
|
+
'default-1.2',
|
|
87
|
+
'hunyuan-3b',
|
|
88
|
+
'hunyuan-7b-dense',
|
|
89
|
+
'hunyuan-2.0-instruct',
|
|
90
|
+
'deepseek-r1-0528',
|
|
91
|
+
'deepseek-v3-0324',
|
|
92
|
+
'deepseek-v3-0324-taco-completion',
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
/** 请求格式不符(11150)的模型,状态不明。 */
|
|
96
|
+
const FORMAT_UNSUPPORTED_IDS = ['glm-5.0-turbo'];
|
|
97
|
+
|
|
98
|
+
/** 实验预览系列(能对话,但未列入安全清单)。 */
|
|
99
|
+
const EXPERIMENTAL_PREFIXES = ['hy4-preview', 'hy4preview-'];
|
|
100
|
+
|
|
101
|
+
const TIER_LABEL = {
|
|
102
|
+
safe: '安全清单',
|
|
103
|
+
caution: '安全清单(谨慎)',
|
|
104
|
+
alias: '内部路由别名',
|
|
105
|
+
disabled: '目录已标 disabled',
|
|
106
|
+
media: '媒体生成模型',
|
|
107
|
+
code: '代码补全模型',
|
|
108
|
+
retired: '已下架',
|
|
109
|
+
format: '状态不明',
|
|
110
|
+
experimental: '实验预览',
|
|
111
|
+
unknown: '不在安全清单',
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
function inList(id, list) {
|
|
115
|
+
return list.includes(id);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function startsWithAny(id, prefixes) {
|
|
119
|
+
return prefixes.some((p) => id.startsWith(p));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 判定一个模型属于哪一档。
|
|
124
|
+
* @param {string} id
|
|
125
|
+
* @param {{ disabled?: boolean }} [model]
|
|
126
|
+
* @returns {{ id: string, tier: string, label: string, reason: string, allow: boolean, caution: boolean }}
|
|
127
|
+
*/
|
|
128
|
+
export function classifyModel(id, model) {
|
|
129
|
+
const mid = String(id ?? '');
|
|
130
|
+
const make = (tier, reason) => ({
|
|
131
|
+
id: mid,
|
|
132
|
+
tier,
|
|
133
|
+
label: TIER_LABEL[tier] ?? tier,
|
|
134
|
+
reason,
|
|
135
|
+
allow: tier === 'safe' || tier === 'caution',
|
|
136
|
+
caution: tier === 'caution',
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (model?.disabled === true) return make('disabled', '目录里标了 disabled,WorkBuddy 明确建议不要调用');
|
|
140
|
+
if (inList(mid, SAFE_MODEL_IDS)) {
|
|
141
|
+
return inList(mid, CAUTION_MODEL_IDS)
|
|
142
|
+
? make('caution', '公开对话模型;属免费试用或 preview,能用但不适合高频自动化调用')
|
|
143
|
+
: make('safe', '公开对话模型,WorkBuddy 建议的调用范围');
|
|
144
|
+
}
|
|
145
|
+
if (inList(mid, ALIAS_IDS)) return make('alias', '内部路由别名,疑似仅限 WorkBuddy 应用内使用');
|
|
146
|
+
if (inList(mid, MEDIA_IDS)) return make('media', '图像/视频生成模型,不在对话接口范围内');
|
|
147
|
+
if (inList(mid, CODE_IDS)) return make('code', '代码补全模型,对话接口不会给出正文');
|
|
148
|
+
if (inList(mid, RETIRED_IDS)) return make('retired', '已下架(11102 service info not found)');
|
|
149
|
+
if (inList(mid, FORMAT_UNSUPPORTED_IDS)) return make('format', '请求格式不符(11150),状态不明');
|
|
150
|
+
if (startsWithAny(mid, EXPERIMENTAL_PREFIXES)) return make('experimental', '实验预览模型,未列入安全清单');
|
|
151
|
+
return make('unknown', 'WorkBuddy 未把它列为公开对话模型');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** 白名单模型的建议默认次序:优先 flash 档,避开试用/预览。 */
|
|
155
|
+
export function preferredSafeModel(availableIds = SAFE_MODEL_IDS) {
|
|
156
|
+
const order = [DEFAULT_SAFE_MODEL, 'deepseek-v4-flash', 'glm-5.3-flash', 'minimax-m3.1-flash'];
|
|
157
|
+
for (const id of order) if (availableIds.includes(id)) return id;
|
|
158
|
+
const firstStable = availableIds.find((id) => inList(id, SAFE_MODEL_IDS) && !inList(id, CAUTION_MODEL_IDS));
|
|
159
|
+
return firstStable ?? DEFAULT_SAFE_MODEL;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 白名单按分组展开,只保留确实在目录里的。 */
|
|
163
|
+
export function safeGroupsOf(availableIds) {
|
|
164
|
+
const set = new Set(availableIds ?? []);
|
|
165
|
+
return SAFE_MODEL_GROUPS.map((g) => ({ label: g.label, ids: g.ids.filter((id) => set.has(id)) })).filter(
|
|
166
|
+
(g) => g.ids.length > 0,
|
|
167
|
+
);
|
|
168
|
+
}
|
package/src/upstream.js
CHANGED
|
@@ -401,6 +401,8 @@ function normalizeModel(m) {
|
|
|
401
401
|
supportsToolCall: m.supportsToolCall === true || m.supports_tool_call === true,
|
|
402
402
|
description: str(m.descriptionZh ?? m.description ?? m.descriptionEn),
|
|
403
403
|
reasoning: parseReasoning(m.reasoning),
|
|
404
|
+
/** 目录里标了 disabled=true 的模型:调用它们等于绕过官方限制,安全清单直接拒。 */
|
|
405
|
+
disabled: m.disabled === true,
|
|
404
406
|
};
|
|
405
407
|
return out;
|
|
406
408
|
}
|
package/src/webui/client.js
CHANGED
|
@@ -85,6 +85,13 @@ const TEXT = {
|
|
|
85
85
|
totalCredit: '合计积分',
|
|
86
86
|
todayCredit: '今日积分',
|
|
87
87
|
todayTokens: '今日 token',
|
|
88
|
+
colSafety: '合规',
|
|
89
|
+
safetySafe: '安全清单',
|
|
90
|
+
safetyCaution: '安全清单·谨慎',
|
|
91
|
+
showAllModels: '显示全部模型(含不建议项)',
|
|
92
|
+
unsafePick: '该模型不在安全清单内,已拒绝选择',
|
|
93
|
+
safeOnlyOn: '仅调用安全清单模型',
|
|
94
|
+
safeOnlyOff: 'safeOnly 已关闭:全部模型可调用(含 WorkBuddy 不建议外部调用的项)',
|
|
88
95
|
},
|
|
89
96
|
en: {
|
|
90
97
|
refresh: 'Refresh from WorkBuddy',
|
|
@@ -166,6 +173,13 @@ const TEXT = {
|
|
|
166
173
|
totalCredit: 'Total credits',
|
|
167
174
|
todayCredit: 'Credits today',
|
|
168
175
|
todayTokens: 'Tokens today',
|
|
176
|
+
colSafety: 'Policy',
|
|
177
|
+
safetySafe: 'Safe list',
|
|
178
|
+
safetyCaution: 'Safe list · caution',
|
|
179
|
+
showAllModels: 'Show all models (including discouraged ones)',
|
|
180
|
+
unsafePick: 'This model is outside the safe list and cannot be selected',
|
|
181
|
+
safeOnlyOn: 'Safe-list only',
|
|
182
|
+
safeOnlyOff: 'safeOnly is off: every model is callable (including ones WorkBuddy discourages)',
|
|
169
183
|
},
|
|
170
184
|
};
|
|
171
185
|
|
|
@@ -205,7 +219,10 @@ function mountModels(ctx) {
|
|
|
205
219
|
|
|
206
220
|
const sheet = ui.sheet({
|
|
207
221
|
title: s.colModel === '模型' ? 'WorkBuddy 模型目录' : 'WorkBuddy model catalog',
|
|
208
|
-
desc:
|
|
222
|
+
desc:
|
|
223
|
+
s.colModel === '模型'
|
|
224
|
+
? '默认只列 WorkBuddy 公开对话模型(安全清单);勾选「显示全部模型」可查看被排除项及其理由。'
|
|
225
|
+
: 'Only WorkBuddy public chat models are listed by default; tick "Show all models" to see excluded ones and why.',
|
|
209
226
|
});
|
|
210
227
|
ctx.root.append(sheet.el);
|
|
211
228
|
|
|
@@ -213,14 +230,15 @@ function mountModels(ctx) {
|
|
|
213
230
|
const refreshBtn = ui.button(s.refresh, { variant: 'primary' });
|
|
214
231
|
const stateChip = ui.chip('', 'plain');
|
|
215
232
|
const currentChip = ui.chip('', 'accent');
|
|
216
|
-
|
|
233
|
+
const allBox = ui.checkbox(s.showAllModels, { checked: false });
|
|
234
|
+
head.append(refreshBtn, stateChip, currentChip, allBox.el);
|
|
217
235
|
sheet.body.append(head);
|
|
218
236
|
|
|
219
237
|
const errLine = ui.msgline();
|
|
220
238
|
sheet.body.append(errLine);
|
|
221
239
|
|
|
222
240
|
const table = ui.table({
|
|
223
|
-
head: [s.colModel, s.colMultiplier, s.colContext, s.colReasoning, s.colAction],
|
|
241
|
+
head: [s.colModel, s.colSafety, s.colMultiplier, s.colContext, s.colReasoning, s.colAction],
|
|
224
242
|
maxHeight: 'calc(100vh - 320px)',
|
|
225
243
|
});
|
|
226
244
|
sheet.body.append(table.el);
|
|
@@ -228,16 +246,29 @@ function mountModels(ctx) {
|
|
|
228
246
|
let state = null;
|
|
229
247
|
|
|
230
248
|
function renderChips() {
|
|
231
|
-
|
|
249
|
+
const models = state?.models ?? [];
|
|
250
|
+
const safeCount = models.filter((m) => m.safety?.allow).length;
|
|
251
|
+
stateChip.textContent = `${s.lastRefresh}: ${fmtTs(state?.catalogAt, s)} · ${safeCount}/${models.length} ${s.models} · ${
|
|
252
|
+
state?.safeOnly === false ? s.safeOnlyOff : s.safeOnlyOn
|
|
253
|
+
}`;
|
|
232
254
|
currentChip.textContent = `${s.currentModel}: ${state?.model || '—'}`;
|
|
233
255
|
}
|
|
234
256
|
|
|
257
|
+
/** 当前是否允许选这个模型:safeOnly 开时只放行安全清单。 */
|
|
258
|
+
function pickable(m) {
|
|
259
|
+
if (state?.safeOnly === false) return true;
|
|
260
|
+
return m.safety?.allow !== false;
|
|
261
|
+
}
|
|
262
|
+
|
|
235
263
|
function renderTable() {
|
|
236
|
-
const
|
|
264
|
+
const all = state?.models ?? [];
|
|
265
|
+
const models = allBox.checked ? all : all.filter((m) => m.safety?.allow);
|
|
237
266
|
table.clear(models.length ? undefined : s.empty);
|
|
238
267
|
for (const m of models) {
|
|
239
268
|
const mul = fmtMultiplier(m, s);
|
|
240
269
|
const isCurrent = state.model === m.id;
|
|
270
|
+
const safety = m.safety ?? {};
|
|
271
|
+
|
|
241
272
|
const nameCell = ui.h('div');
|
|
242
273
|
nameCell.append(ui.h('div', null, m.name || m.id));
|
|
243
274
|
const sub = ui.h('div', 'dim', m.id);
|
|
@@ -246,13 +277,28 @@ function mountModels(ctx) {
|
|
|
246
277
|
nameCell.append(sub);
|
|
247
278
|
if (m.description) nameCell.title = m.description;
|
|
248
279
|
|
|
280
|
+
const safePill = ui.pill(
|
|
281
|
+
safety.label ?? '—',
|
|
282
|
+
safety.allow ? (safety.caution ? 'plain' : 'on') : 'plain',
|
|
283
|
+
);
|
|
284
|
+
if (safety.reason) safePill.title = safety.reason;
|
|
285
|
+
if (!safety.allow) {
|
|
286
|
+
safePill.style.opacity = '0.6';
|
|
287
|
+
nameCell.style.opacity = '0.6';
|
|
288
|
+
}
|
|
289
|
+
|
|
249
290
|
const mulPill = ui.pill(mul.text, mul.free ? 'on' : 'plain');
|
|
250
291
|
if (mul.promo) mulPill.title = mul.promo;
|
|
251
292
|
|
|
293
|
+
const allowed = pickable(m);
|
|
252
294
|
const btn = ui.button(isCurrent ? s.selected : s.select, {
|
|
253
295
|
size: 'sm',
|
|
254
296
|
variant: isCurrent ? 'plain' : 'primary',
|
|
255
297
|
onClick: async () => {
|
|
298
|
+
if (!allowed) {
|
|
299
|
+
ui.toast(`${s.unsafePick}${safety.reason ? `:${safety.reason}` : ''}`, 'bad');
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
256
302
|
const off = ui.disable(btn);
|
|
257
303
|
try {
|
|
258
304
|
const r = await ctx.invoke('select', [m.id]);
|
|
@@ -272,9 +318,14 @@ function mountModels(ctx) {
|
|
|
272
318
|
},
|
|
273
319
|
});
|
|
274
320
|
if (isCurrent) btn.disabled = true;
|
|
321
|
+
if (!allowed) {
|
|
322
|
+
btn.disabled = true;
|
|
323
|
+
btn.title = safety.reason ?? '';
|
|
324
|
+
}
|
|
275
325
|
|
|
276
326
|
table.addRow([
|
|
277
327
|
nameCell,
|
|
328
|
+
safePill,
|
|
278
329
|
mulPill,
|
|
279
330
|
m.contextWindow ? String(m.contextWindow) : '—',
|
|
280
331
|
m.reasoning?.supportedEfforts?.join(' / ') ?? '—',
|
|
@@ -283,6 +334,11 @@ function mountModels(ctx) {
|
|
|
283
334
|
}
|
|
284
335
|
}
|
|
285
336
|
|
|
337
|
+
allBox.input.addEventListener('change', () => {
|
|
338
|
+
renderChips();
|
|
339
|
+
renderTable();
|
|
340
|
+
});
|
|
341
|
+
|
|
286
342
|
async function load() {
|
|
287
343
|
state = await ctx.invoke('state');
|
|
288
344
|
renderChips();
|
package/src/world.js
CHANGED
|
@@ -12,6 +12,7 @@ import { discoverCredentials, maskToken, resolveTokensWithKey } from './credenti
|
|
|
12
12
|
import { detectWorkBuddy } from './wbproc.js';
|
|
13
13
|
import { attachPromos, dailyWindowText, discountText, priceViewOf, promoActiveAt } from './promo.js';
|
|
14
14
|
import { UsageLedger, summarize } from './usage.js';
|
|
15
|
+
import { DEFAULT_SAFE_MODEL, SAFE_MODEL_IDS, classifyModel, preferredSafeModel, safeGroupsOf } from './safety.js';
|
|
15
16
|
import { backupPathOf, describeHostInjection, ensureHostInjection, findAppRoot, inspectHostInjection, removeHostInjection } from './host.js';
|
|
16
17
|
|
|
17
18
|
const ENV_PROMPT_FILE = fileURLToPath(new URL('./ENV_PROMPT.md', import.meta.url));
|
|
@@ -53,9 +54,15 @@ export const WB_TOOL_DECLS = [
|
|
|
53
54
|
{
|
|
54
55
|
name: 'wb_list_models',
|
|
55
56
|
description:
|
|
56
|
-
'List the WorkBuddy models
|
|
57
|
+
'List the WorkBuddy chat models that are safe to call (official public catalog only), each with its credit multiplier (e.g. x0.79, already including time-limited discounts), context window and reasoning efforts. Read-only; costs nothing. Pass all=true to also list models that are disabled, internal routing aliases, media/code models or delisted ones — those are not callable through this bridge while worlds.workbuddy.safeOnly is on.',
|
|
57
58
|
tags: ['read'],
|
|
58
|
-
parameters: {
|
|
59
|
+
parameters: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
additionalProperties: false,
|
|
62
|
+
properties: {
|
|
63
|
+
all: { type: 'boolean', description: 'Also list models outside the safe list (shown with their risk reason).' },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
59
66
|
},
|
|
60
67
|
{
|
|
61
68
|
name: 'wb_select_model',
|
|
@@ -434,6 +441,37 @@ export class WorkBuddyWorld {
|
|
|
434
441
|
return this.catalog.find((m) => m.id === id);
|
|
435
442
|
}
|
|
436
443
|
|
|
444
|
+
// -------------------------------------------------------------------------
|
|
445
|
+
// 合规闸门:只放行 WorkBuddy 公开对话模型
|
|
446
|
+
// -------------------------------------------------------------------------
|
|
447
|
+
|
|
448
|
+
/** 目录里哪些模型进了安全清单(按目录顺序)。 */
|
|
449
|
+
safeModelIds() {
|
|
450
|
+
return this.catalog.filter((m) => classifyModel(m.id, m).allow).map((m) => m.id);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** 单个模型的合规判定。 */
|
|
454
|
+
safetyOf(id) {
|
|
455
|
+
return classifyModel(id, this.findModel(id));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** 把模型的合规判定挂进 models/state 的返回,供面板渲染。 */
|
|
459
|
+
describeModel(m) {
|
|
460
|
+
const safety = classifyModel(m.id, m);
|
|
461
|
+
return { ...m, safety, price: priceViewOf(m, Date.now()) };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* wb_chat / wb_select_model 的准入检查。
|
|
466
|
+
* @returns {string|null} 拒绝时返回给模型看的说明,放行时 null。
|
|
467
|
+
*/
|
|
468
|
+
rejectUnsafeModel(id) {
|
|
469
|
+
const safety = this.safetyOf(id);
|
|
470
|
+
if (safety.allow || this.cfg.safeOnly === false) return null;
|
|
471
|
+
const hint = '(workbuddy.safeOnly 已开;如确需使用,可在 World 配置里关掉它,风险自负)';
|
|
472
|
+
return `模型 ${id} 不在 WorkBuddy 安全清单内,判定为「${safety.label}」:${safety.reason}。${hint}`;
|
|
473
|
+
}
|
|
474
|
+
|
|
437
475
|
// -------------------------------------------------------------------------
|
|
438
476
|
// World 契约
|
|
439
477
|
// -------------------------------------------------------------------------
|
|
@@ -454,6 +492,7 @@ export class WorkBuddyWorld {
|
|
|
454
492
|
'workbuddy.authed': this.tokens ? 'yes' : 'no',
|
|
455
493
|
'workbuddy.priority': this.preferOn ? 'on' : 'off',
|
|
456
494
|
'workbuddy.process': this.proc.running ? 'running' : 'stopped',
|
|
495
|
+
'workbuddy.safeOnly': this.cfg.safeOnly === false ? 'off' : 'on',
|
|
457
496
|
'workbuddy.todayCredit': String(Math.round(this.today.credit * 100) / 100),
|
|
458
497
|
'workbuddy.todayTokens': String(this.today.prompt + this.today.completion),
|
|
459
498
|
};
|
|
@@ -463,7 +502,7 @@ export class WorkBuddyWorld {
|
|
|
463
502
|
return [
|
|
464
503
|
{
|
|
465
504
|
...WB_TOOL_DECLS[0],
|
|
466
|
-
handler: async () => this.toolListModels(),
|
|
505
|
+
handler: async (args) => this.toolListModels({ all: args?.all === true }),
|
|
467
506
|
},
|
|
468
507
|
{
|
|
469
508
|
...WB_TOOL_DECLS[1],
|
|
@@ -581,10 +620,12 @@ export class WorkBuddyWorld {
|
|
|
581
620
|
// 工具实现(回执只陈述可确认的事实)
|
|
582
621
|
// -------------------------------------------------------------------------
|
|
583
622
|
|
|
584
|
-
toolListModels() {
|
|
623
|
+
toolListModels({ all = false } = {}) {
|
|
585
624
|
if (this.catalog.length === 0) return `WorkBuddy 模型目录为空。${this.lastError || '请先在面板刷新目录。'}`;
|
|
586
625
|
const now = Date.now();
|
|
587
|
-
const
|
|
626
|
+
const safeOnly = this.cfg.safeOnly !== false;
|
|
627
|
+
|
|
628
|
+
const describe = (m) => {
|
|
588
629
|
const parts = [`- ${m.id}`];
|
|
589
630
|
if (m.name !== m.id) parts.push(`(${m.name})`);
|
|
590
631
|
const price = priceViewOf(m, now);
|
|
@@ -602,10 +643,24 @@ export class WorkBuddyWorld {
|
|
|
602
643
|
if (m.reasoning?.supportedEfforts) parts.push(`推理档 ${m.reasoning.supportedEfforts.join('/')}`);
|
|
603
644
|
if (m.supportsImages) parts.push('支持图片输入');
|
|
604
645
|
return parts.join(',');
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
const safeModels = this.catalog.filter((m) => classifyModel(m.id, m).allow);
|
|
649
|
+
const otherModels = this.catalog.filter((m) => !classifyModel(m.id, m).allow);
|
|
650
|
+
|
|
651
|
+
const groups = safeGroupsOf(safeModels.map((m) => m.id));
|
|
652
|
+
const groupBlocks = groups.map((g) => {
|
|
653
|
+
const rows = g.ids.map((id) => describe(this.findModel(id))).filter(Boolean);
|
|
654
|
+
return `【${g.label}】\n${rows.join('\n')}`;
|
|
605
655
|
});
|
|
656
|
+
|
|
606
657
|
const current = this.cfg.model
|
|
607
658
|
? `当前选择:${this.cfg.model}(此刻 ${this.selectedMultiplier() ?? '倍率未知'})\n`
|
|
608
659
|
: '当前未选择模型。\n';
|
|
660
|
+
const policy = safeOnly
|
|
661
|
+
? `安全清单:${safeModels.length}/${this.catalog.length} 个模型可调用(WorkBuddy 公开目录里的对话模型;wb_list_models 传 all=true 可看其余项及原因)。\n`
|
|
662
|
+
: `注意:safeOnly 已关闭,${this.catalog.length} 个模型全部可调用——其中含 disabled/别名/媒体等 WorkBuddy 不建议外部调用的项。\n`;
|
|
663
|
+
|
|
609
664
|
const promoLines = this.promos
|
|
610
665
|
.filter((p) => Array.isArray(p.modelIds) && p.modelIds.length > 0)
|
|
611
666
|
.map((p) => {
|
|
@@ -616,12 +671,28 @@ export class WorkBuddyWorld {
|
|
|
616
671
|
return `- ${p.id}:${discountText(p)}${when ? `,${when}` : ''},影响 ${p.modelIds.join('/')}(${flag})`;
|
|
617
672
|
});
|
|
618
673
|
const promoBlock = promoLines.length > 0 ? `\n限时优惠:\n${promoLines.join('\n')}` : '';
|
|
619
|
-
|
|
674
|
+
|
|
675
|
+
if (!all) {
|
|
676
|
+
const body = groupBlocks.length > 0 ? groupBlocks.join('\n') : '(安全清单里的模型当前都不在目录中,先刷新目录。)';
|
|
677
|
+
return `${current}${policy}\n${body}${promoBlock}`;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const otherRows = otherModels.map((m) => {
|
|
681
|
+
const safety = classifyModel(m.id, m);
|
|
682
|
+
return `- ${m.id}:${safety.label}——${safety.reason}`;
|
|
683
|
+
});
|
|
684
|
+
const otherBlock =
|
|
685
|
+
otherRows.length > 0
|
|
686
|
+
? `\n\n其余 ${otherRows.length} 个模型(不建议通过外部软件调用${safeOnly ? ',当前会被拒绝' : ',但 safeOnly 已关闭'}):\n${otherRows.join('\n')}`
|
|
687
|
+
: '';
|
|
688
|
+
return `${current}${policy}\n${groupBlocks.join('\n')}${otherBlock}${promoBlock}`;
|
|
620
689
|
}
|
|
621
690
|
|
|
622
691
|
async toolSelectModel(id) {
|
|
623
692
|
const model = this.findModel(id);
|
|
624
693
|
if (!model) return { text: `目录里没有模型「${id}」。先用 wb_list_models 看可用 id。`, failed: true };
|
|
694
|
+
const reject = this.rejectUnsafeModel(id);
|
|
695
|
+
if (reject) return { text: reject, failed: true };
|
|
625
696
|
this.cfg.model = id;
|
|
626
697
|
// 框架的 ctx.persist(patch) 必须带 patch;只写本 World 段内的字段。
|
|
627
698
|
this.ctx.persist({ model: id });
|
|
@@ -644,7 +715,15 @@ export class WorkBuddyWorld {
|
|
|
644
715
|
return { text: '未检测到 WorkBuddy 主进程:请先启动 WorkBuddy 桌面端再试。', failed: true };
|
|
645
716
|
}
|
|
646
717
|
const useId = model || this.cfg.model;
|
|
647
|
-
if (!useId)
|
|
718
|
+
if (!useId) {
|
|
719
|
+
const suggestion = preferredSafeModel(this.safeModelIds());
|
|
720
|
+
return {
|
|
721
|
+
text: `尚未选择模型;先 wb_list_models 再 wb_select_model(建议 ${suggestion})。`,
|
|
722
|
+
failed: true,
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
const reject = this.rejectUnsafeModel(useId);
|
|
726
|
+
if (reject) return { text: reject, failed: true };
|
|
648
727
|
const tokens = await this.ensureTokens();
|
|
649
728
|
if (!tokens) return { text: '未发现 WorkBuddy 登录态,无法调用。', failed: true };
|
|
650
729
|
const messages = [];
|
|
@@ -837,6 +916,7 @@ export class WorkBuddyWorld {
|
|
|
837
916
|
{ name: 'workbuddy.authed', description: '是否已有可用登录态。' },
|
|
838
917
|
{ name: 'workbuddy.priority', description: '「中转站优先」开关:on / off。' },
|
|
839
918
|
{ name: 'workbuddy.process', description: 'WorkBuddy 主进程状态:running / stopped。' },
|
|
919
|
+
{ name: 'workbuddy.safeOnly', description: '安全清单闸门:on(只放行公开对话模型)/ off。' },
|
|
840
920
|
{ name: 'workbuddy.todayCredit', description: '今日已消耗积分(本地记账)。' },
|
|
841
921
|
{ name: 'workbuddy.todayTokens', description: '今日 token 用量(本地记账)。' },
|
|
842
922
|
],
|
|
@@ -1034,7 +1114,8 @@ export class WorkBuddyWorld {
|
|
|
1034
1114
|
model: this.cfg.model,
|
|
1035
1115
|
catalogAt: this.catalogAt,
|
|
1036
1116
|
lastError: this.lastError,
|
|
1037
|
-
|
|
1117
|
+
safeOnly: this.cfg.safeOnly !== false,
|
|
1118
|
+
models: this.catalog.map((m) => this.describeModel(m)),
|
|
1038
1119
|
prefer: this.preferOn,
|
|
1039
1120
|
running: !!this.proc.running,
|
|
1040
1121
|
mainPid: this.proc.mainPid,
|
|
@@ -1053,6 +1134,8 @@ export class WorkBuddyWorld {
|
|
|
1053
1134
|
const id = String(args[0] ?? '');
|
|
1054
1135
|
const found = this.findModel(id);
|
|
1055
1136
|
if (!found) return { ok: false, error: `unknown model: ${id}` };
|
|
1137
|
+
const reject = this.rejectUnsafeModel(id);
|
|
1138
|
+
if (reject) return { ok: false, error: reject, unsafe: true };
|
|
1056
1139
|
this.cfg.model = id;
|
|
1057
1140
|
this.ctx.persist({ model: id });
|
|
1058
1141
|
return { ok: true, model: id, multiplier: found.creditMultiplier ?? null };
|