1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
+ ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
236 lines
20 KiB
Markdown
236 lines
20 KiB
Markdown
# 85 · 模型密钥开放给用户自助配置(两层密钥:自配自用 + 平台共享回落)
|
||
|
||
- 日期:2026-09-13
|
||
- 状态:✅ **已上线并端到端验证**(后端已部署 + 插件 **0.3.7** 已铺发 admin/guest)
|
||
- 触发:用户原话「**能否把配置模型密钥的功能开放给用户自己配置,参考原本 dsh 模型设置功能,用户自己配置模型自己用**」+补充「**admin 设置的 用户也可以用,和用户设置的区分开就行**」
|
||
- 落点:`src/web/server.ts`(注入层)+ `src/web/routes/auth.ts`(路由层)+ `poc/business-plugins/lib/client.js`(实例内入口)+ `web/portal.html`(门户文案)
|
||
|
||
> **TL;DR**|① 原先平台是「**统一 KEY**」——`resolveApiKey()` **显式忽略入参 userId**,不管谁 spawn 都注入 admin 那一把;② 本轮改为**两层**:**用户自己的 key 优先,没有才回落管理员的「平台共享密钥」**;③ 路由 `/api/me/keys` 由 `requireAdmin` 放开为 `requireAuth`(用户只能管**自己那一格**,数据层本来就是 per-user 的 `credential_vault`);④ 实例内新增「**我的密钥**」分区(**所有用户可见**);⑤ 换 key 只重启**自己的**实例(admin 改的是共享 key ⇒ 才广播)。
|
||
> ⚠️ 本轮同时撞上**并行会话同号冲突**(见 §八),版本从 0.3.6 提升为 **0.3.7**。
|
||
|
||
---
|
||
|
||
## 一、先取证:原口径是什么(不是猜)
|
||
|
||
| 层 | 原实现 | 证据 |
|
||
|---|---|---|
|
||
| 注入 | `resolveApiKey = async (_userId) => { … getEnabledCredentialKeyRef(admins[0].id) }` —— **下划线参数名就是"故意忽略"**;注释原文「统一 KEY 模式:所有用户共用管理员(admin)设置的启用 key,**用户不可自配**」 | `src/web/server.ts:66-78`(改前) |
|
||
| 路由 | `/api/me/keys` GET/POST/DELETE、`/:id/select` **四个全部 `requireAdmin`**;注释原文「普通用户不再有自配 key 的入口/能力;这些路由对非 admin 一律 403」 | `src/web/routes/auth.ts:135-175`(改前) |
|
||
| 数据 | **本来就是 per-user**:表 `credential_vault(user_id, key_name, secret_ref, enabled)`,`listCredentialKeys(userId)` / `getEnabledCredentialKeyRef(userId)` / `setCredentialKey`(写入时"把该用户其它 key 全禁用"再启用这把) | `src/db/repo.ts:214-268` |
|
||
| 生效 | `DEEPSEEK_API_KEY` 是 **spawn 时注入 env 的快照** ⇒ 换 key **必须重启实例**;原实现一律 `restartAllMains()` | `src/supervisor/orchestrator.ts:524`、`auth.ts:157-160`(改前) |
|
||
| 线上现状 | `credential_vault` 只有一行:`admin / deepseek-main / enabled=1`;**guest 一行都没有** ⇒ guest 一直靠"回落 admin"在跑 | 服务器 db 实测(§五) |
|
||
|
||
⇒ **结论:数据层天生支持 per-user,被卡住的只有「路由权限」与「注入选择」两处。**
|
||
⇒ 所以这不是"新建一套体系",而是**把已经存在的能力放开 + 加一层回落**。
|
||
|
||
## 二、设计(两层,互相独立、互不覆盖)
|
||
|
||
```
|
||
spawn 某个用户的实例
|
||
└─ resolveApiKey(userId)
|
||
├─ ① 该用户自己的启用 key(credential_vault.enabled=1)→ 有 ⇒ 用它
|
||
└─ ② 没有 ⇒ 回落 管理员(role=admin 第一个)的启用 key =「平台共享密钥」
|
||
```
|
||
|
||
| 原则 | 说明 |
|
||
|---|---|
|
||
| **区分开**(用户原话) | 两层各自独立:用户配了就用自己的;没配就用共享的。用户**不能**改、也**看不到**共享密钥的内容 |
|
||
| 删除即回落 | 用户删掉自己最后一把 key ⇒ 自动回落到平台共享密钥,**不需要额外开关**(这是"两层"的应有语义) |
|
||
| 刷新粒度跟随影响面 | admin 改的 key **就是**共享 key ⇒ `restartAllMains()`(所有仍在回落的用户都要刷新);**其他人改自己的 ⇒ 只 `restartMain(自己)`**,不动任何人 |
|
||
| 零新增后端路由 | 复用既有 `/api/me/keys` 四路由,只改 preHandler 与返回结构 |
|
||
| 不返回密钥原文 | GET 只返回元数据(`name / enabled / updatedAt`)+ 共享密钥的 `name/owner/ownerIsMe`;**任何路径都不回显密钥本身** |
|
||
|
||
### 接口变化(`GET /api/me/keys`)
|
||
|
||
```jsonc
|
||
{
|
||
"keys": [], // 我自己的(元数据)
|
||
"effective": "shared", // own | shared | none —— 当前**实际生效**的来源
|
||
"shared": { "available": true, "name": "deepseek-main", "owner": "admin", "ownerIsMe": false }
|
||
}
|
||
```
|
||
> `ownerIsMe`:admin 看的是**自己**配的那把 ⇒ 前端文案要区分「我配的共享 key」与「别人配的」,否则 admin 会困惑"为什么我也是'回落'到别人"。
|
||
|
||
### R5 权限影响评估(**先评估再动手**)
|
||
|
||
| 维度 | 判断 |
|
||
|---|---|
|
||
| 是否**扩大**权限? | **否**。用户新增的能力 = 管理**自己那一格**的 key(`WHERE user_id = 自己`,`deleteCredentialKey`/`selectCredentialKey` 都带 `AND user_id = ?`)。**不能**读/改/删他人(含 admin)的 key,也拿不到共享密钥的内容 |
|
||
| 是否新增**攻击面**? | 极小:`apiKey` 仍走既有 header-safe 白名单 `^[A-Za-z0-9\-_.]{1,256}$`;`name` 仍 `^[A-Za-z0-9\-_ .]{1,32}$`;写入仍 `encrypt(secret, deriveKey(encryptionSecret))` 后落 `secret_ref` |
|
||
| 新增的"用户可触发动作" | 用户能让**自己**的实例重启(`restartMain(自己)`)—— 平台本来就有 `/api/dsh/stop|launch`(requireAuth),不构成新的越权面 |
|
||
| 反向风险(**要盯**) | 用户配了**无效/欠费**的 key ⇒ 他自己的实例调不了模型。这是 BYOK 的固有代价,且**只影响他自己**;删掉即回落共享 ✅ |
|
||
| 结论 | **不需要**新的权限门禁;仅 admin 侧文案调整为「平台共享密钥」 |
|
||
|
||
## 三、改动清单
|
||
|
||
| 文件 | 改动 |
|
||
|---|---|
|
||
| `src/web/server.ts` | `resolveApiKey(userId)` 改**两级**(抽出 `decryptRef()` 复用解密+损坏兜底);新增注释块说明两层语义与刷新义务 |
|
||
| `src/web/routes/auth.ts` | 4 个路由 `requireAdmin` → **`requireAuth`**;新增 3 个内部辅助 `keySourceOf(userId)`(判 own/shared/none)、`sharedKeyInfo(userId)`(共享密钥非敏感信息 + `ownerIsMe`)、`refreshAfterKeyChange(userId, role)`(admin ⇒ 广播;其他人 ⇒ 只重启自己);GET 返回加 `effective` + `shared`;移除已无用的 `requireAdmin` import |
|
||
| `poc/business-plugins/lib/client.js` | 新增 `MyKeysSection` + 注册 **`settings.section` `id: my-keys`, order 103**(**无条件注册,不判角色** ⇒ 所有用户可见);词典新增 **20 个 `mk.*` 键**(zh/en 一一对应);复用「系统管理」那套 `.pa-*`(门户同款)样式 |
|
||
| `web/portal.html` | 「密钥管理」页语义改名:卡片说明 `全局 API 密钥` → **`平台共享密钥`**;页头副标题 → 「平台共享密钥(未自配密钥的用户默认使用;仅管理员可改)」;空态文案改为指引用户去实例「设置 → 我的密钥」 |
|
||
| `scripts/verify-platform-admin-section.mjs` | 断言同步:注册数 2 → **3**(admin)/ 非 admin **2**(含 my-keys);新增 `mk` 分区 order=103、非 admin 可渲染「我的密钥/当前生效」、明示"只重启自己的实例";stub 数据加 `/api/me/keys` |
|
||
|
||
**实例内新分区(「我的密钥」)结构**:`① 我的密钥`(列表:名称 + 启用/删除;下方添加行:名称 + `sk-…` + 添加;附加密存储说明)+ `② 当前生效`(一行状态 + 依据来源的描述 + 「改 key 只重启你自己,其他用户不受影响」提示)。
|
||
|
||
## 四、出厂与铺发
|
||
|
||
- 平台:本机 `tsc` → 传 `src/web/{server.ts,routes/auth.ts}`(**转 LF**)→ 服务器 `npm run build` → `systemctl restart dshs`
|
||
- 门户:`web/portal.html` 直传 `/opt/dshs/web/portal.html`(静态页,无需重启)
|
||
- 插件:**`business-plugins-0.3.7.tgz`**(45,400 B)→ `scp /opt/dsh/artifacts/` → `ensure-biz-plugins.cjs --all --restart` ⇒ admin/guest 均 0.3.7;`sha256 = 16bb37dbcc13…` **本机 = artifacts = 两实例 `.dsh-stage` 三方一致**
|
||
- 回滚件:`/opt/dsh/backups/server.ts.bak-20260913-byok`、`/opt/dsh/backups/auth.ts.bak-20260913-byok`
|
||
|
||
## 五、验收(**改完必须在真环境跑一次** —— 本次做了端到端)
|
||
|
||
### 5.1 静态层
|
||
`npm run verify` **全绿**(含并行会话新增的 `verify-mem-model.mjs`);`node --check` 通过;`tsc --noEmit` rc=0。
|
||
|
||
### 5.2 真环境端到端(**R4 合规**:临时 session 直插,用完即删,未用真实账号登录)
|
||
|
||
用 `role=active`(guest)的临时 session `curl`:
|
||
|
||
| 步骤 | 结果 |
|
||
|---|---|
|
||
| ① `GET /api/me/keys`(guest) | **HTTP 200** `{"keys":[],"effective":"shared","shared":{"available":true,"name":"deepseek-main","owner":"admin","ownerIsMe":false}}` ← **改前这里应是 403**,这条即证明放开生效 |
|
||
| ② 对照:未登录 | **401** ✅ |
|
||
| ③ `POST /api/me/keys`(假 key `sk-test-byok-e2e-0000`) | **200**,返回 key 对象 |
|
||
| ④ 再 `GET` | `effective` 由 `shared` → **`own`** ✅ |
|
||
| ⑤ `DELETE /api/me/keys/:id` | **200** `{"ok":true}` |
|
||
| ⑥ 再 `GET` | `effective` 回落 **`shared`** ✅("删掉即回落"闭环成立) |
|
||
|
||
### 5.3 清理(**测试痕迹必须清干净**)
|
||
临时 session `user_agent='byok-e2e-test'` **0 条残留**;`credential_vault` 只剩 `admin/deepseek-main/enabled=1`(测试 key 已删);本机与服务器临时脚本已删。
|
||
|
||
## 六、未做 / 待办
|
||
|
||
1. **只支持"换 key",不支持"换 endpoint / 选模型"**:平台注入的是单一 `DEEPSEEK_API_KEY` env,base URL 仍是官方默认。若日后要支持自建网关/代理 endpoint,需要另开 env 注入通道(属新需求)。
|
||
2. **admin 的共享密钥在实例内没有编辑入口**:admin 要在门户「密钥管理」改它;实例内「我的密钥」把 admin 自己那把显示为「你配置的共享密钥」(`ownerIsMe`)。**是否把两者合并成一处编辑,未做**(保持"门户管共享、实例管自己"的分工)。
|
||
3. **未做浏览器截图验收**(本机 `agent-browser` daemon 起不来),视觉层只有 harness 断言 + 用户实看。
|
||
|
||
## 七、回滚
|
||
|
||
```bash
|
||
# 后端
|
||
cp /opt/dsh/backups/server.ts.bak-20260913-byok /opt/dshs/src/web/server.ts
|
||
cp /opt/dsh/backups/auth.ts.bak-20260913-byok /opt/dshs/src/web/routes/auth.ts
|
||
cd /opt/dshs && npm run build && systemctl restart dshs
|
||
# 插件:把候选池/artifacts 的 business-plugins 回落上一版并重铺
|
||
```
|
||
> 回滚后:`/api/me/keys` 重新只对 admin 开放;注入回到"统一 KEY"。**已写入的用户密钥会保留在库里**(只是不再被读取)—— 若要彻底清掉需手工删 `credential_vault` 里非 admin 的行。
|
||
|
||
## 八、⚠️ 本轮踩到的**并行冲突**(如实记录,供流程改进)
|
||
|
||
**现象**:我在 21:40 抢到全局执行锁后开工,21:50 打包时才从 `package.json` 发现 —— **另一个会话已经把版本改成了 `0.3.6`**(内容是「内存条改读平台真实配额」,见档案 84),且**它已经铺发过 0.3.6**。
|
||
|
||
**后果(差点出事)**:
|
||
1. `poc/business-plugins/lib/client.js` 里**同时含两方改动**(它的内存模型对齐 + 我的 MyKeysSection)—— 这是我 **pack 前才发现的**,不是抢锁时就知道的;
|
||
2. 我第一次 `npm pack` 出的 0.3.6 是**我的内容**,而 `ensure-biz-plugins.cjs` 按**文件名**判版本 ⇒ 输出「已是 business-plugins-0.3.6.tgz → **跳过**」⇒ **实例里其实还是它那版、artifacts 里已是我的版** ⇒ 典型的「**同号不同内容**」(这正是本项目三铁律里"同一名字不能既做脱敏目标又做公开值"的同一类陷阱)。
|
||
|
||
**处置**:
|
||
- 拒绝"同号不同内容",**提升为 0.3.7** 重新铺发(0.3.6 → 0.3.7 已实测生效);
|
||
- 出厂前跑**全量** `npm run verify`(含它挂的 `verify-mem-model.mjs`),**两方改动一起把关**,全绿才发;
|
||
- 本档案如实记录,并在 `package.json` 描述里注明"原拟 0.3.6,因并行同号冲突提升为 0.3.7"。
|
||
|
||
**🔴 教训(已写入记忆)**:
|
||
1. **抢到锁 ≠ 工作区干净**。接手前必须 `git status` + 看 `package.json` 版本,**先判断有没有别人的在途改动**;
|
||
2. **`ensure-biz-plugins.cjs` 按文件名判版本** ⇒ 「同号」会静默跳过。升版本号前必须先确认该号没被别人用过;
|
||
3. 多会话同改一个包时,**"谁先铺发"决定线上内容**,而 artifacts 可能已被后来者覆盖 ⇒ 铺发后**必须回读实例内的 `package.json` 版本 + sha256 三方比对**(本轮就是这么发现的)。
|
||
|
||
---
|
||
|
||
## 九、修正(2026-09-13 22:0x–22:2x):**改用官方「模型」页,不仿制** —— 并顺带炸出两处平台侧硬约束
|
||
|
||
> 用户追加要求(原话):「需要参考 **dsh 原始的配置密钥页面格式** 去开发页面,**那里面是可以选模型厂家的**」+「**界面交互都要和官方的一摸一样**」
|
||
|
||
### 9.1 为什么不"仿制"而要"直接用官方页"
|
||
|
||
「一模一样」这个要求,**只有用本体才能满足**(仿制品永远有差异)。于是先去读官方那个包:
|
||
|
||
| 事实 | 证据 |
|
||
|---|---|
|
||
| 包在 | `/usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-client-ui-settings-models`(**官方代码,只读**) |
|
||
| 它支持什么 | `ProviderEditor`(每家一张卡:**一个 write-only API key 输入** + 折叠的「自定义设置」= **baseURL** + DeepSeek 的模型目录)+ `CustomProviderCard`(**声明官方目录里没有的厂家**:OpenAI 兼容网关 / 自建服务 / 更新更快的厂家,必填 endpoint + protocol + ≥1 个模型) |
|
||
| 数据来源 | `remote.llm` / `remote.settings` / `remote.credentials`(**dsh 内部通道,不是外网**)⇒ 之前补丁注释里"provider 目录在此环境不可用"的判断**已不成立** |
|
||
| 配置落点 | `settings.yaml`(`providers.<route>` / `agent-default-model`)+ `$DSH_HOME/.credentials.yaml`(凭据) |
|
||
| 平台原本 | `ensure-role-profile-patch.cjs` 把它 `disabled: true`(理由:「模型 KEY 由管理员统一管控」+「避免用户绕过统一 key」) |
|
||
|
||
**实测放开**:给 guest 删掉该禁用行 → 重启 → 日志出现 `POST /api/session/modelCatalog` **200**(用户当场在操作界面)⇒ **官方页在平台环境完全可用**。
|
||
|
||
### 9.2 🔴 平台侧两处硬约束(**决定了实现方式**,都是读官方源码挖出来的)
|
||
|
||
**(1) env 优先级最高** —— `dsh-credentials-local` 头注释原文:
|
||
|
||
```text
|
||
inherited process environment (read-only, wins)
|
||
> $DSH_HOME/.credentials.yaml (provider-managed, writable)
|
||
> <invocation cwd>/.env (read-only fallback)
|
||
> $DSH_HOME/.env (read-only fallback)
|
||
```
|
||
|
||
**(2) 更狠的:`write()` 里有 `assertUnshadowed()`** —— 只要 env 存在同名 ref,用户在官方模型页**保存会直接抛错**:
|
||
|
||
> `credentials-local: "DEEPSEEK_API_KEY" is supplied read-only by the launching environment, so set would be shadowed; unset it in the shell you start dsh from instead`
|
||
|
||
⇒ **结论:只要平台继续注入 `DEEPSEEK_API_KEY` env,用户就被锁死在"不能自配 DeepSeek key"**(能配自定义厂家,但配 DeepSeek 报错)。这与"开放自配"直接冲突。
|
||
|
||
### 9.3 所以改成:**平台不再注入 env,改为把共享密钥预置进凭据文件**
|
||
|
||
`src/web/server.ts` 的 `resolveApiKey(userId)` 语义整体换掉:
|
||
|
||
| | 改前(§三~§七) | 改后(本节) |
|
||
|---|---|---|
|
||
| 共享 key 怎么给 | spawn 时注入 `DEEPSEEK_API_KEY` **env** | spawn 时**写入** `$DSH_HOME/.credentials.yaml` 的 `refs:` 段 |
|
||
| 返回值 | 返回 key 明文(⇒ 被注入 env) | **恒返回 `null`**(不注入 env —— 这是刻意的,不是"没有 key") |
|
||
| 用户自配 | 被 env 静默盖掉 / 保存报错 | **直接覆盖同一个 ref**,官方页保存成功、立即生效 |
|
||
| 用户没配 | 用共享 key | 平台预置的共享 key ⇒ 照旧能用 |
|
||
|
||
写入函数 `ensureRefInCredentials()` 的保守约束:① 只在 `refs:` **没有**该 ref 时写(用户配过绝不碰);② **只在 `version: 1`** 的文档上插入,不重排、不重写其它行;③ 写前备份;④ 写完 `chown` 给 home 属主(否则 600 权限下实例读不了自己的凭据文件);⑤ 认不出的布局**宁可不动**。失败时**退回 env 注入**保底。
|
||
|
||
**同一轮撤掉「我的密钥」分区**(`poc/business-plugins` **0.3.8**):官方页已承担该职能,两套入口并存会互相干扰(平台 vault 与官方凭据是**两个存储**,用户会分不清谁生效)。
|
||
|
||
### 9.4 🔴🔴 本轮**真事故**:备份文件落进 home ⇒ 实例崩溃循环(已修)
|
||
|
||
第一次实现时,写前备份落在 **`$DSH_HOME/.credentials.yaml.bak-platform`**(root 属主、600)。结果 guest 实例**起不来**:
|
||
|
||
```text
|
||
Error: EACCES: permission denied, watch '/var/lib/.../home/.credentials.yaml.bak-platform'
|
||
at chokidar ... NodeFsHandler._watchWithNodeFs
|
||
[crash-restart] {"event":"instance-restart", ..., "attempt":5, ...}
|
||
```
|
||
|
||
**真因**:dsh 用 **chokidar watch 整个 home 目录**;只要 home 里出现一个**实例身份读不了**的文件,watcher 就 `EACCES` ⇒ **进程直接退出** ⇒ 崩溃自愈反复重试(attempt 1→5)。
|
||
|
||
**修复**:备份改落**平台自己的目录** `/opt/dsh/backups/`(实例看不到),并补了**同族隐患检查**(扫 home 里 root 属主的文件)。
|
||
**用户影响**:约 1 分钟(22:14–22:15),期间 guest 实例反复重启;**已恢复**且后续验证 0 崩溃。
|
||
|
||
> **教训(已写进记忆)**:**用户 home 是 dsh 的 watch 域 ⇒ 绝不能往里放实例读不了的文件**。任何"平台侧顺手在 home 里放个东西"的动作,都要先问「实例能不能读」,否则就是崩溃循环。
|
||
|
||
### 9.5 验证(真环境,逐条实测)
|
||
|
||
| 检查 | 结果 |
|
||
|---|---|
|
||
| guest 的 `cordis.patch.yml` 里 `ui-settings-models` 禁用行 | **0 处** ⇒ 模型分区已对用户可见 |
|
||
| 用户操作官方页(`/api/session/modelCatalog`) | **200** ✅ |
|
||
| spawn 后凭据文件 | 被**预置** `refs: DEEPSEEK_API_KEY: <…>`,原 `records:` 内容**完整保留** |
|
||
| 凭据文件属主 | `dsh-eeccbc638afc46bdb663`(**实例 uid,非 root**)⇒ 实例可读 |
|
||
| 实例进程 env 里 `DEEPSEEK_API_KEY` | **0 条**(改前是 1 条)⇒ 不再注入 |
|
||
| 实例稳定性 | 起后 30s+ 稳定运行,`instance-restart` 0 次 |
|
||
| 备份落点 | `/opt/dsh/backups/credentials-home-*.yaml`(**不在 home**) |
|
||
| 抹掉 `refs` 段再 spawn | **又被正确重新预置**(写入分支闭环) |
|
||
|
||
### 9.6 回滚
|
||
|
||
```bash
|
||
cp /opt/dsh/backups/server.ts.bak-20260913-byok /opt/dshs/src/web/server.ts # ⚠️ 这是 §五 那版(两层 vault 版),如需回滚到"env 注入"需另取
|
||
cd /opt/dshs && npm run build && systemctl restart dshs
|
||
# 插件回落 0.3.7(含「我的密钥」分区);并给用户 patch 重新加回 models 禁用行
|
||
```
|
||
> ⚠️ **回到"env 注入"意味着用户在官方模型页**保存 DeepSeek key 会报错**(§9.2)—— 回滚前先想清楚要不要这个副作用。
|
||
|
||
### 9.7 未做 / 待观察
|
||
|
||
1. **`settings.yaml` 侧平台完全不介入**:用户选哪家、哪个模型由官方页自己写(`providers.<route>` / `agent-default-model`)。平台**不校验**用户配的自定义 endpoint(实例可出网,nft OUTPUT 默认 accept)—— 这是"用户自己的 key 自己的选择",但**如果**日后要限制出网目标,得另立需求。
|
||
2. **admin 自己的实例**同样走"预置共享 key"⇒ admin 若想在实例里用自己的 key,直接在官方模型页改即可(覆盖同一 ref)。
|
||
3. **待观察**:用户首次真配一把 DeepSeek key 后,确认 `.credentials.yaml` 里出现的是 `refs.DEEPSEEK_API_KEY`(本文的判据就是它)。
|
||
|