Fix preset publication, volatile settings and client form binding

This commit is contained in:
oliver 2026-10-11 12:54:02 +08:00
parent fe0fe70d33
commit 0b9a3a82bc
66 changed files with 8546 additions and 3690 deletions

View file

@ -1,10 +1,14 @@
/**
* Build lib/index.js (host) and lib/agent-tools.js (Ops preset tool mount).
* Build lib/index.js (host) and lib/agent-tools*.js (preset tool mounts).
* Node refuses to strip TypeScript inside node_modules, so GitHub/npm installs
* must ship prebuilt JS. Usage: bun run scripts/build-host.mjs
*
* Agent-tools entries are built as self-contained bundles (no shared chunk).
* Cordis/Desktop load each export path independently; a split chunk once missed
* a connection-publish race and is harder to reason about under custom loaders.
*/
import { mkdirSync, writeFileSync } from 'node:fs'
import { basename, dirname, join } from 'node:path'
import { mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
@ -16,6 +20,7 @@ const external = [
'@deepseek-ai/dsh-credentials',
'@deepseek-ai/dsh-settings',
'@deepseek-ai/dsh-tools',
'@deepseek-ai/dsh-scope',
'@deepseek-ai/dsh-llm',
'@deepseek-ai/dsh-brand',
'@deepseek-ai/dsh-agent',
@ -39,6 +44,21 @@ const entries = [
mkdirSync(outDir, { recursive: true })
/** Drop split-build leftovers from 0.1.56–0.1.57. */
for (const name of readdirSync(outDir)) {
if (name === 'agent-tools-shared.js' || /^chunk-.*\.js$/.test(name)) {
rmSync(join(outDir, name), { force: true })
}
}
for (const name of ['_split-test', '_split-test2']) {
const target = resolve(outDir, name)
const within = relative(resolve(root), target)
if (within.startsWith('..') || isAbsolute(within) || dirname(target) !== resolve(outDir)) {
throw new Error(`Refusing cleanup outside the build directory: ${target}`)
}
rmSync(target, { recursive: true, force: true })
}
for (const { entry, out } of entries) {
const result = await Bun.build({
entrypoints: [entry],

View file

@ -17,7 +17,9 @@ import { composeNetxopsPresetArtifacts } from '../src/agent-preset-install.ts'
import { readFileSync } from 'node:fs'
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
const artifacts = composeNetxopsPresetArtifacts()
// This distributable declaration targets Desktop; runtime installation composes
// separately against the running Host instead of reusing this static template.
const artifacts = composeNetxopsPresetArtifacts('desktop')
const declarativePath = join(root, 'presets', 'netxops.preset.patch.yml')
writeFileSync(declarativePath, artifacts.declarativePatch)
console.log(

View file

@ -14,7 +14,7 @@
import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, symlinkSync } from 'node:fs'
import { homedir } from 'node:os'
import { dirname, join } from 'node:path'
import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
@ -27,6 +27,7 @@ const peers = [
'dsh-credentials',
'dsh-settings',
'dsh-tools',
'dsh-scope',
'dsh-llm',
]
@ -38,7 +39,7 @@ function profileArg() {
/**
* Prefer the shared healed fallback `profiles/node_modules` (one coherent peer
* set: schemastery 3.18.2 + dsh-* 0.1.5-rc.2). Do NOT prefer the Desktop
* set). Do NOT prefer the Desktop
* profile's schemastery first — Desktop 0.2 ships 3.18.4 which, when linked
* alone into this package, makes `Config({})` return cosmokit volatile proxies
* instead of defaults and Host apply fails ("web boot: dsh-netxops failed").
@ -64,9 +65,13 @@ function linkOne(shortName, target) {
const dest = join(root, 'node_modules', '@deepseek-ai', shortName)
mkdirSync(dirname(dest), { recursive: true })
if (existsSync(dest)) {
const within = relative(resolve(root, 'node_modules', '@deepseek-ai'), resolve(dest))
if (!within || within.startsWith('..') || isAbsolute(within) || within.includes('/') || within.includes('\\')) {
throw new Error(`refusing to replace path outside the peer directory: ${dest}`)
}
const st = lstatSync(dest)
if (st.isSymbolicLink() || st.isDirectory()) {
rmSync(dest, { recursive: true, force: true })
if (st.isSymbolicLink()) {
rmSync(dest, { force: true })
} else {
throw new Error(`refusing to replace non-link path ${dest}`)
}
@ -81,9 +86,11 @@ const dirs = candidateDirs(profile)
console.log(`DSH_HOME=${dshHome} profile=${profile}`)
console.log(`search:\n ${dirs.join('\n ')}`)
const coherent = dirs.find(dir => peers.every(name => existsSync(join(dir, name, 'package.json'))))
if (!coherent) throw new Error('No complete Harness peer set found; refusing to mix package generations.')
let missing = 0
for (const name of peers) {
const target = resolvePeer(name, dirs)
const target = resolvePeer(name, [coherent])
if (!target) {
console.error(`missing peer @deepseek-ai/${name}`)
missing++

View file

@ -18,8 +18,7 @@ param(
$ErrorActionPreference = 'Stop'
$Root = Split-Path -Parent $PSScriptRoot
$DstRoot = Join-Path $Root 'node_modules\@deepseek-ai'
# Shared heal first — profile-local schemastery (Desktop 0.2 = 3.18.4) breaks
# Config({}) when mixed with shared dsh-* 0.1.5-rc.2 (volatile proxies, Host fail).
# Select one complete peer set, avoiding packages from different Host generations.
$SearchRoots = @(
(Join-Path $DshHome 'profiles\node_modules\@deepseek-ai'),
(Join-Path $DshHome "profiles\$Profile\node_modules\@deepseek-ai"),
@ -33,15 +32,25 @@ $Peers = @(
'dsh-credentials',
'dsh-settings',
'dsh-tools',
'dsh-scope',
'dsh-llm'
)
New-Item -ItemType Directory -Force -Path $DstRoot | Out-Null
Write-Host "DSH_HOME=$DshHome profile=$Profile"
$CoherentRoot = $null
foreach ($searchRoot in $SearchRoots) {
$complete = $true
foreach ($peer in $Peers) {
if (-not (Test-Path -LiteralPath (Join-Path $searchRoot "$peer\package.json"))) { $complete = $false; break }
}
if ($complete) { $CoherentRoot = $searchRoot; break }
}
if (-not $CoherentRoot) { throw 'No complete Harness peer set found; refusing to mix package generations.' }
foreach ($name in $Peers) {
$src = $null
foreach ($root in $SearchRoots) {
foreach ($root in @($CoherentRoot)) {
$candidate = Join-Path $root $name
if (Test-Path (Join-Path $candidate 'package.json')) {
$src = $candidate
@ -52,12 +61,17 @@ foreach ($name in $Peers) {
throw "Peer @deepseek-ai/$name not found under:`n $($SearchRoots -join "`n ")"
}
$dst = Join-Path $DstRoot $name
$absoluteDst = [IO.Path]::GetFullPath($dst)
$absolutePeerRoot = [IO.Path]::GetFullPath($DstRoot).TrimEnd('\')
if ([IO.Path]::GetDirectoryName($absoluteDst) -ne $absolutePeerRoot) {
throw "Refusing to replace a path outside the peer directory: $absoluteDst"
}
if (Test-Path $dst) {
$item = Get-Item $dst -Force
if ($item.LinkType -eq 'Junction' -or $item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
cmd /c "rmdir `"$dst`""
Remove-Item -LiteralPath $absoluteDst -Force
} else {
Remove-Item -Recurse -Force $dst
throw "Refusing to replace a non-link peer directory: $absoluteDst"
}
}
New-Item -ItemType Junction -Path $dst -Target $src | Out-Null

View file

@ -0,0 +1,33 @@
/** Local UI QA using the real controller; no requests to Netx or the installed Host. */
import { createRequire } from 'node:module'
import { existsSync } from 'node:fs'
import { homedir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const root = dirname(dirname(fileURLToPath(import.meta.url)))
const home = process.env.DSH_HOME?.trim() || join(homedir(), '.dsh')
const anchors = ['desktop', 'web', ''].map(profile =>
join(home, 'profiles', profile, 'node_modules', '@deepseek-ai', 'dsh-client-ui-renderer', 'package.json'))
let require
for (const anchor of anchors.filter(existsSync)) {
const candidate = createRequire(anchor)
try { candidate.resolve('react-dom/client'); require = candidate; break } catch { /* Try another Host. */ }
}
if (!require) throw new Error('UI preview requires the installed Harness React runtime.')
const result = await Bun.build({ entrypoints: [join(root, 'examples/local-debug/ui-preview.tsx')],
target: 'browser', format: 'esm', define: { 'process.env.NODE_ENV': JSON.stringify('development') },
plugins: [{ name: 'host-react', setup(build) {
build.onResolve({ filter: /^(react(?:\/.*)?|react-dom\/client)$/ }, args => ({ path: require.resolve(args.path) }))
} }],
})
if (!result.success) throw new Error(result.logs.map(String).join('\n'))
const bundle = await result.outputs[0].text()
const server = Bun.serve({ hostname: '127.0.0.1', port: 0, fetch(request) {
if (new URL(request.url).pathname === '/app.js') return new Response(bundle, { headers: { 'content-type': 'text/javascript' } })
return new Response(`<!doctype html><html lang="zh"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Netx Ops · UI QA</title><style>body{margin:0;background:#f5f6f8;font-family:system-ui,sans-serif}main{max-width:820px;margin:24px auto;padding:0 20px}.preview-note{font-size:12px;color:#646a73;margin-bottom:16px}</style>
<main><p class="preview-note">本地 UI 验证 · 模拟预设与状态,仅在内存中保存。Local UI preview; simulated Host data.</p><div id="root"></div></main><script type="module" src="/app.js"></script></html>`,
{ headers: { 'content-type': 'text/html; charset=utf-8' } })
} })
console.log(`Netx Ops UI preview: ${server.url}`)

54
scripts/typecheck.mjs Normal file
View file

@ -0,0 +1,54 @@
/** Check both plugin faces against the locally installed Harness declarations. */
import { createRequire } from 'node:module'
import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
import { homedir, tmpdir } from 'node:os'
import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { spawnSync } from 'node:child_process'
const root = dirname(dirname(fileURLToPath(import.meta.url)))
const home = process.env.DSH_HOME?.trim() || join(homedir(), '.dsh')
const anchors = [join(root, 'package.json'), ...['desktop', 'web', ''].map(profile =>
join(home, 'profiles', profile, 'node_modules', '@deepseek-ai', 'dsh-client-ui-renderer', 'package.json'))]
const requires = anchors.filter(existsSync).flatMap(path => [createRequire(path), createRequire(realpathSync(path))])
function findPackage(name) {
for (const req of requires) {
try { return req.resolve(`${name}/package.json`) } catch { /* Try another installed profile. */ }
}
throw new Error(`Missing typecheck peer ${name}; install the Harness and run link:peers.`)
}
const paths = {}
for (const spec of [
'@deepseek-ai/dsh-client-ui-slots', '@deepseek-ai/dsh-client-locale/client',
'@deepseek-ai/dsh-client-ui-renderer/client', '@deepseek-ai/dsh-api-remotes/client',
'@deepseek-ai/dsh-client-ui-settings/client',
'@deepseek-ai/dsh-client-ui-settings-plugins/client',
]) {
const parts = spec.split('/'), pkgName = parts.slice(0, 2).join('/'), subpath = parts.slice(2).join('/')
const pkgPath = findPackage(pkgName), pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
const entry = pkg.exports?.[subpath ? `./${subpath}` : '.']
const types = typeof entry === 'object' ? entry.types : pkg.types
if (!types) throw new Error(`No declarations for ${spec}`)
paths[spec] = [join(dirname(pkgPath), types)]
}
const nodeTypes = dirname(findPackage('@types/node'))
const folder = mkdtempSync(join(tmpdir(), 'netxops-typecheck-'))
try {
const configPath = join(folder, 'tsconfig.json')
writeFileSync(configPath, JSON.stringify({ compilerOptions: {
target: 'ES2022', module: 'ESNext', moduleResolution: 'Bundler', jsx: 'react-jsx',
noEmit: true, strict: true, skipLibCheck: true, allowImportingTsExtensions: true,
types: ['node', 'react'], typeRoots: [dirname(nodeTypes), join(root, 'node_modules', '@types')], paths,
}, include: [join(root, 'src', '**', '*.ts'), join(root, 'src', '**', '*.tsx')].map(path => path.replaceAll('\\', '/')) }))
const result = spawnSync(process.execPath, [join(root, 'node_modules', 'typescript', 'bin', 'tsc'), '-p', configPath],
{ cwd: root, stdio: 'inherit' })
process.exitCode = result.status ?? 1
} finally {
// The only cleanup target is the exact unique temporary directory returned above.
const target = realpathSync(folder)
const within = relative(realpathSync(tmpdir()), target)
if (!within || within.startsWith('..') || isAbsolute(within) || !target.startsWith(resolve(folder))) {
throw new Error(`Refusing cleanup outside the temporary workspace: ${target}`)
}
rmSync(target, { recursive: true, force: true })
}