Files
dsh_shenxian/ensure-role-profile-patch.cjs
T

127 lines
5.7 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;dsh 官方 web profile 的模型 provider 目录在此环境不可用,
* 且避免普通用户误配自用 key 绕过统一 key)。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 保留给用户。',
'- id: ui-settings-models',
' name: "@deepseek-ai/dsh-client-ui-settings-models"',
' disabled: true',
'- 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)) {
if (FORCE && !current.includes('ui-settings-plugins')) {
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()