Files
dsh_ai1net_server/ensure-role-profile-patch.cjs
T
admin eb50ca2d5b feat(keys): 模型密钥开放给用户自配 —— 放开官方「模型」页 + 平台改预置凭据
用户要求「把配置模型密钥开放给用户自己配」且「界面交互和官方一模一样」⇒ 不仿制,直接放开官方 ui-settings-models 页(可选厂家:DeepSeek 内置 + 自定义 OpenAI 兼容网关含 baseURL/模型)。

- ensure-role-profile-patch.cjs:不再对普通用户禁用 ui-settings-models(保留 plugins/inventory/cordis 禁用);--force 时能把「还禁着 models」的旧块升级。

- src/web/server.ts:resolveApiKey 不再注入 DEEPSEEK_API_KEY env,改为把「平台共享密钥」预置进 $DSH_HOME/.credentials.yaml 的 refs 段(新增 ensureRefInCredentials:只在无该 ref 时写 / 只在 version:1 上插入 / 备份落平台目录 / 写完 chown 给实例 uid / 失败退回 env)。真因:dsh 凭据解析里 env 优先级最高,且 dsh-credentials-local.write() 的 assertUnshadowed() 会让用户在模型页保存直接报错 ⇒ 注入 env 等于锁死用户自配。

- src/web/routes/auth.ts:/api/me/keys 由 requireAdmin 放开为 requireAuth(平台侧密钥 API 保留,UI 不再暴露)。

- poc/business-plugins 0.3.4→0.3.8:「系统管理」对齐门户 6 个功能页(同名同构,PA_PAGES 逐函数移植 portal)/ 官方插件列表高度改为「离弹窗底部约 100px」/ 撤掉自造的「我的密钥」分区(改由官方模型页承担)。

- web/portal.html:keys 页语义改名「平台共享密钥(未自配密钥的用户默认使用;仅管理员可改)」。scripts/verify-platform-admin-section.mjs:断言同步升级(含 zh/en 词典键集一致性、内联 HTML class 扫描)。
2026-09-13 22:27:49 +08:00

139 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* ensure-role-profile-patch.cjs — 按角色给 dsh profile 注入 cordis patch。
*
* 背景:
* · 2026-09-09:普通用户在设置面板不应看到「模型」分区(模型 KEY 由管理员经门户统一
* 管控,档案 03;且避免普通用户误配自用 key 绕过统一 key)。
* · **2026-09-13(档案 86):「模型」分区对普通用户放开** —— 用户要求把配置模型密钥
* 开放给用户自己配,且「界面交互和官方一模一样」(⇒ 直接用官方页,不仿制)。
* 实测:官方页在本环境**可用**(`/api/session/modelCatalog` 返回 200,"provider 目录
* 不可用"的旧判断已不成立)。配套改平台注入:用户自配 ⇒ 不注入共享 env
* (`src/web/server.ts` `userHasOwnKey()`,否则 env 优先级会静默盖掉用户配的 key)。
* 仍禁用:plugins / plugin-inventory / cordis(骨架插件禁任一都可能搞坏实例)。
* cordis patch 支持对 client 插件行
* `disabled: true`(dsh-app-boot applyEntryPatches:非 insert patch 按 id 合入
* overrides)——生效位置 = profile 层 `cordis.patch.yml`(实例启动时打包 client
* bundle,改后必须重启实例才生效;patchReload:live 对 client 增减不生效,已实测)。
*
* 用法:
* node ensure-role-profile-patch.cjs # 全部非 admin 用户
* node ensure-role-profile-patch.cjs guest # 指定用户名(可多个)
* node ensure-role-profile-patch.cjs --restart guest # 写后重启其实例(kill main,
* # 由 watchdog 拉新)
* 幂等:cordis.patch.yml 已含管理标记头或非默认空内容 → 跳过不覆盖。
*/
const { execFileSync } = require('node:child_process')
const { readFileSync, writeFileSync, existsSync } = require('node:fs')
const { join } = require('node:path')
const Database = require('/opt/dshs/node_modules/better-sqlite3')
const DB_PATH = '/var/lib/dshs/dshs.db'
const PROFILE = 'web' // 当前唯一 profile
const MARK = '# dshs role patch'
// --force:已由本脚本管理但内容落后(缺新版禁用项)时,整体升级为当前块。
const FORCE = process.argv.includes('--force')
const DISABLE_MODELS_BLOCK = [
'# dshs role patch: 收归核心插件开关(档案 15)',
'# admin 保留;由 ensure-role-profile-patch.cjs 管理,勿手改',
'# 148 个 @deepseek-ai 官方插件均为运行骨架,用户禁用任一都可能搞坏实例,',
'# 故插件栏 / 插件清单 / cordis 面板只对 admin 开放;ui-skill、ui-permission 保留给用户。',
'#',
'# ⚠️ ui-settings-models **自 2026-09-13 起不再禁用**(档案 86):用户要自助配置模型厂家与',
'# key(「界面交互和官方一模一样」)。配套:平台注入策略改为「用户自配则不注入共享 env」,',
'# 使模型页配的 key 真能生效 —— 见 src/web/server.ts 的 userHasOwnKey()。',
'- id: ui-settings-plugins',
' name: "@deepseek-ai/dsh-client-ui-settings-plugins"',
' disabled: true',
'- id: ui-settings-plugin-inventory',
' name: "@deepseek-ai/dsh-client-ui-settings-plugin-inventory"',
' disabled: true',
'- id: ui-cordis',
' name: "@deepseek-ai/dsh-client-ui-cordis"',
' disabled: true',
'',
].join('\n')
function isEmptyPatch(content) {
const body = content
.split('\n')
.map((l) => l.trim())
.filter((l) => l !== '' && !l.startsWith('#'))
return body.length === 0 || body.join('') === '[]'
}
function ensureUserPatch(user) {
const patchPath = join(user.home_dir, 'profiles', PROFILE, 'cordis.patch.yml')
if (!existsSync(patchPath)) {
return { user: user.username, action: 'NO_PROFILE', detail: 'profile 尚未创建(用户未首登 spawn);请先登录一次再跑' }
}
const current = readFileSync(patchPath, 'utf8')
if (current.includes(MARK)) {
// 需要升级的两种旧态:① 缺「插件开关收归」;② **还禁着 ui-settings-models**
// (2026-09-13 之前写入的块 —— 那一版把「模型」分区也对用户藏了,现已放开,见档案 86)。
const legacy =
!current.includes('ui-settings-plugins') || /^-\s*id:\s*ui-settings-models\s*$/m.test(current)
if (FORCE && legacy) {
writeFileSync(patchPath, DISABLE_MODELS_BLOCK, 'utf8')
return { user: user.username, action: 'upgraded', detail: '已升级(放开「模型」分区 + 保留核心插件开关收归)' }
}
return { user: user.username, action: 'skip', detail: '已由本脚本管理' }
}
if (!isEmptyPatch(current)) return { user: user.username, action: 'skip', detail: '用户已定制 cordis.patch.yml,不覆盖' }
writeFileSync(patchPath, DISABLE_MODELS_BLOCK, 'utf8')
return { user: user.username, action: 'wrote', detail: '已写入 disable models patch' }
}
function restartUserInstance(user) {
// kill main 实例 → orchestrator 崩溃自愈(scheduleRestart,指数退避)拉起新实例
// (读取新 patch 打包 bundle)。注:watchdog 因 enablePatch=false 不会启动(档案 20)。
const out = execFileSync('pgrep', ['-f', `--profile ${PROFILE}`], { encoding: 'utf8' }).trim()
const pids = out === '' ? [] : out.split('\n')
// 该用户实例 cwd = users/<id>/ws(与 DB home_dir 同根),用 uid 匹配
const uid = user.uid !== null && user.uid !== undefined ? String(user.uid) : null
const envCmd = 'ps -o pid,user,args -C node | grep -E "dsh --profile web"'
let killed = 0
try {
const lines = execFileSync('bash', ['-c', envCmd], { encoding: 'utf8' }).trim().split('\n')
for (const line of lines) {
const m = line.trim().match(/^\s*(\d+)\s+(\S+)/)
if (!m) continue
if (uid !== null && m[2] === `dsh-${user.id.replace(/-/g, '').slice(0, 20)}`) {
execFileSync('kill', [m[1]])
killed++
}
}
} catch {
// pgrep 无匹配等
}
return killed
}
function main() {
const args = process.argv.slice(2)
const restart = args.includes('--restart')
const usernames = args.filter((a) => !a.startsWith('--'))
const db = new Database(DB_PATH, { readonly: true })
let rows
if (usernames.length > 0) {
rows = usernames.map((u) => db.prepare('SELECT id, username, role, home_dir, uid FROM users WHERE username=?').get(u)).filter(Boolean)
} else {
rows = db.prepare("SELECT id, username, role, home_dir, uid FROM users WHERE role != 'admin'").all()
}
db.close()
if (rows.length === 0) {
console.log('no non-admin user found')
return
}
for (const user of rows) {
const r = ensureUserPatch(user)
if (restart && r.action === 'wrote') {
const k = restartUserInstance(user)
r.detail += `;已 kill 实例进程 ${k} 个(watchdog 将拉起新实例)`
}
console.log(JSON.stringify(r))
}
}
main()