chore(k8s): 下线 K8s 后端形态,移除依赖 @kubernetes/client-node

生产形态是单机 local(DEFAULT_DEPLOY_MODE=local,env 未覆盖)⇒ K8s 分支
在 local 下本来不可达;且该形态与官方 dsh 基座、插件体系均无关
=> 整体下线,并移除该形态唯一的第三方依赖。

移除(备份在 D:/github/_dsh_shenxian_K8s后端备份_20260915/,含还原命令与
「集群化方案要复用的模板清单」):
  src/supervisor/{k8s-spawner,leader,reconcile}.ts
  src/fs/k8s-user-fs.ts · src/tcp-bridge.ts · src/web/file-service.ts
  test/{k8s-spawner,leader}.test.mjs · scripts/smoke-file-service.mjs

改写调用方:cli.ts(5 条 import / 选主块 / file-service 与 tcp-bridge 两个
子命令 / dispatch / HELP)、web/server.ts(改为 fail-loud 守卫 + 恒用
LocalSpawner)、fs/provider.ts(只留 LocalUserFs)、package.json(测试与
smoke 入口),外加 3 处指向已删类型的悬空 JSDoc。

保留(集群化方案列为未来可选):deploy/ · poc/01-04 · Dockerfile.dsh ·
docs/k8s*.md(已加「代码已下线」状态横幅)· config.ts 的 K8s 配置字段与
DeployMode 联合类型。

验证:tsc --noEmit exit 0;npm test 36 测试 / 35 通过 / 0 失败 / 1 跳过;
npm run verify exit 0;依赖与被删符号全仓 0 命中。
This commit is contained in:
admin committed 2026-09-15 06:36:20 +08:00
1 parent ed0c3c0ad5
commit cf8b7b1f5c
19 files changed
+29 -2690

No files matched your search

-103
View File
@@ -1,103 +0,0 @@
// File sidecar round-trip: drive K8sUserFs against a real buildFileService
// instance and assert it behaves identically to LocalUserFs on the same volume.
// This is the k8s desktop-FS path (docs/k8s.md §4.10) exercised without a
// cluster: the sidecar is bound on loopback and the client's Service-DNS
// resolution is stubbed to point at it.
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { buildFileService } from '../lib/web/file-service.js'
import { K8sUserFs } from '../lib/fs/k8s-user-fs.js'
import { LocalUserFs } from '../lib/fs/local-user-fs.js'
import { UserFsError } from '../lib/fs/user-fs.js'
function assert(condition, message) {
if (!condition) throw new Error('ASSERT: ' + message)
}
async function rejectsWith(fn, code, message) {
try {
await fn()
} catch (err) {
assert(err instanceof UserFsError, `${message} (got ${err})`)
assert(err.code === code, `${message} (got ${err.code})`)
return
}
throw new Error('ASSERT: ' + message + ' (resolved instead)')
}
const dataRoot = mkdtempSync(join(tmpdir(), 'dsh-smoke-sidecar-'))
const userRoot = join(dataRoot, 'users', 'u1')
// The sidecar serves exactly this one user's root, as its Pod would.
const sidecar = buildFileService(userRoot, { bodyLimit: 8 * 1024 * 1024, logLevel: 'warn' })
await sidecar.listen({ host: '127.0.0.1', port: 0 })
const port = sidecar.server.address().port
// In-cluster this resolves to `dsh-files-<id>.<ns>.svc.cluster.local:8082`;
// here it points at the loopback sidecar bound above.
const local = new LocalUserFs(() => userRoot)
let ensured = 0
const remote = new K8sUserFs(async () => { ensured += 1 }, () => ({ host: '127.0.0.1', port }))
try {
await remote.initUserRoot('u1')
assert(ensured > 0, 'initUserRoot brings the sidecar up first')
// Seed a plugin profile so the catalog read has something to find.
const profile = join(userRoot, 'home', 'profiles', 'web')
mkdirSync(join(profile, 'node_modules', 'p1'), { recursive: true })
writeFileSync(join(profile, 'package.json'), JSON.stringify({ dsh: { profile: { bundles: ['p1', '@deepseek-ai/core'] } } }))
writeFileSync(join(profile, 'node_modules', 'p1', 'package.json'), JSON.stringify({ description: 'first plugin' }))
let entries = await remote.listDir('u1', '')
assert(entries.length === 0, 'fresh workspace is empty')
const dirName = await remote.createEntry('u1', '', 'proj', 'dir')
assert(dirName === 'proj', 'createEntry returns the sanitized name')
assert(await remote.isDirectory('u1', 'proj'), 'created entry is a directory')
const uploaded = await remote.upload('u1', 'proj', 'notes.txt', Buffer.from('hello sidecar'))
assert(uploaded === 'notes.txt', 'upload returns the sanitized name')
await remote.mkdir('u1', 'proj/sub')
// Same volume, two implementations → identical view.
entries = await remote.listDir('u1', 'proj')
const localEntries = await local.listDir('u1', 'proj')
assert(JSON.stringify(entries) === JSON.stringify(localEntries), 'sidecar and local agree on the listing')
const file = entries.find((e) => e.name === 'notes.txt')
assert(file.type === 'file' && file.size === 13, 'uploaded file has the right type/size')
const plugins = await remote.listInstalledPlugins('u1')
assert(plugins.length === 1 && plugins[0].id === 'p1', 'installation-scoped bundles are filtered out')
assert(plugins[0].description === 'first plugin', 'plugin description comes from its package.json')
assert(JSON.stringify(plugins) === JSON.stringify(await local.listInstalledPlugins('u1')), 'catalogs agree')
await remote.writeHandoff('u1', JSON.stringify({ command: 'echo hi' }))
// Error codes survive the HTTP hop as the same UserFsError the UI switches on.
await rejectsWith(() => remote.listDir('u1', '../../etc'), 'bad_path', 'traversal rejected')
await rejectsWith(() => remote.listDir('u1', 'nope'), 'not_found', 'missing dir is not_found')
await rejectsWith(() => remote.createEntry('u1', '', 'proj', 'dir'), 'exists', 'duplicate create is exists')
await rejectsWith(() => remote.createEntry('u1', 'nope', 'x', 'file'), 'parent_missing', 'missing parent')
await rejectsWith(() => remote.upload('u1', '', '..', Buffer.from('x')), 'bad_name', 'path in name rejected')
// resolvePath is pure POSIX path math on the in-Pod layout.
assert(
remote.resolvePath('u1', 'proj') === '/var/lib/dshs/users/u1/ws/proj',
'resolvePath yields the in-Pod path',
)
let escaped = false
try {
remote.resolvePath('u1', '../../etc')
} catch (err) {
escaped = err instanceof UserFsError && err.code === 'bad_path'
}
assert(escaped, 'resolvePath rejects traversal')
console.log('OK: file sidecar round-trip matches LocalUserFs')
} finally {
await sidecar.close()
rmSync(dataRoot, { recursive: true, force: true })
}