@whispering233/ai-editor-server 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/client-dist/assets/index-Bi1eymph.js +14 -0
  2. package/client-dist/assets/index-j-zzWAuh.css +1 -0
  3. package/client-dist/index.html +22 -0
  4. package/dist/consistency.d.ts +33 -0
  5. package/dist/consistency.d.ts.map +1 -0
  6. package/dist/consistency.js +53 -0
  7. package/dist/consistency.js.map +1 -0
  8. package/dist/debug.d.ts +29 -0
  9. package/dist/debug.d.ts.map +1 -0
  10. package/dist/debug.js +105 -0
  11. package/dist/debug.js.map +1 -0
  12. package/dist/index.d.ts +61 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +284 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/middleware/error.d.ts +40 -0
  17. package/dist/middleware/error.d.ts.map +1 -0
  18. package/dist/middleware/error.js +64 -0
  19. package/dist/middleware/error.js.map +1 -0
  20. package/dist/middleware/project.d.ts +67 -0
  21. package/dist/middleware/project.d.ts.map +1 -0
  22. package/dist/middleware/project.js +146 -0
  23. package/dist/middleware/project.js.map +1 -0
  24. package/dist/routes/chat.d.ts +98 -0
  25. package/dist/routes/chat.d.ts.map +1 -0
  26. package/dist/routes/chat.js +478 -0
  27. package/dist/routes/chat.js.map +1 -0
  28. package/dist/routes/delta.d.ts +4 -0
  29. package/dist/routes/delta.d.ts.map +1 -0
  30. package/dist/routes/delta.js +144 -0
  31. package/dist/routes/delta.js.map +1 -0
  32. package/dist/routes/entity.d.ts +7 -0
  33. package/dist/routes/entity.d.ts.map +1 -0
  34. package/dist/routes/entity.js +126 -0
  35. package/dist/routes/entity.js.map +1 -0
  36. package/dist/routes/outline.d.ts +11 -0
  37. package/dist/routes/outline.d.ts.map +1 -0
  38. package/dist/routes/outline.js +250 -0
  39. package/dist/routes/outline.js.map +1 -0
  40. package/dist/routes/project.d.ts +27 -0
  41. package/dist/routes/project.d.ts.map +1 -0
  42. package/dist/routes/project.js +552 -0
  43. package/dist/routes/project.js.map +1 -0
  44. package/dist/routes/proposal.d.ts +4 -0
  45. package/dist/routes/proposal.d.ts.map +1 -0
  46. package/dist/routes/proposal.js +145 -0
  47. package/dist/routes/proposal.js.map +1 -0
  48. package/dist/routes/relation.d.ts +6 -0
  49. package/dist/routes/relation.d.ts.map +1 -0
  50. package/dist/routes/relation.js +87 -0
  51. package/dist/routes/relation.js.map +1 -0
  52. package/dist/routes/settings.d.ts +37 -0
  53. package/dist/routes/settings.d.ts.map +1 -0
  54. package/dist/routes/settings.js +91 -0
  55. package/dist/routes/settings.js.map +1 -0
  56. package/dist/routes/trash.d.ts +4 -0
  57. package/dist/routes/trash.d.ts.map +1 -0
  58. package/dist/routes/trash.js +135 -0
  59. package/dist/routes/trash.js.map +1 -0
  60. package/package.json +48 -0
package/dist/index.js ADDED
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env node
2
+ // @whispering233/ai-editor-server 入口(T6.1 服务骨架)
3
+ //
4
+ // 职责(doc/design/architecture.md 第 307-336 行「构建与部署」):
5
+ // - startServer(projectRoot, opts?):检测/初始化项目(决策 8)→ 装配 Hono(错误中间件 +
6
+ // 来源校验 + 项目上下文 + /api/v1/health 探活)→ 端口策略监听(dev 被占报错 / 生产 +1)→
7
+ // 可选打开浏览器(127.0.0.1,决策 8:禁 localhost)
8
+ // - SPA 静态托管:client/dist 静态文件 + 非 /api GET fallback 到 index.html(决策 8 单进程架构)
9
+ // - 直接执行(node packages/server/dist/index.js [projectRoot])时自动启动;业务路由(routes/)
10
+ // 留到切片 1 挂载(结构预留:health 旁并列注册即可)
11
+ // - bin 入口(打包安装):package.json "bin": {"ai-editor": "dist/index.js"}——
12
+ // shebang 必须是文件首行(tsc 构建保留),npm 全局/本地安装后生成 ai-editor 命令;
13
+ // argv[2] 为项目根(缺省 cwd),NODE_ENV 非 development 即生产态(端口占用自动 +1)
14
+ import { execFile } from "node:child_process";
15
+ import { readFile } from "node:fs/promises";
16
+ import { existsSync, realpathSync } from "node:fs";
17
+ import { dirname, extname, join, resolve, sep } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+ import { Hono } from "hono";
20
+ import { logger } from "hono/logger";
21
+ import { createAdaptorServer } from "@hono/node-server";
22
+ import { errorHandler, fail, ok } from "./middleware/error.js";
23
+ import { initDebugConfig, isCategoryEnabled } from "./debug.js";
24
+ import { chatRoutes } from "./routes/chat.js";
25
+ import { deltaRoutes } from "./routes/delta.js";
26
+ import { entityRoutes } from "./routes/entity.js";
27
+ import { settingsRoutes } from "./routes/settings.js";
28
+ import { closeProject, detectProject, originCheckMiddleware, projectMiddleware, setCurrentProject, } from "./middleware/project.js";
29
+ import { projectRoutes, setProjectRoot } from "./routes/project.js";
30
+ import { outlineRoutes } from "./routes/outline.js";
31
+ import { proposalRoutes } from "./routes/proposal.js";
32
+ import { trashRoutes } from "./routes/trash.js";
33
+ import { relationRoutes } from "./routes/relation.js";
34
+ import { logSoftDeleteReconcile, reconcileSoftDelete } from "./consistency.js";
35
+ /** 默认端口(决策 8 / 17;dev 态 Vite proxy 写死 3456) */
36
+ export const DEFAULT_PORT = 3456;
37
+ /** 生产态端口 +1 重试上限(决策 8:占用自动 +1) */
38
+ const MAX_PORT_ATTEMPTS = 20;
39
+ /** 静态文件 MIME(client/dist 产物类型) */
40
+ const CONTENT_TYPES = {
41
+ ".html": "text/html; charset=utf-8",
42
+ ".js": "text/javascript; charset=utf-8",
43
+ ".css": "text/css; charset=utf-8",
44
+ ".json": "application/json",
45
+ ".svg": "image/svg+xml",
46
+ ".png": "image/png",
47
+ ".jpg": "image/jpeg",
48
+ ".jpeg": "image/jpeg",
49
+ ".gif": "image/gif",
50
+ ".ico": "image/x-icon",
51
+ ".woff": "font/woff",
52
+ ".woff2": "font/woff2",
53
+ ".ttf": "font/ttf",
54
+ ".map": "application/json",
55
+ };
56
+ /**
57
+ * 探测 client/dist 位置(双路径解析,纯函数——baseDir 为当前模块所在目录):
58
+ * 1. **monorepo 开发态**:packages/server/{dist,src} → ../../client/dist(Vite 构建产物原位)
59
+ * 2. **打包安装态**(fallback):node_modules/@whispering233/ai-editor-server/dist → ../client-dist
60
+ * (prepack 时由 scripts/copy-client-dist.mjs 复制到包根,随 tarball 携带)
61
+ * 探测优先:monorepo 路径存在即用(开发态 client/dist 已构建时走 1);都不存在返回 2 的路径,
62
+ * SPA fallback 优雅降级(404 JSON 提示「client/dist 未构建」,不崩溃)。
63
+ * 导出供单测以 fixture 目录覆盖三种探测情形(defaultClientDist 保持私有调用)。
64
+ */
65
+ export function resolveClientDist(baseDir) {
66
+ const monoDist = resolve(baseDir, "../../client/dist");
67
+ if (existsSync(monoDist)) {
68
+ return monoDist;
69
+ }
70
+ return resolve(baseDir, "../client-dist");
71
+ }
72
+ function defaultClientDist() {
73
+ return resolveClientDist(dirname(fileURLToPath(import.meta.url)));
74
+ }
75
+ function contentTypeFor(file) {
76
+ return CONTENT_TYPES[extname(file).toLowerCase()] ?? "application/octet-stream";
77
+ }
78
+ /** 安全读取文件:不存在/目录/越权返回 null */
79
+ async function readFileSafe(filePath) {
80
+ try {
81
+ return await readFile(filePath);
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ /**
88
+ * 启动服务(决策 8 / 17):
89
+ * 1. 检测项目(detectProject):目录含 project.json → 打开并设为当前项目(部署场景「启动即用」);
90
+ * 无 project.json → 待命(不初始化、不建文件——前端 Dashboard 引导 create/open,S1.4)
91
+ * 2. 装配 Hono:errorHandler → originCheck → projectMiddleware → 路由(health + project + settings + SPA)
92
+ * 3. 监听端口:dev 被占直接报错;生产被占自动 +1 重试
93
+ * 4. 非 dev 且 openBrowser 默认开启时,打开 http://127.0.0.1:{实际端口}
94
+ */
95
+ export async function startServer(projectRoot, options = {}) {
96
+ const dev = options.dev ?? process.env.NODE_ENV === "development";
97
+ const port = options.port ?? DEFAULT_PORT;
98
+ const openBrowser = options.openBrowser ?? !dev;
99
+ const clientDist = options.clientDist ?? defaultClientDist();
100
+ // 创作根归一化(2026-08 修复):CLI 以相对路径启动(node packages/server/dist/index.js
101
+ // test-project)时,原值直接注入会导致 GET /api/v1/project/list 返回相对 rootPath,
102
+ // 前端 buildBookPath(rootPath, name) 拼出相对路径后,POST /project/create 的
103
+ // resolveProjectDir 中 isAbsolute 校验失败 → 400 INVALID_PROJECT_PATH。此处统一基于
104
+ // process.cwd() resolve 为绝对路径(对绝对输入幂等),detectProject/setProjectRoot
105
+ // 及其下游消费方全部使用归一化值;create/open 请求体 path 仍要求绝对(安全校验不变)。
106
+ const root = resolve(projectRoot);
107
+ // 调试配置初始化(**启动读一次**:<创作根>/.ai-editor/config.json——唯一来源,细粒度
108
+ // 五类别 chat/request/stream/usage/http;文件不存在/非法 JSON/结构不符 → 全关;
109
+ // 不阻断启动。运行中改配置文件不生效——热加载 YAGNI)
110
+ initDebugConfig(root);
111
+ // 检测语义(设计缺陷修复):不再无条件初始化——待命态下 GET /project/config → 409
112
+ // NO_PROJECT_OPEN,前端引导「新建/打开项目」(client store loadConfig 已处理该错误码)
113
+ const project = detectProject(root);
114
+ if (project !== null) {
115
+ setCurrentProject(project); // 启动即打开(决策 8 部署场景);null 则保持待命
116
+ // S4.2 启动一致性校验(决策 16 修订):以大纲节点软删为准补标 DB 关联记录
117
+ //(先 DB 后 JSON 崩溃窗口的幽灵形态兜底,幂等;无软删节点不输出日志)
118
+ logSoftDeleteReconcile(reconcileSoftDelete(project));
119
+ }
120
+ // 书架模式(S1.5):root = 创作根,GET /api/v1/project/list 扫描 books/ 子目录
121
+ // 需要创作根路径(与 currentProject 无关——待命态也要能列书);兼容旧语义:
122
+ // 创作根自身有 project.json 仍按 detectProject 打开,list 只列 books/(根自身不是书)
123
+ setProjectRoot(root);
124
+ const app = new Hono();
125
+ // 中间件装配顺序:错误兜底 →(调试)请求日志 → 来源校验 → 项目上下文注入
126
+ app.onError(errorHandler());
127
+ // 请求日志(hono 内置中间件,零新依赖):**仅 http 类别开启时挂载**——测试与日常
128
+ // 启动默认输出干净(hono logger 逐请求打印会刷屏);调试配置 categories 含 "http"(或
129
+ // env 回退全开)时每个请求打一行(方法 路径 状态码 耗时,hono logger 内置格式,
130
+ // console.log stdout)
131
+ if (isCategoryEnabled("http")) {
132
+ app.use("*", logger());
133
+ }
134
+ app.use("*", originCheckMiddleware());
135
+ app.use("*", projectMiddleware());
136
+ // 探活路由(本卡基础路由;切片 1 起在下方并列挂载 routes/ 业务路由)
137
+ app.get("/api/v1/health", (c) => c.json(ok({ status: "ok" })));
138
+ // 设置路由(S1.3):GET/PUT /api/v1/settings/llm(用户级配置,决策 17)
139
+ app.route("/api/v1/settings", settingsRoutes);
140
+ // 实体路由(S3.3):GET/POST /api/v1/entity/:type、GET/PUT/DELETE /:type/:id
141
+ app.route("/api/v1/entity", entityRoutes);
142
+ // 项目路由(S1.2):create/open/close/config(项目管理,决策 8/13/17)
143
+ app.route("/api/v1/project", projectRoutes);
144
+ // 大纲路由(S2.2):整树/创建/更新/移动/软删/路径(严格三层,决策 19)
145
+ app.route("/api/v1/outline", outlineRoutes);
146
+ // 回收站路由(S2.2 大纲侧;实体侧 S4 扩展):列表/还原/物理清除(决策 12)
147
+ app.route("/api/v1/trash", trashRoutes);
148
+ // 关系路由(S3.4):查询(k 跳)/创建(判重)/物理删(决策 2/12)
149
+ app.route("/api/v1/relation", relationRoutes);
150
+ // Delta 路由(S5.3):追加 / 按节点查询 / compute 状态计算(决策 9/12)
151
+ app.route("/api/v1/delta", deltaRoutes);
152
+ // 对话路由(U3):会话列表 / 消息历史(决策 18 按项目隔离;POST SSE 端点属后续切片)
153
+ app.route("/api/v1/chat", chatRoutes);
154
+ // 提案路由(S7.5):confirm/reject(决策 14:仅内存提案 + 快照重校验 + 项目绑定)
155
+ app.route("/api/v1/proposal", proposalRoutes);
156
+ // 兜底:/api/* → JSON 404;其他 GET/HEAD → 静态文件 → SPA fallback index.html(决策 8)
157
+ app.notFound(async (c) => {
158
+ const path = c.req.path;
159
+ if (path.startsWith("/api/")) {
160
+ return c.json(fail("NOT_FOUND", `未知 API 端点: ${path}`), 404);
161
+ }
162
+ if (c.req.method !== "GET" && c.req.method !== "HEAD") {
163
+ return c.json(fail("NOT_FOUND", `未知端点: ${path}`), 404);
164
+ }
165
+ // 静态文件(含 client 的 public/ 拷贝):路径必须解析在 clientDist 内(防目录穿越)
166
+ const clientRoot = resolve(clientDist);
167
+ const candidate = resolve(clientRoot, `.${path}`);
168
+ if (candidate.startsWith(clientRoot + sep)) {
169
+ const data = await readFileSafe(candidate);
170
+ if (data) {
171
+ return new Response(data, { headers: { "Content-Type": contentTypeFor(candidate) } });
172
+ }
173
+ }
174
+ // SPA fallback(决策 8:/* → index.html;client/dist 未构建时优雅降级提示)
175
+ const html = await readFileSafe(join(clientRoot, "index.html"));
176
+ if (!html) {
177
+ return c.json(fail("NOT_FOUND", "client/dist 未构建(请先运行 pnpm --filter @whispering233/ai-editor-client build)"), 404);
178
+ }
179
+ return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8" } });
180
+ });
181
+ // 端口策略:dev 单次尝试(被占报错);生产 EADDRINUSE 自动 +1(决策 8 / 17 修订)
182
+ const maxPort = dev ? port : port + MAX_PORT_ATTEMPTS - 1;
183
+ let server = null;
184
+ let actualPort = port;
185
+ for (let p = port; p <= maxPort; p++) {
186
+ try {
187
+ server = await listenOnce(app.fetch, p);
188
+ actualPort = p === 0 ? server.address().port : p;
189
+ break;
190
+ }
191
+ catch (err) {
192
+ const eaddrinuse = err.code === "EADDRINUSE";
193
+ if (!eaddrinuse || p === maxPort) {
194
+ if (eaddrinuse) {
195
+ throw new Error(`端口 ${port} 已被占用(${dev ? "dev 态不自动 +1,请指定其他端口" : `已尝试至 ${maxPort}`})`);
196
+ }
197
+ throw err;
198
+ }
199
+ // 生产态:+1 重试
200
+ }
201
+ }
202
+ if (server === null)
203
+ throw new Error("端口监听失败(不可达)");
204
+ if (openBrowser) {
205
+ await openBrowserUrl(`http://127.0.0.1:${actualPort}`); // 127.0.0.1 而非 localhost(决策 8)
206
+ }
207
+ return {
208
+ app,
209
+ server,
210
+ port: actualPort,
211
+ project,
212
+ close: async () => {
213
+ await new Promise((resolveClose) => server.close(() => resolveClose()));
214
+ if (project !== null) {
215
+ closeProject(project); // 待命态(project=null)无连接可释放
216
+ }
217
+ },
218
+ };
219
+ }
220
+ /** 基于 @hono/node-server 的监听:'listening' 成功 / 'error'(如 EADDRINUSE)失败 */
221
+ function listenOnce(fetchHandler, port) {
222
+ return new Promise((resolveListen, reject) => {
223
+ const server = createAdaptorServer({ fetch: fetchHandler, hostname: "127.0.0.1", port });
224
+ server.once("error", reject);
225
+ server.listen(port, "127.0.0.1", () => {
226
+ server.removeListener("error", reject);
227
+ resolveListen(server);
228
+ });
229
+ });
230
+ }
231
+ /** 打开浏览器(决策 8:xdg-open/open/start,失败静默——无图形环境不阻塞) */
232
+ export async function openBrowserUrl(url) {
233
+ const platform = process.platform;
234
+ const cmd = platform === "win32" ? "cmd" : platform === "darwin" ? "open" : "xdg-open";
235
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
236
+ try {
237
+ await new Promise((resolveOpen, reject) => {
238
+ execFile(cmd, args, (err) => (err ? reject(err) : resolveOpen()));
239
+ });
240
+ }
241
+ catch {
242
+ // 打开失败静默(无图形环境等)
243
+ }
244
+ }
245
+ // ============ 直接执行入口(bin/生产态:ai-editor [projectRoot]) ============
246
+ /**
247
+ * 解析 AI_EDITOR_PORT 环境变量(bin 入口用,防御非法输入):
248
+ * 合法范围 = 1-65535 的整数端口;未设置/空串/NaN/越界/非整数 → undefined,
249
+ * 调用方回退默认端口 DEFAULT_PORT(3456)。此前 `Number("abc")` → NaN 会传入
250
+ * 端口逻辑导致监听行为未定义;回退默认是文档声明端口,启动日志会打印实际端口,
251
+ * 无歧义且不中断启动(与生产态端口容错精神一致,不升级为启动失败)。
252
+ */
253
+ export function parsePortEnv(value) {
254
+ if (value === undefined || value === "")
255
+ return undefined;
256
+ const n = Number(value);
257
+ if (!Number.isInteger(n) || n < 1 || n > 65535)
258
+ return undefined;
259
+ return n;
260
+ }
261
+ //
262
+ // isDirectRun 判定(打包安装实测修复):npm 安装 bin 后 node_modules/.bin/ai-editor 是指向
263
+ // dist/index.js 的**符号链接**——经 bin 执行时 process.argv[1] 是 symlink 路径而非真实文件路径,
264
+ // 直接与 import.meta.url 比较会误判为「非直接执行」导致进程静默退出(无输出、exit 0)。
265
+ // 因此两侧都经 realpathSync 归一化后再比较。
266
+ const isDirectRun = process.argv[1] !== undefined &&
267
+ realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
268
+ if (isDirectRun) {
269
+ const projectRoot = process.argv[2] ?? process.cwd();
270
+ const dev = process.env.NODE_ENV === "development";
271
+ // AI_EDITOR_PORT 环境变量可覆盖默认端口(决策 8 端口策略;测试/多实例场景用,
272
+ // 如打包安装冒烟与 dev server 并存时指定独立端口);非法值(NaN/越界)回退默认 3456
273
+ const port = parsePortEnv(process.env.AI_EDITOR_PORT);
274
+ const handle = await startServer(projectRoot, { dev, ...(port !== undefined ? { port } : {}) });
275
+ console.log(handle.project === null
276
+ ? `[ai-editor] 服务已启动: http://127.0.0.1:${handle.port}(未打开项目,等待创建或打开: ${projectRoot})`
277
+ : `[ai-editor] 服务已启动: http://127.0.0.1:${handle.port}(已打开项目: ${handle.project.root})`);
278
+ const shutdown = () => {
279
+ void handle.close().finally(() => process.exit(0));
280
+ };
281
+ process.on("SIGINT", shutdown);
282
+ process.on("SIGTERM", shutdown);
283
+ }
284
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import type { ContentfulStatusCode } from "hono/utils/http-status";
2
+ import type { ErrorHandler } from "hono";
3
+ import type { ErrorCode } from "@whispering233/ai-editor-shared";
4
+ /** 服务端补充错误码(不在 shared ErrorCode 枚举内) */
5
+ export declare const SERVER_ERROR_CODES: readonly ["INTERNAL_ERROR", "FORBIDDEN", "NOT_FOUND", "NO_PROJECT_OPEN", "PROJECT_ALREADY_EXISTS", "LLM_API_KEY_MISSING"];
6
+ export type ServerErrorCode = (typeof SERVER_ERROR_CODES)[number];
7
+ export type ApiErrorCode = ErrorCode | ServerErrorCode;
8
+ /** 成功响应包裹(endpoints.md) */
9
+ export declare function ok<T>(data: T): {
10
+ success: true;
11
+ data: T;
12
+ };
13
+ /** 错误响应包裹(endpoints.md);fields 仅 VALIDATION_ERROR 附带(校验失败字段路径) */
14
+ export declare function fail(code: ApiErrorCode, message: string, fields?: string[]): {
15
+ success: false;
16
+ error: {
17
+ code: ApiErrorCode;
18
+ message: string;
19
+ fields?: string[];
20
+ };
21
+ };
22
+ /**
23
+ * 业务错误:路由内 throw new HttpError(status, code, message),
24
+ * 由 errorHandler 统一转成错误响应包裹(切片 1 起使用;本卡先定义)
25
+ */
26
+ export declare class HttpError extends Error {
27
+ readonly status: ContentfulStatusCode;
28
+ readonly code: ApiErrorCode;
29
+ constructor(status: ContentfulStatusCode, code: ApiErrorCode, message: string);
30
+ }
31
+ /**
32
+ * 统一错误处理(Hono 4 onError 处理器,**必须注册为 app.onError 而非 app.use**):
33
+ * HttpError → 按 status/code 透传;ZodError(路由内 schema.parse 抛出)→ 400 VALIDATION_ERROR(含 fields);
34
+ * 未处理异常 → 500 包裹。
35
+ * 说明:Hono 4 的 compose 在 dispatch 内部捕获路由错误并直接调用 onError,
36
+ * 错误不会沿 next() 链传播——middleware 内的 try/catch 捕获不到下游错误
37
+ * (实测 hono@4.12 compose.js),因此本处理器以 ErrorHandler 形式导出。
38
+ */
39
+ export declare function errorHandler(): ErrorHandler;
40
+ //# sourceMappingURL=error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../src/middleware/error.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AACzC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAEjE,wCAAwC;AACxC,eAAO,MAAM,kBAAkB,2HAOrB,CAAC;AACX,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAElE,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,eAAe,CAAC;AAEvD,2BAA2B;AAC3B,wBAAgB,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,CAEzD;AAED,kEAAkE;AAClE,wBAAgB,IAAI,CAClB,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,EAAE,GAChB;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE;QAAE,IAAI,EAAE,YAAY,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAA;CAAE,CAEvF;AAED;;;GAGG;AACH,qBAAa,SAAU,SAAQ,KAAK;IAClC,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;gBAChB,MAAM,EAAE,oBAAoB,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM;CAM9E;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,IAAI,YAAY,CAgB3C"}
@@ -0,0 +1,64 @@
1
+ // 统一错误处理(T6.1 + S1.2)
2
+ //
3
+ // 契约来源:doc/api/endpoints.md「通用约定」——
4
+ // 成功响应 { success: true, data: T };错误响应 { success: false, error: { code, message } }。
5
+ // ErrorCode 单一来源为 @whispering233/ai-editor-shared(types/api.ts);本文件补充服务端侧错误码
6
+ // (不在 shared 枚举内,与 client 的 CLIENT_NETWORK_ERROR 同类做法):
7
+ // INTERNAL_ERROR(未处理异常 500)、FORBIDDEN(来源校验拒绝 403)、NOT_FOUND(未知端点 404)、
8
+ // NO_PROJECT_OPEN(S1.2:无当前项目时业务操作 409)、PROJECT_ALREADY_EXISTS(S1.2:create 目录已是项目 409)
9
+ // 记录:错误码分散(shared 枚举 + 服务端补充 + client 补充)为已知技术债,MVP 不收敛
10
+ import { ZodError } from "zod";
11
+ /** 服务端补充错误码(不在 shared ErrorCode 枚举内) */
12
+ export const SERVER_ERROR_CODES = [
13
+ "INTERNAL_ERROR",
14
+ "FORBIDDEN",
15
+ "NOT_FOUND",
16
+ "NO_PROJECT_OPEN",
17
+ "PROJECT_ALREADY_EXISTS",
18
+ "LLM_API_KEY_MISSING", // S7.6:POST /chat 未配置 DeepSeek key(决策 17)——400
19
+ ];
20
+ /** 成功响应包裹(endpoints.md) */
21
+ export function ok(data) {
22
+ return { success: true, data };
23
+ }
24
+ /** 错误响应包裹(endpoints.md);fields 仅 VALIDATION_ERROR 附带(校验失败字段路径) */
25
+ export function fail(code, message, fields) {
26
+ return { success: false, error: { code, message, ...(fields ? { fields } : {}) } };
27
+ }
28
+ /**
29
+ * 业务错误:路由内 throw new HttpError(status, code, message),
30
+ * 由 errorHandler 统一转成错误响应包裹(切片 1 起使用;本卡先定义)
31
+ */
32
+ export class HttpError extends Error {
33
+ status;
34
+ code;
35
+ constructor(status, code, message) {
36
+ super(message);
37
+ this.name = "HttpError";
38
+ this.status = status;
39
+ this.code = code;
40
+ }
41
+ }
42
+ /**
43
+ * 统一错误处理(Hono 4 onError 处理器,**必须注册为 app.onError 而非 app.use**):
44
+ * HttpError → 按 status/code 透传;ZodError(路由内 schema.parse 抛出)→ 400 VALIDATION_ERROR(含 fields);
45
+ * 未处理异常 → 500 包裹。
46
+ * 说明:Hono 4 的 compose 在 dispatch 内部捕获路由错误并直接调用 onError,
47
+ * 错误不会沿 next() 链传播——middleware 内的 try/catch 捕获不到下游错误
48
+ * (实测 hono@4.12 compose.js),因此本处理器以 ErrorHandler 形式导出。
49
+ */
50
+ export function errorHandler() {
51
+ return (err, c) => {
52
+ if (err instanceof HttpError) {
53
+ return c.json(fail(err.code, err.message), err.status);
54
+ }
55
+ if (err instanceof ZodError) {
56
+ // 路由层 schema 校验失败(S1.3 settings PUT 等)→ 400 VALIDATION_ERROR,fields 为出错字段路径
57
+ const fields = err.issues.map((issue) => issue.path.join("."));
58
+ return c.json(fail("VALIDATION_ERROR", err.issues[0]?.message ?? "参数校验失败", fields), 400);
59
+ }
60
+ console.error("[server] 未处理异常:", err);
61
+ return c.json(fail("INTERNAL_ERROR", err instanceof Error ? err.message : "服务器内部错误"), 500);
62
+ };
63
+ }
64
+ //# sourceMappingURL=error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.js","sourceRoot":"","sources":["../../src/middleware/error.ts"],"names":[],"mappings":"AAAA,sBAAsB;AACtB,EAAE;AACF,oCAAoC;AACpC,uFAAuF;AACvF,6EAA6E;AAC7E,wDAAwD;AACxD,yEAAyE;AACzE,wFAAwF;AACxF,wDAAwD;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAC;AAK/B,wCAAwC;AACxC,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,gBAAgB;IAChB,WAAW;IACX,WAAW;IACX,iBAAiB;IACjB,wBAAwB;IACxB,qBAAqB,EAAE,+CAA+C;CAC9D,CAAC;AAKX,2BAA2B;AAC3B,MAAM,UAAU,EAAE,CAAI,IAAO;IAC3B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AACjC,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,IAAI,CAClB,IAAkB,EAClB,OAAe,EACf,MAAiB;IAEjB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;AACrF,CAAC;AAED;;;GAGG;AACH,MAAM,OAAO,SAAU,SAAQ,KAAK;IACzB,MAAM,CAAuB;IAC7B,IAAI,CAAe;IAC5B,YAAY,MAA4B,EAAE,IAAkB,EAAE,OAAe;QAC3E,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;QACxB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY;IAC1B,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE;QAChB,IAAI,GAAG,YAAY,SAAS,EAAE,CAAC;YAC7B,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;QACzD,CAAC;QACD,IAAI,GAAG,YAAY,QAAQ,EAAE,CAAC;YAC5B,4EAA4E;YAC5E,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YAC/D,OAAO,CAAC,CAAC,IAAI,CACX,IAAI,CAAC,kBAAkB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,CAAC,EACpE,GAAG,CACJ,CAAC;QACJ,CAAC;QACD,OAAO,CAAC,KAAK,CAAC,iBAAiB,EAAE,GAAG,CAAC,CAAC;QACtC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,GAAG,CAAC,CAAC;IAC7F,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,67 @@
1
+ import type { Context, MiddlewareHandler } from "hono";
2
+ import type { ProjectFileConfig } from "@whispering233/ai-editor-shared";
3
+ import { type Db } from "@whispering233/ai-editor-db";
4
+ /** data.db 文件名(决策 8:项目根目录) */
5
+ export declare const DATA_DB_FILE_NAME = "data.db";
6
+ /** 项目上下文:内存中单一 currentProject(data-flow.md 第 46 行,所有 API 共享) */
7
+ export interface ProjectContext {
8
+ root: string;
9
+ config: ProjectFileConfig;
10
+ db: Db;
11
+ }
12
+ /** Hono 上下文变量声明(routes 里 c.get("project") 获得类型) */
13
+ export interface ProjectVariables {
14
+ project: ProjectContext;
15
+ }
16
+ /** 从 Hono 上下文取项目上下文(路由内使用) */
17
+ export declare function getProject(c: Context<{
18
+ Variables: ProjectVariables;
19
+ }>): ProjectContext;
20
+ /** 设置当前项目(create/open 成功后调用;传 null 清空——close 时) */
21
+ export declare function setCurrentProject(project: ProjectContext | null): void;
22
+ /** 读取当前项目(可能为 null) */
23
+ export declare function getCurrentProject(): ProjectContext | null;
24
+ /**
25
+ * 路由内取当前项目;无已打开项目 → 409 NO_PROJECT_OPEN。
26
+ * (错误码为服务端补充码,不在 shared ErrorCode——记录待收敛,见 error.ts 注释)
27
+ */
28
+ export declare function requireCurrentProject(): ProjectContext;
29
+ /**
30
+ * 检测并打开项目(决策 8 启动流程 ④ 修订——设计缺陷修复):
31
+ * project.json 存在 → openDatabase 打开,返回项目上下文(打开语义,部署场景「启动即用」);
32
+ * project.json 不存在 → **返回 null(待命)**——不初始化、不建任何文件(含目录),
33
+ * 由前端 Dashboard 引导走 POST /project/create 或 /project/open(S1.4 开/建页;
34
+ * 此前无条件初始化导致引导永不显示、dev 态污染 packages/server 包目录)。
35
+ * project.json 损坏(JSON 解析失败)→ 抛错(readProjectFile 语义:不静默重建,防数据误伤)。
36
+ * 打开时不写 user_version——版本检测交给 open 路由的 ensureSchemaCompatible(S1.1);
37
+ * 显式初始化(create 路由)用 initProject(内部写 user_version=SCHEMA_VERSION)。
38
+ */
39
+ export declare function detectProject(root: string): ProjectContext | null;
40
+ /**
41
+ * 显式初始化新项目(S1.2 create 路由专用,决策 8「首次初始化三文件」语义):
42
+ * 建目录(mkdir recursive,含嵌套不存在的父目录)→ 写 project.json(id=proj- 前缀 nanoid、
43
+ * name 默认取目录名、language 默认 zh、schema_version=SCHEMA_VERSION,created_at/updated_at
44
+ * 应用层写当前 ISO 时间)→ 写 outline.json 最小空树 → openDatabase 建 data.db(自动建表)
45
+ * → setUserVersion(SCHEMA_VERSION)(S1.1 审核建议:brand-new 库立即写版本号,
46
+ * 避免后续 open 时 ensureSchemaCompatible 触发无意义重建并留空库 data.db.v0.bak)。
47
+ *
48
+ * @param configOverride 可选覆盖 {name?, language?, prompt?}(create 请求的 config 字段);
49
+ * 传入时 updated_at 一并刷新
50
+ * @returns 已打开的项目上下文(调用方负责 closeProject;create 路由创建后即关闭)
51
+ */
52
+ export declare function initProject(root: string, configOverride?: Partial<Pick<ProjectFileConfig, "name" | "language" | "prompt">>): ProjectContext;
53
+ /** 关闭项目:释放数据库连接(data-flow.md 第 46 行;WAL + synchronous=FULL 已即时落盘) */
54
+ export declare function closeProject(project: ProjectContext): void;
55
+ /**
56
+ * 来源校验中间件(决策 17 修订):
57
+ * Origin 头存在 → 校验其 host;Origin 缺失(地址栏直接导航)→ 退化为校验 Host 头 host。
58
+ * 两者皆拒 → 403(防 CSRF / DNS rebinding;DNS rebinding 下读操作同样是敏感操作)。
59
+ */
60
+ export declare function originCheckMiddleware(): MiddlewareHandler;
61
+ /**
62
+ * 项目上下文注入中间件:从 currentProject 单例读取并 c.set("project", ...),
63
+ * 路由经 getProject / requireCurrentProject 读取。create/open 切换单例后新请求自动生效;
64
+ * 无当前项目时 c.set 不执行(业务路由由 requireCurrentProject 兜底 409)。
65
+ */
66
+ export declare function projectMiddleware(): MiddlewareHandler;
67
+ //# sourceMappingURL=project.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../src/middleware/project.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AACvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AAEzE,OAAO,EAA+C,KAAK,EAAE,EAAE,MAAM,6BAA6B,CAAC;AAOnG,8BAA8B;AAC9B,eAAO,MAAM,iBAAiB,YAAY,CAAC;AAK3C,gEAAgE;AAChE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,iBAAiB,CAAC;IAC1B,EAAE,EAAE,EAAE,CAAC;CACR;AAED,mDAAmD;AACnD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,cAAc,CAAC;CACzB;AAED,8BAA8B;AAC9B,wBAAgB,UAAU,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,SAAS,EAAE,gBAAgB,CAAA;CAAE,CAAC,GAAG,cAAc,CAEtF;AAWD,mDAAmD;AACnD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,GAAG,IAAI,CAEtE;AAED,uBAAuB;AACvB,wBAAgB,iBAAiB,IAAI,cAAc,GAAG,IAAI,CAEzD;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI,cAAc,CAKtD;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAMjE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,cAAc,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,iBAAiB,EAAE,MAAM,GAAG,UAAU,GAAG,QAAQ,CAAC,CAAC,GAChF,cAAc,CAmBhB;AAED,qEAAqE;AACrE,wBAAgB,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,CAE1D;AAYD;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,iBAAiB,CASzD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,iBAAiB,CAQrD"}
@@ -0,0 +1,146 @@
1
+ // 项目上下文中间件(T6.1 + S1.2)
2
+ //
3
+ // 职责(doc/design/architecture.md 第 121-122 行 middleware/project.ts「项目路径注入」):
4
+ // 1. detectProject:检测 project.json——存在则打开(部署场景「启动即用」);不存在返回
5
+ // null 待命(不初始化、不建文件,由前端 Dashboard 引导 create/open;S1.4 开/建页依赖)
6
+ // initProject:显式初始化三文件(S1.2 create 路由专用,含建目录 + user_version)
7
+ // 2. currentProject 内存单例(S1.2,data-flow.md 第 46 行):create/open 切换、close 清空,
8
+ // 模块级可变状态由路由(routes/project.ts)读写,projectMiddleware 从状态注入 Hono 上下文
9
+ // 3. 来源校验(决策 17 修订):全部请求校验 Origin(缺失时退化为 Host)的 host
10
+ // ∈ {127.0.0.1, localhost, ::1},不匹配拒绝 403;**不校验端口**
11
+ // (端口 +1 可变;dev 态 Vite proxy 转发后端口为 5173,校验端口会误杀开发请求)
12
+ import { basename, join } from "node:path";
13
+ import { mkdirSync } from "node:fs";
14
+ import { generateProjectId } from "@whispering233/ai-editor-shared";
15
+ import { closeDatabase, openDatabase, setUserVersion } from "@whispering233/ai-editor-db";
16
+ import { readProjectFile, writeProjectFile } from "@whispering233/ai-editor-db";
17
+ import { writeOutlineFile } from "@whispering233/ai-editor-db";
18
+ import { SCHEMA_VERSION } from "@whispering233/ai-editor-db";
19
+ import { nowIso } from "@whispering233/ai-editor-db";
20
+ import { HttpError, fail } from "./error.js";
21
+ /** data.db 文件名(决策 8:项目根目录) */
22
+ export const DATA_DB_FILE_NAME = "data.db";
23
+ /** 来源白名单 host(决策 17:仅允许本机访问;IPv6 ::1 去括号后比对) */
24
+ const ALLOWED_HOSTS = ["127.0.0.1", "localhost", "::1"];
25
+ /** 从 Hono 上下文取项目上下文(路由内使用) */
26
+ export function getProject(c) {
27
+ return c.get("project");
28
+ }
29
+ // ============ currentProject 内存单例(S1.2) ============
30
+ //
31
+ // 语义(data-flow.md 第 46 行):单进程内存中只有一个 currentProject,所有 API 调用共享;
32
+ // create/open 切换它、close 清空它。多项目并发打开不在 MVP 范围(backlog)。
33
+ // 模块级可变状态 + 显式读写函数:路由层可读可写,中间件只读注入。
34
+ /** 当前打开的项目(null = 无) */
35
+ let currentProject = null;
36
+ /** 设置当前项目(create/open 成功后调用;传 null 清空——close 时) */
37
+ export function setCurrentProject(project) {
38
+ currentProject = project;
39
+ }
40
+ /** 读取当前项目(可能为 null) */
41
+ export function getCurrentProject() {
42
+ return currentProject;
43
+ }
44
+ /**
45
+ * 路由内取当前项目;无已打开项目 → 409 NO_PROJECT_OPEN。
46
+ * (错误码为服务端补充码,不在 shared ErrorCode——记录待收敛,见 error.ts 注释)
47
+ */
48
+ export function requireCurrentProject() {
49
+ if (currentProject === null) {
50
+ throw new HttpError(409, "NO_PROJECT_OPEN", "当前无已打开的项目,请先 POST /api/v1/project/open");
51
+ }
52
+ return currentProject;
53
+ }
54
+ /**
55
+ * 检测并打开项目(决策 8 启动流程 ④ 修订——设计缺陷修复):
56
+ * project.json 存在 → openDatabase 打开,返回项目上下文(打开语义,部署场景「启动即用」);
57
+ * project.json 不存在 → **返回 null(待命)**——不初始化、不建任何文件(含目录),
58
+ * 由前端 Dashboard 引导走 POST /project/create 或 /project/open(S1.4 开/建页;
59
+ * 此前无条件初始化导致引导永不显示、dev 态污染 packages/server 包目录)。
60
+ * project.json 损坏(JSON 解析失败)→ 抛错(readProjectFile 语义:不静默重建,防数据误伤)。
61
+ * 打开时不写 user_version——版本检测交给 open 路由的 ensureSchemaCompatible(S1.1);
62
+ * 显式初始化(create 路由)用 initProject(内部写 user_version=SCHEMA_VERSION)。
63
+ */
64
+ export function detectProject(root) {
65
+ const existing = readProjectFile(root);
66
+ if (existing === null) {
67
+ return null;
68
+ }
69
+ return { root, config: existing, db: openDatabase(join(root, DATA_DB_FILE_NAME)) };
70
+ }
71
+ /**
72
+ * 显式初始化新项目(S1.2 create 路由专用,决策 8「首次初始化三文件」语义):
73
+ * 建目录(mkdir recursive,含嵌套不存在的父目录)→ 写 project.json(id=proj- 前缀 nanoid、
74
+ * name 默认取目录名、language 默认 zh、schema_version=SCHEMA_VERSION,created_at/updated_at
75
+ * 应用层写当前 ISO 时间)→ 写 outline.json 最小空树 → openDatabase 建 data.db(自动建表)
76
+ * → setUserVersion(SCHEMA_VERSION)(S1.1 审核建议:brand-new 库立即写版本号,
77
+ * 避免后续 open 时 ensureSchemaCompatible 触发无意义重建并留空库 data.db.v0.bak)。
78
+ *
79
+ * @param configOverride 可选覆盖 {name?, language?, prompt?}(create 请求的 config 字段);
80
+ * 传入时 updated_at 一并刷新
81
+ * @returns 已打开的项目上下文(调用方负责 closeProject;create 路由创建后即关闭)
82
+ */
83
+ export function initProject(root, configOverride) {
84
+ mkdirSync(root, { recursive: true });
85
+ const now = nowIso();
86
+ const config = {
87
+ id: generateProjectId(), // proj- 前缀(endpoints.md id 约定)
88
+ name: basename(root),
89
+ language: "zh",
90
+ prompt: "",
91
+ schema_version: SCHEMA_VERSION,
92
+ current_position: null,
93
+ created_at: now,
94
+ updated_at: now,
95
+ ...configOverride, // 覆盖参数(若有);updated_at 已含当前时间
96
+ };
97
+ writeProjectFile(root, config);
98
+ writeOutlineFile(root, { id: "root", type: "root", schema_version: SCHEMA_VERSION, children: [] });
99
+ const db = openDatabase(join(root, DATA_DB_FILE_NAME)); // 文件不存在则创建 + 自动建表
100
+ setUserVersion(db, SCHEMA_VERSION); // 与 project.json/outline.json 的 schema_version 同步(决策 13)
101
+ return { root, config, db };
102
+ }
103
+ /** 关闭项目:释放数据库连接(data-flow.md 第 46 行;WAL + synchronous=FULL 已即时落盘) */
104
+ export function closeProject(project) {
105
+ closeDatabase(project.db);
106
+ }
107
+ /** 从 Origin / Host 头提取 hostname(端口剥离;IPv6 去括号) */
108
+ function extractHostname(value) {
109
+ try {
110
+ const url = new URL(value.includes("://") ? value : `http://${value}`);
111
+ return url.hostname.replace(/^\[|\]$/g, "");
112
+ }
113
+ catch {
114
+ return "";
115
+ }
116
+ }
117
+ /**
118
+ * 来源校验中间件(决策 17 修订):
119
+ * Origin 头存在 → 校验其 host;Origin 缺失(地址栏直接导航)→ 退化为校验 Host 头 host。
120
+ * 两者皆拒 → 403(防 CSRF / DNS rebinding;DNS rebinding 下读操作同样是敏感操作)。
121
+ */
122
+ export function originCheckMiddleware() {
123
+ return async (c, next) => {
124
+ const origin = c.req.header("origin");
125
+ const value = origin ?? c.req.header("host") ?? "";
126
+ if (!ALLOWED_HOSTS.includes(extractHostname(value))) {
127
+ return c.json(fail("FORBIDDEN", "来源校验失败:仅允许本机(127.0.0.1/localhost/::1)访问"), 403);
128
+ }
129
+ await next();
130
+ };
131
+ }
132
+ /**
133
+ * 项目上下文注入中间件:从 currentProject 单例读取并 c.set("project", ...),
134
+ * 路由经 getProject / requireCurrentProject 读取。create/open 切换单例后新请求自动生效;
135
+ * 无当前项目时 c.set 不执行(业务路由由 requireCurrentProject 兜底 409)。
136
+ */
137
+ export function projectMiddleware() {
138
+ return async (c, next) => {
139
+ const project = getCurrentProject();
140
+ if (project !== null) {
141
+ c.set("project", project);
142
+ }
143
+ await next();
144
+ };
145
+ }
146
+ //# sourceMappingURL=project.js.map