web.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. /**
  2. * `dsh web` — the web-shape assembly: startHost + dist resolution +
  3. * startWebServer + the URL line + signal wiring. Mixing host and carrier
  4. * concerns is this app module's job (packages stay single-sided).
  5. */
  6. import { parseArgs } from 'node:util'
  7. import { networkInterfaces } from 'node:os'
  8. import { createRequire } from 'node:module'
  9. import { mountWebPlugins, startHost } from '@deepseek-ai/dsh-host-runtime'
  10. import { createHostWebPluginRegistry, startWebServer } from '@deepseek-ai/dsh-host-webserver'
  11. const LOOPBACK_HOST = '127.0.0.1'
  12. const ALL_INTERFACES_HOST = '0.0.0.0'
  13. export async function runWeb(argv: string[]): Promise<void> {
  14. const { values } = parseArgs({
  15. args: argv,
  16. options: {
  17. host: { type: 'string', default: LOOPBACK_HOST },
  18. port: { type: 'string', default: '3080' },
  19. },
  20. allowPositionals: false,
  21. })
  22. if (values.host !== LOOPBACK_HOST && values.host !== ALL_INTERFACES_HOST) {
  23. process.stderr.write(
  24. `dsh web: invalid --host ${values.host}; expected ${LOOPBACK_HOST} or ${ALL_INTERFACES_HOST}\n`,
  25. )
  26. process.exit(1)
  27. }
  28. const hostAddress = values.host
  29. const port = Number(values.port)
  30. if (!Number.isInteger(port) || port < 0 || port > 65535) {
  31. process.stderr.write(`dsh web: invalid --port ${values.port}\n`)
  32. process.exit(1)
  33. }
  34. // A missing DEEPSEEK_API_KEY throws here (plugin load is fail-loud, uncaught by design).
  35. const host = await startHost({
  36. boot: {
  37. persistenceRoot: './.sessions',
  38. workspaceContext: { maxBytes: 65_536 },
  39. sessionTitleLlm: true,
  40. },
  41. })
  42. // Web UI plugin chain: in-memory Loader tree over the eight UI packages,
  43. // then the registry that feeds __DSH_BOOT__ and /plugins/<id>/client.js.
  44. const mounted = await mountWebPlugins(host.ctx)
  45. const webPlugins = createHostWebPluginRegistry({
  46. ctx: host.ctx,
  47. loader: mounted.loader,
  48. resolvePkgJson: mounted.resolvePkgJson,
  49. onError: (err: Error) => { process.stderr.write(`dsh web: plugin rescan: ${String(err)}\n`) },
  50. })
  51. // Published so the webserver invariant companion can audit manifest/bundle
  52. // consistency; nothing else reads this key.
  53. host.ctx.reflect.provide('webPlugins', webPlugins)
  54. // Dist location is workspace knowledge of this app: resolved through
  55. // @deepseek-ai/dsh-frontend's package exports, not configured.
  56. const require = createRequire(import.meta.url)
  57. let distIndex: string
  58. try {
  59. distIndex = require.resolve('@deepseek-ai/dsh-frontend/dist/index.html')
  60. } catch {
  61. process.stderr.write('dsh web: frontend dist not built; run pnpm --filter @deepseek-ai/dsh-frontend build first\n')
  62. await host.dispose()
  63. process.exit(1)
  64. }
  65. let exiting = false
  66. async function shutdown(code: number): Promise<void> {
  67. if (exiting) return
  68. exiting = true
  69. try {
  70. await server.close()
  71. await host.dispose()
  72. } finally {
  73. process.exit(code)
  74. }
  75. }
  76. let server: Awaited<ReturnType<typeof startWebServer>>
  77. try {
  78. server = await startWebServer(
  79. { host: hostAddress, port, distIndex, apiHandler: host.handler, webPlugins },
  80. (err: Error) => {
  81. process.stderr.write(`dsh web: ${String(err)}\n`)
  82. void shutdown(1)
  83. },
  84. )
  85. } catch (error: unknown) {
  86. // listen failed (EADDRINUSE…): no server to close, dispose the host directly.
  87. process.stderr.write(`dsh web: ${String(error)}\n`)
  88. await host.dispose()
  89. process.exit(1)
  90. }
  91. const lan = hostAddress === ALL_INTERFACES_HOST
  92. ? Object.values(networkInterfaces()).flat()
  93. .find(iface => iface !== undefined && iface.family === 'IPv4' && !iface.internal)
  94. : undefined
  95. const localUrl = `http://${LOOPBACK_HOST}:${server.port}`
  96. console.log(`dsh web: ${localUrl}${lan === undefined ? '' : ` (LAN: http://${lan.address}:${server.port})`}`)
  97. process.on('SIGTERM', () => { void shutdown(0) })
  98. process.on('SIGINT', () => { void shutdown(130) })
  99. }