2026-09-13 16:18:10 +08:00
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
/**
|
|
|
|
|
|
* ensure-role-profile-patch.cjs — 按角色给 dsh profile 注入 cordis patch。
|
|
|
|
|
|
*
|
2026-09-13 22:27:49 +08:00
|
|
|
|
* 背景:
|
|
|
|
|
|
* · 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 插件行
|
2026-09-13 16:18:10 +08:00
|
|
|
|
* `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 = [
|
2026-09-13 22:27:49 +08:00
|
|
|
|
'# dshs role patch: 收归核心插件开关(档案 15)',
|
2026-09-13 16:18:10 +08:00
|
|
|
|
'# admin 保留;由 ensure-role-profile-patch.cjs 管理,勿手改',
|
|
|
|
|
|
'# 148 个 @deepseek-ai 官方插件均为运行骨架,用户禁用任一都可能搞坏实例,',
|
|
|
|
|
|
'# 故插件栏 / 插件清单 / cordis 面板只对 admin 开放;ui-skill、ui-permission 保留给用户。',
|
2026-09-13 22:27:49 +08:00
|
|
|
|
'#',
|
|
|
|
|
|
'# ⚠️ ui-settings-models **自 2026-09-13 起不再禁用**(档案 86):用户要自助配置模型厂家与',
|
|
|
|
|
|
'# key(「界面交互和官方一模一样」)。配套:平台注入策略改为「用户自配则不注入共享 env」,',
|
|
|
|
|
|
'# 使模型页配的 key 真能生效 —— 见 src/web/server.ts 的 userHasOwnKey()。',
|
2026-09-13 16:18:10 +08:00
|
|
|
|
'- 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')
|
2026-09-13 22:27:49 +08:00
|
|
|
|
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: '已由本脚本管理' }
|
2026-09-13 16:18:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
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()
|