Files
dsh_ai1net_server/归档/试验与样例/poc-dsh-local/workspace-scoped-picker/ensure-workspace-picker.cjs
T
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

190 lines
7.9 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-workspace-picker.cjs —— 全量/新用户「目录选择器收敛」自动铺开(幂等)
*
* 做两件事(缺一不可):
* ① 把受限目录选择器插件装进该用户的 profile(每个用户 profile 独立,必须逐用户 pnpm add)
* ② 把「平台段」写进该用户的 <profile>/cordis.patch.yml(让 dsh 启动时加载插件并 disable 官方 picker)
*
* 用法:
* node ensure-workspace-picker.cjs # 全部用户(幂等),产物取 /opt/dsh/artifacts 下最新
* node ensure-workspace-picker.cjs --tgz <path> # 指定插件 tgz
* node ensure-workspace-picker.cjs --dry-run # 只打印计划
* node ensure-workspace-picker.cjs --restart # 写完后 kill 实例(崩溃自愈会用新配置拉起)
* node ensure-workspace-picker.cjs admin guest # 指定用户名
* node ensure-workspace-picker.cjs --user-id <uuid> # 指定用户(编排器自愈用,档案 18 v3 第二层)
*
* 幂等性:
* · 平台段以 BEGIN/END 标记包裹;已存在即跳过(不改内容)
* · 插件按已装版本比对;版本一致即跳过安装
* 安全:只追加平台段、不触碰既有内容(角色 patch 等);失败逐用户隔离,不影响他人。
*/
const { execFileSync } = require('node:child_process')
const { existsSync, readFileSync, readdirSync, writeFileSync } = require('node:fs')
const { join } = require('node:path')
const Database = require('/opt/dshs/node_modules/better-sqlite3')
const DB = '/var/lib/dshs/dshs.db'
const ARTIFACTS = '/opt/dsh/artifacts'
const PROFILE = 'web'
const BEGIN = '# >>> platform: workspace-scoped-picker (managed by ensure-workspace-picker.cjs)'
const END = '# <<< platform: workspace-scoped-picker'
// marker 宽松匹配:历史上有过 `…picker-patch.cjs` 的旧标记形式,必须同样识别(2026-09-11 事故)
const BEGIN_RE = /^# >>> platform: workspace-scoped-picker/
const END_RE = /^# <<< platform: workspace-scoped-picker/
/** 剥离**所有**平台段(任意标记形式),返回剩余正文与剥离份数(用于自愈重复段)。 */
function stripPlatformSegments(text) {
const kept = []
let skipping = false
let removed = 0
for (const line of text.split('\n')) {
if (BEGIN_RE.test(line)) {
skipping = true
removed += 1
continue
}
if (skipping) {
if (END_RE.test(line)) skipping = false
continue
}
kept.push(line)
}
return { body: kept.join('\n').trim(), removed }
}
const argv = process.argv.slice(2)
const DRY = argv.includes('--dry-run')
const RESTART = argv.includes('--restart')
const valueOf = (flag) => {
const i = argv.indexOf(flag)
return i >= 0 && argv[i + 1] !== undefined && !argv[i + 1].startsWith('--') ? argv[i + 1] : ''
}
const tgzFlag = valueOf('--tgz')
const onlyIds = valueOf('--user-id') === '' ? [] : [valueOf('--user-id')]
// 位置参数 = 用户名(排除各 flag 的取值)
const flagValues = new Set([tgzFlag, ...onlyIds].filter((v) => v !== ''))
const only = argv.filter((a) => !a.startsWith('--') && !flagValues.has(a))
function pickTgz() {
if (tgzFlag !== '') return tgzFlag
const files = readdirSync(ARTIFACTS)
.filter((f) => /^workspace-scoped-picker-.*\.tgz$/.test(f))
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
if (files.length === 0) throw new Error(`未在 ${ARTIFACTS} 找到插件产物`)
return join(ARTIFACTS, files[files.length - 1])
}
const TGZ = pickTgz()
const VER = (TGZ.match(/workspace-scoped-picker-(.+)\.tgz$/) || [])[1] || 'unknown'
const BLOCK = [
BEGIN,
'# 目录选择器收敛(档案 18 v3):官方对话框 UI 保留(单独插入 client 面),',
'# host 面换成受限实现(根=自有 ws,越界拒绝);插件 client 面再注入 CSS 隐藏「改路径」入口。',
'- insert:',
' - id: workspace-scoped-picker',
' name: "@dsh-local/workspace-scoped-picker"',
' - id: ui-directory-picker-browse',
' name: "@deepseek-ai/dsh-client-ui-directory-picker-browse"',
'- id: directory-picker',
' name: "@deepseek-ai/dsh-host-directory-picker-auto"',
' disabled: true',
END,
'',
].join('\n')
const db = new Database(DB, { readonly: true })
const users = db
.prepare('SELECT id, username, uid, home_dir FROM users')
.all()
.filter(
(u) =>
(only.length === 0 && onlyIds.length === 0) || only.includes(u.username) || onlyIds.includes(u.id),
)
console.log(`插件产物: ${TGZ}(版本 ${VER})`)
for (const user of users) {
const profileDir = join(user.home_dir, 'profiles', PROFILE)
const patchPath = join(profileDir, 'cordis.patch.yml')
// 2026-09-11 修复:不再把 tgz 复制进用户工作区、也不再让 pnpm 把 store/cache 写进 ws。
// 旧做法(HOME=<ws> pnpm add file:<ws>/xxx.tgz)会在 ws 里生成 .local/(pnpm store)、
// .cache/(metadata)与 *.tgz —— 实测污染 admin ws 达 17MB / 2045 个文件。
// 现在:直接用 artifacts 里的 tgz 绝对路径(root 可读、全局只读),store/cache 显式指向 <home>。
const storeDir = join(user.home_dir, '.pnpm-store')
const cacheDir = join(user.home_dir, '.pnpm-cache')
if (!existsSync(profileDir)) {
console.log(` ${user.username}: NO_PROFILE(用户还没首登 spawn;下次 provisioning/启动会补)`)
continue
}
// ① 平台段(先剥离所有旧段再比对 → 天然自愈重复/旧标记)
const raw = existsSync(patchPath) ? readFileSync(patchPath, 'utf8') : ''
const { body, removed } = stripPlatformSegments(raw)
const normalized = body === '' || body === '[]' ? '' : body + '\n\n'
const want = normalized + BLOCK
const needPatch = want !== raw
// ② 插件版本
const installed = (() => {
try {
const pj = join(profileDir, 'node_modules', '@dsh-local', 'workspace-scoped-picker', 'package.json')
return JSON.parse(readFileSync(pj, 'utf8')).version
} catch {
return null
}
})()
const needInstall = installed !== VER
if (!needPatch && !needInstall) {
console.log(` ${user.username}: skip(平台段已在,插件 v${installed})`)
continue
}
if (DRY) {
console.log(` ${user.username}: [dry-run] patch=${needPatch} install=${needInstall}${removed > 1 ? ` 旧段=${removed}` : ''}`)
continue
}
if (needPatch) {
writeFileSync(patchPath, want, 'utf8')
try {
execFileSync('chown', [`${user.uid}:${user.uid}`, patchPath])
} catch {}
console.log(` ${user.username}: 平台段已写入${removed > 1 ? `(并自愈了 ${removed} 份重复/旧标记段)` : ''}`)
}
if (needInstall) {
try {
// profile 若为 pnpm workspace 根,必须 -w(否则 ERR_PNPM_ADDING_TO_ROOT)
const isRoot = existsSync(join(profileDir, 'pnpm-workspace.yaml'))
const args = ['--reuid', String(user.uid), '--regid', String(user.uid), '--clear-groups',
'env', `HOME=${user.home_dir}`, 'pnpm', 'add',
'--store-dir', storeDir, '--cache-dir', cacheDir]
if (isRoot) args.push('-w')
args.push(`file:${TGZ}`)
execFileSync('setpriv', args, { cwd: profileDir, stdio: 'pipe', timeout: 180000 })
console.log(` ${user.username}: 插件已装 v${VER}${isRoot ? '(-w)' : ''}(store/cache 在 home,ws 保持干净)`)
} catch (err) {
console.log(` ${user.username}: 插件安装失败 → ${String(err.message || err).split('\n')[0]}`)
continue
}
}
if (RESTART) {
try {
const out = execFileSync('ps', ['-eo', 'pid,user', '--no-headers'], { encoding: 'utf8' })
for (const line of out.split('\n')) {
const [pid, uname] = line.trim().split(/\s+/)
if (pid && uname === `dsh-${user.uid}`) {
try {
process.kill(Number(pid))
} catch {}
}
}
console.log(` ${user.username}: 实例已重启(自愈拉起)`)
} catch {}
}
}
db.close()
console.log('done')