|
2 | 2 | /** |
3 | 3 | * JevLoop · 开发服务器 |
4 | 4 | * |
5 | | - * node --experimental-strip-types server.ts |
6 | | - * CWD_ROOT=/path/to/project node --experimental-strip-types server.ts |
| 5 | + * npx jevloop serve # 从包装出来的版本 |
| 6 | + * node --experimental-strip-types src/server.ts # 从 clone |
| 7 | + * CWD_ROOT=/path/to/project node --experimental-strip-types src/server.ts |
7 | 8 | * |
8 | 9 | * 把 agent loop 的事件流通过 SSE 推给浏览器,同时托管 web/ 下的界面。 |
9 | 10 | * 只用 Node 标准库 —— 没有 express,没有构建步骤。 |
|
17 | 18 | * 双向通信在这个界面里没有任何用途。 |
18 | 19 | * |
19 | 20 | * 会话落盘在 `JEVLOOP_HOME/sessions/`(默认 `~/.jevloop/`),规则见 |
20 | | - * `src/session-store.ts`。**盘上全留,只在读给 agent 用时截到 `MAX_TURNS` 轮** —— |
| 21 | + * `session-store.ts`。**盘上全留,只在读给 agent 用时截到 `MAX_TURNS` 轮** —— |
21 | 22 | * 以前是内存里的一个 Map,重启就没了,而且界面上看不出来。 |
22 | 23 | * |
| 24 | + * ── 为什么不能再拆 ──────────────────────────────────────────── |
| 25 | + * |
| 26 | + * `docs/CODE-STYLE.md` §12 问的是能不能用一句话说完它负责什么。这里能: |
| 27 | + * **这个文件是开发服务器** —— 收请求、把事件流推给浏览器、托管 `web/`。 |
| 28 | + * 路由按端点分节,每节都只是「解析请求 → 调内核 → 写响应」,而它们共享同一份 |
| 29 | + * 状态(会话、工作区、正在跑的 run);拆成 `server-routes.ts` 之类只会让 |
| 30 | + * 「一共有哪些端点」从一眼可见变成要翻三个文件。 |
| 31 | + * |
| 32 | + * 真的会长的那半已经在别处:会话存储、工作区登记、目录浏览各自成文件 |
| 33 | + * (`session-store.ts` / `workspace.ts` / `dir-browse.ts`),本文件只做接线。 |
| 34 | + * |
| 35 | + * ⚠️ **本文件在 `src/` 里,但它读的文件在包根。** `web/` 和 `DECISION.md` |
| 36 | + * 既不编译也不属于 `src/`,所以 `ROOT` 不是「本文件所在目录」,而是 |
| 37 | + * **往上找到的第一个有 `package.json` 的目录** —— 从 `src/` 跑和从 `dist/` 跑 |
| 38 | + * 都指回包根。写成前者的话,编译之后 `web/` 会指到 `dist/web`。 |
| 39 | + * |
23 | 40 | * @module JevLoop/server |
24 | 41 | */ |
25 | 42 |
|
26 | 43 | import { createServer, type IncomingMessage, type ServerResponse } from 'node:http' |
27 | 44 | import { mkdir, readFile, stat, writeFile } from 'node:fs/promises' |
28 | | -import { realpathSync } from 'node:fs' |
29 | | -import { basename, extname, isAbsolute, join, normalize, relative } from 'node:path' |
| 45 | +import { realpathSync, existsSync } from 'node:fs' |
| 46 | +import { basename, dirname, extname, isAbsolute, join, normalize, relative } from 'node:path' |
30 | 47 | import { fileURLToPath } from 'node:url' |
31 | 48 | import { homedir } from 'node:os' |
32 | 49 |
|
33 | | -import { Decider } from './src/decide.ts' |
34 | | -import { Meter } from './src/meter.ts' |
35 | | -import { runAgent } from './src/agent.ts' |
36 | | -import { resolveProvider, resolveGenerator } from './src/backends.ts' |
37 | | -import { loadEnv } from './src/env.ts' |
38 | | -import { parseDecisionDoc, summarize, headline, isGate } from './src/decisiondoc.ts' |
39 | | -import { compilePredicate } from './src/decision-compile.ts' |
40 | | -import type { AgentEvent } from './src/events.ts' |
41 | | -import { SessionStore, assertSessionId } from './src/session-store.ts' |
42 | | -import { migrateLegacyLayout } from './src/session-migrate.ts' |
43 | | -import { createDir, listDirs } from './src/dir-browse.ts' |
44 | | -import { WorkspaceStore } from './src/workspace.ts' |
45 | | -import { WorkspaceError, type WorkspaceErrorCode } from './src/vocab-workspace.ts' |
46 | | - |
47 | | -const ROOT = fileURLToPath(new URL('.', import.meta.url)) |
| 50 | +import { Decider } from './decide.ts' |
| 51 | +import { Meter } from './meter.ts' |
| 52 | +import { runAgent } from './agent.ts' |
| 53 | +import { parseDecisionDoc, summarize, headline, isGate } from './decisiondoc.ts' |
| 54 | +import { compilePredicate } from './decision-compile.ts' |
| 55 | +import type { AgentEvent } from './events.ts' |
| 56 | +import { SessionStore, assertSessionId } from './session-store.ts' |
| 57 | +import { migrateLegacyLayout } from './session-migrate.ts' |
| 58 | +import { createDir, listDirs } from './dir-browse.ts' |
| 59 | +import { WorkspaceStore } from './workspace.ts' |
| 60 | +import { WorkspaceError, type WorkspaceErrorCode } from './vocab-workspace.ts' |
| 61 | +// `backends` 和 `env` 都是 L6,和本文件同层 —— §11 只允许 L0 内部互相指涉、 |
| 62 | +// 以及 L2 指向定义角,同层直接 import 是违规的。走门面(`index.ts` 不受层约束), |
| 63 | +// 这也是 `cli.ts` 的写法。 |
| 64 | +import { resolveGenerator, resolveProvider, loadEnv } from './index.ts' |
| 65 | + |
| 66 | +/** |
| 67 | + * 包根目录:从本文件往上找到的第一个带 `package.json` 的目录。 |
| 68 | + * |
| 69 | + * **不能写成「本文件所在目录」** —— `web/` 和 `DECISION.md` 在包根, |
| 70 | + * 而本文件从 `src/server.ts` 跑时所在的是 `src/`、编译后是 `dist/`, |
| 71 | + * 两者都指不到包根。往上找 `package.json` 对两种位置都成立。 |
| 72 | + */ |
| 73 | +function findRoot(from: string): string { |
| 74 | + let dir = from |
| 75 | + for (;;) { |
| 76 | + if (existsSync(join(dir, 'package.json'))) return dir |
| 77 | + const up = dirname(dir) |
| 78 | + if (up === dir) throw new Error(`no package.json above ${from} — is this file installed correctly?`) |
| 79 | + dir = up |
| 80 | + } |
| 81 | +} |
| 82 | + |
| 83 | +const ROOT = findRoot(dirname(fileURLToPath(import.meta.url))) |
48 | 84 | const WEB_DIR = join(ROOT, 'web') |
49 | 85 | const DECISION_MD = join(ROOT, 'DECISION.md') |
50 | 86 | const PORT = Number(process.env.PORT ?? 7799) |
|
0 commit comments