@whispering233/ai-editor-server 0.0.33 → 0.0.38
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.
- package/client-dist/assets/{index-oxaM6dHH.css → index-IF0z2BR1.css} +1 -1
- package/client-dist/assets/{index-tfvJfexp.js → index-Sx1BGdOS.js} +196 -196
- package/client-dist/index.html +2 -2
- package/dist/backup.d.ts +135 -25
- package/dist/backup.d.ts.map +1 -1
- package/dist/backup.js +348 -68
- package/dist/backup.js.map +1 -1
- package/dist/cloud/auto-push.d.ts +39 -0
- package/dist/cloud/auto-push.d.ts.map +1 -0
- package/dist/cloud/auto-push.js +145 -0
- package/dist/cloud/auto-push.js.map +1 -0
- package/dist/cloud/device.d.ts +3 -0
- package/dist/cloud/device.d.ts.map +1 -0
- package/dist/cloud/device.js +15 -0
- package/dist/cloud/device.js.map +1 -0
- package/dist/cloud/state.d.ts +59 -0
- package/dist/cloud/state.d.ts.map +1 -0
- package/dist/cloud/state.js +162 -0
- package/dist/cloud/state.js.map +1 -0
- package/dist/cloud/sync.d.ts +102 -0
- package/dist/cloud/sync.d.ts.map +1 -0
- package/dist/cloud/sync.js +447 -0
- package/dist/cloud/sync.js.map +1 -0
- package/dist/cloud/webdav.d.ts +69 -0
- package/dist/cloud/webdav.d.ts.map +1 -0
- package/dist/cloud/webdav.js +326 -0
- package/dist/cloud/webdav.js.map +1 -0
- package/dist/device-name.d.ts +3 -0
- package/dist/device-name.d.ts.map +1 -0
- package/dist/device-name.js +13 -0
- package/dist/device-name.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/middleware/error.d.ts +8 -2
- package/dist/middleware/error.d.ts.map +1 -1
- package/dist/middleware/error.js +18 -1
- package/dist/middleware/error.js.map +1 -1
- package/dist/middleware/project.d.ts +27 -3
- package/dist/middleware/project.d.ts.map +1 -1
- package/dist/middleware/project.js +79 -6
- package/dist/middleware/project.js.map +1 -1
- package/dist/routes/cloud.d.ts +9 -0
- package/dist/routes/cloud.d.ts.map +1 -0
- package/dist/routes/cloud.js +206 -0
- package/dist/routes/cloud.js.map +1 -0
- package/dist/routes/delta.d.ts.map +1 -1
- package/dist/routes/delta.js +20 -2
- package/dist/routes/delta.js.map +1 -1
- package/dist/routes/project.d.ts.map +1 -1
- package/dist/routes/project.js +35 -18
- package/dist/routes/project.js.map +1 -1
- package/dist/routes/relation.d.ts.map +1 -1
- package/dist/routes/relation.js +33 -3
- package/dist/routes/relation.js.map +1 -1
- package/package.json +7 -7
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Context, MiddlewareHandler } from "hono";
|
|
2
2
|
import type { ProjectFileConfig } from "@whispering233/ai-editor-shared";
|
|
3
3
|
import { type Db } from "@whispering233/ai-editor-db";
|
|
4
|
+
import { type MigrationResult } from "@whispering233/ai-editor-db";
|
|
4
5
|
/** data.db 文件名(项目根目录) */
|
|
5
6
|
export declare const DATA_DB_FILE_NAME = "data.db";
|
|
6
7
|
/** 项目上下文:内存中单一 currentProject(所有 API 共享) */
|
|
@@ -31,15 +32,38 @@ export declare function getCurrentProject(): ProjectContext | null;
|
|
|
31
32
|
* (错误码为服务端补充码,不在 shared ErrorCode——记录待收敛,见 error.ts 注释)
|
|
32
33
|
*/
|
|
33
34
|
export declare function requireCurrentProject(): ProjectContext;
|
|
35
|
+
/**
|
|
36
|
+
* data.db 打开 + schema 版本对齐的**唯一管道**(卡 2.8 收敛):
|
|
37
|
+
* 开机路径(detectProject)与显式 `POST /project/open` 共用,避免两条路径语义漂移。
|
|
38
|
+
*
|
|
39
|
+
* 语义(`docs/db/schema.md`「schema 版本与迁移」三态分流):
|
|
40
|
+
* - `user_version === SCHEMA_VERSION` → 原连接返回(rebuilt=false);
|
|
41
|
+
* - 旧版本**有迁移路径** → 前向迁移(迁移前自动快照 `data.db.v{n}.{时间戳}.bak`);
|
|
42
|
+
* - 旧版本**无迁移路径** → 删库重建兜底(备份 `data.db.v{n}.bak` + outline 重置);
|
|
43
|
+
* - **未来版本** → 关闭连接并抛 `SchemaVersionError`(调用方决定策略:
|
|
44
|
+
* 路由映射 409 `PROJECT_VERSION_NEWER`;开机态回待命)。
|
|
45
|
+
*
|
|
46
|
+
* @returns 已对齐版本的活动连接 + 迁移/重建结果(路由用于响应提示)
|
|
47
|
+
* @throws SchemaVersionError 未来版本拒绝打开;迁移/重建过程的 I/O 错误
|
|
48
|
+
*/
|
|
49
|
+
export declare function openProjectDatabase(dir: string): {
|
|
50
|
+
db: Db;
|
|
51
|
+
result: MigrationResult;
|
|
52
|
+
};
|
|
34
53
|
/**
|
|
35
54
|
* 检测并打开项目(启动流程 ④ 修订——设计缺陷修复):
|
|
36
|
-
* project.json 存在 →
|
|
55
|
+
* project.json 存在 → 走 `openProjectDatabase`(版本检测 + 迁移/重建,与 open 路由同管道),
|
|
56
|
+
* 返回项目上下文(打开语义,部署场景「启动即用」);
|
|
37
57
|
* project.json 不存在 → **返回 null(待命)**——不初始化、不建任何文件(含目录),
|
|
38
58
|
* 由前端 Dashboard 引导走 POST /project/create 或 /project/open(S1.4 开/建页;
|
|
39
59
|
* 此前无条件初始化导致引导永不显示、dev 态污染 packages/server 包目录)。
|
|
40
60
|
* project.json 损坏(JSON 解析失败)→ 抛错(readProjectFile 语义:不静默重建,防数据误伤)。
|
|
41
|
-
*
|
|
42
|
-
*
|
|
61
|
+
*
|
|
62
|
+
* **版本对齐(卡 2.8)**:旧库有迁移路径 → 前向迁移(含迁移前快照);无路径 → 删库重建兜底;
|
|
63
|
+
* **未来版本库**(`SchemaVersionError`)→ 拒绝打开、**零写操作**,本函数记日志并回待命
|
|
64
|
+
*(用户点开这本书时 open 路由仍返回 409 `PROJECT_VERSION_NEWER`);打开/迁移的其他错误
|
|
65
|
+
* 同理回待命并记日志(下次 open/重启重试)——与 `detectLastProject` 的「不阻断启动」
|
|
66
|
+
* 语义一致,数据原封不动。
|
|
43
67
|
*/
|
|
44
68
|
export declare function detectProject(root: string): ProjectContext | null;
|
|
45
69
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../src/middleware/project.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../src/middleware/project.ts"],"names":[],"mappings":"AAeA,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;AACnG,OAAO,EAA8C,KAAK,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAW/G,yBAAyB;AACzB,eAAO,MAAM,iBAAiB,YAAY,CAAC;AAK3C,4CAA4C;AAC5C,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;AA4BD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,GAAG,IAAI,CActE;AAED,uBAAuB;AACvB,wBAAgB,iBAAiB,IAAI,cAAc,GAAG,IAAI,CAEzD;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI,cAAc,CAKtD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,EAAE,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,CAWpF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAiBjE;AAWD;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,cAAc,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,iBAAiB,EAAE,MAAM,GAAG,UAAU,CAAC,CAAC,GACrE,cAAc,CAoBhB;AAED,iDAAiD;AACjD,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"}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// 职责(第 121-122 行 middleware/project.ts「项目路径注入」):
|
|
4
4
|
// 1. detectProject:检测 project.json——存在则打开(部署场景「启动即用」);不存在返回
|
|
5
5
|
// null 待命(不初始化、不建文件,由前端 Dashboard 引导 create/open;S1.4 开/建页依赖)
|
|
6
|
+
// openProjectDatabase:data.db 打开 + schema 版本对齐的**唯一管道**(开机路径与
|
|
7
|
+
// POST /project/open 共用,卡 2.8——开机直达不再跳过迁移)
|
|
6
8
|
// initProject:显式初始化三文件(S1.2 create 路由专用,含建目录 + user_version)
|
|
7
9
|
// 2. currentProject 内存单例(S1.2):create/open 切换、close 清空,
|
|
8
10
|
// 模块级可变状态由路由(routes/project.ts)读写,projectMiddleware 从状态注入 Hono 上下文
|
|
@@ -13,12 +15,15 @@ import { basename, join } from "node:path";
|
|
|
13
15
|
import { mkdirSync } from "node:fs";
|
|
14
16
|
import { generateProjectId, DEFAULT_BACKUP_FREQUENCY_MINUTES } from "@whispering233/ai-editor-shared";
|
|
15
17
|
import { closeDatabase, openDatabase, setUserVersion } from "@whispering233/ai-editor-db";
|
|
18
|
+
import { ensureSchemaCompatible, SchemaVersionError } from "@whispering233/ai-editor-db";
|
|
16
19
|
import { readProjectFile, writeProjectFile } from "@whispering233/ai-editor-db";
|
|
17
20
|
import { writeOutlineFile } from "@whispering233/ai-editor-db";
|
|
18
21
|
import { SCHEMA_VERSION } from "@whispering233/ai-editor-db";
|
|
19
22
|
import { nowIso } from "@whispering233/ai-editor-db";
|
|
20
23
|
import { HttpError, fail } from "./error.js";
|
|
21
|
-
import { migratePromptToAgents, startAutoBackup, stopAutoBackup } from "../backup.js";
|
|
24
|
+
import { ensureParseableBackup, migratePromptToAgents, setProjectTick, startAutoBackup, stopAutoBackup } from "../backup.js";
|
|
25
|
+
import { AUTO_PUSH_THROTTLE_MS, maybeAutoPush } from "../cloud/auto-push.js";
|
|
26
|
+
import { readAutoPush } from "../cloud/state.js";
|
|
22
27
|
import { disposeProjectRuntime } from "../chat-runtime.js";
|
|
23
28
|
/** data.db 文件名(项目根目录) */
|
|
24
29
|
export const DATA_DB_FILE_NAME = "data.db";
|
|
@@ -35,6 +40,22 @@ export function getProject(c) {
|
|
|
35
40
|
// 模块级可变状态 + 显式读写函数:路由层可读可写,中间件只读注入。
|
|
36
41
|
/** 当前打开的项目(null = 无) */
|
|
37
42
|
let currentProject = null;
|
|
43
|
+
/**
|
|
44
|
+
* 定时器钩子注册(composition 层,卡 7)——**模块加载时注册一次**:自动推送**不新起定时器**,
|
|
45
|
+
* 挂在自动备份的 tick 链上(`backup.ts` 的 `setProjectTick`)。排程条件 = 备份频率开启**或**
|
|
46
|
+
* `autoPush` 开启:备份频率关闭时按 `AUTO_PUSH_THROTTLE_MS`(2h)兜底排程——否则设置页里开着的
|
|
47
|
+
* 「自动推送」会随「关掉自动备份」静默失效(两者动机不同,见 `docs/design/40-cloud-sync.md` §5)。
|
|
48
|
+
*
|
|
49
|
+
* 钩子内的读写(`readWebdavConfig` / `readAutoPush`)依赖 `initCloudState(root)` 已执行;
|
|
50
|
+
* 未注入(测试/降级)→ 读到「未配置」→ 不排程,无副作用。`startServer` 的顺序(`initCloudState`
|
|
51
|
+
* 先于第一次 open)已满足。
|
|
52
|
+
*/
|
|
53
|
+
setProjectTick({
|
|
54
|
+
onTick: (project) => void maybeAutoPush(project), // 内部自记失败状态,永不 reject
|
|
55
|
+
// 兜底只看 autoPush 开关(**不看「此刻是否已配置」**):否则「先开项目、后配云盘」的会话里
|
|
56
|
+
// 永远排不上 tick;未配置时 tick 落到 `maybeAutoPush` 会立即返回,零副作用
|
|
57
|
+
fallbackIntervalMs: () => (readAutoPush() ? AUTO_PUSH_THROTTLE_MS : null),
|
|
58
|
+
});
|
|
38
59
|
/**
|
|
39
60
|
* 设置当前项目(create/open 成功后调用;传 null 清空——close 时)。
|
|
40
61
|
* B2.2:自动备份定时器跟随当前项目生命周期——设置非空 → 启动调度
|
|
@@ -47,6 +68,10 @@ export function setCurrentProject(project) {
|
|
|
47
68
|
disposeProjectRuntime();
|
|
48
69
|
currentProject = project;
|
|
49
70
|
if (project !== null) {
|
|
71
|
+
// 升级兜底(卡 A):旧命名份不再被解析,若磁盘上只剩旧命名且**自动备份频率关闭**,
|
|
72
|
+
// 用户点「立即备份」前列表会是空的——先补一份新格式档(best-effort,函数内部自吞异常记日志,
|
|
73
|
+
// 不阻塞打开;已有可解析份则不做任何事)
|
|
74
|
+
ensureParseableBackup(project);
|
|
50
75
|
startAutoBackup(project);
|
|
51
76
|
}
|
|
52
77
|
else {
|
|
@@ -67,27 +92,75 @@ export function requireCurrentProject() {
|
|
|
67
92
|
}
|
|
68
93
|
return currentProject;
|
|
69
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* data.db 打开 + schema 版本对齐的**唯一管道**(卡 2.8 收敛):
|
|
97
|
+
* 开机路径(detectProject)与显式 `POST /project/open` 共用,避免两条路径语义漂移。
|
|
98
|
+
*
|
|
99
|
+
* 语义(`docs/db/schema.md`「schema 版本与迁移」三态分流):
|
|
100
|
+
* - `user_version === SCHEMA_VERSION` → 原连接返回(rebuilt=false);
|
|
101
|
+
* - 旧版本**有迁移路径** → 前向迁移(迁移前自动快照 `data.db.v{n}.{时间戳}.bak`);
|
|
102
|
+
* - 旧版本**无迁移路径** → 删库重建兜底(备份 `data.db.v{n}.bak` + outline 重置);
|
|
103
|
+
* - **未来版本** → 关闭连接并抛 `SchemaVersionError`(调用方决定策略:
|
|
104
|
+
* 路由映射 409 `PROJECT_VERSION_NEWER`;开机态回待命)。
|
|
105
|
+
*
|
|
106
|
+
* @returns 已对齐版本的活动连接 + 迁移/重建结果(路由用于响应提示)
|
|
107
|
+
* @throws SchemaVersionError 未来版本拒绝打开;迁移/重建过程的 I/O 错误
|
|
108
|
+
*/
|
|
109
|
+
export function openProjectDatabase(dir) {
|
|
110
|
+
const dbPath = join(dir, DATA_DB_FILE_NAME);
|
|
111
|
+
const db = openDatabase(dbPath);
|
|
112
|
+
try {
|
|
113
|
+
const out = ensureSchemaCompatible(db, dir, dbPath);
|
|
114
|
+
return { db: out.db, result: out.result };
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
// 幂等:拒绝/迁移失败分支内已关连接(db 包保证无句柄泄漏),此处仅防御
|
|
118
|
+
closeDatabase(db);
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
70
122
|
/**
|
|
71
123
|
* 检测并打开项目(启动流程 ④ 修订——设计缺陷修复):
|
|
72
|
-
* project.json 存在 →
|
|
124
|
+
* project.json 存在 → 走 `openProjectDatabase`(版本检测 + 迁移/重建,与 open 路由同管道),
|
|
125
|
+
* 返回项目上下文(打开语义,部署场景「启动即用」);
|
|
73
126
|
* project.json 不存在 → **返回 null(待命)**——不初始化、不建任何文件(含目录),
|
|
74
127
|
* 由前端 Dashboard 引导走 POST /project/create 或 /project/open(S1.4 开/建页;
|
|
75
128
|
* 此前无条件初始化导致引导永不显示、dev 态污染 packages/server 包目录)。
|
|
76
129
|
* project.json 损坏(JSON 解析失败)→ 抛错(readProjectFile 语义:不静默重建,防数据误伤)。
|
|
77
|
-
*
|
|
78
|
-
*
|
|
130
|
+
*
|
|
131
|
+
* **版本对齐(卡 2.8)**:旧库有迁移路径 → 前向迁移(含迁移前快照);无路径 → 删库重建兜底;
|
|
132
|
+
* **未来版本库**(`SchemaVersionError`)→ 拒绝打开、**零写操作**,本函数记日志并回待命
|
|
133
|
+
*(用户点开这本书时 open 路由仍返回 409 `PROJECT_VERSION_NEWER`);打开/迁移的其他错误
|
|
134
|
+
* 同理回待命并记日志(下次 open/重启重试)——与 `detectLastProject` 的「不阻断启动」
|
|
135
|
+
* 语义一致,数据原封不动。
|
|
79
136
|
*/
|
|
80
137
|
export function detectProject(root) {
|
|
81
138
|
const existing = readProjectFile(root);
|
|
82
139
|
if (existing === null) {
|
|
83
140
|
return null;
|
|
84
141
|
}
|
|
85
|
-
|
|
86
|
-
|
|
142
|
+
let db;
|
|
143
|
+
try {
|
|
144
|
+
({ db } = openProjectDatabase(root));
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
logStartupOpenFailure(root, err);
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const project = { root, config: existing, db };
|
|
151
|
+
// 自动迁移:project.json 有非空 prompt 且无 AGENTS.md → 迁移写入(原样,一次性)
|
|
87
152
|
//(实现位于 backup.ts——backup 模块不依赖 middleware,middleware 侧经 ../backup.js 复用)
|
|
88
153
|
migratePromptToAgents(project);
|
|
89
154
|
return project;
|
|
90
155
|
}
|
|
156
|
+
/** 启动路径打开失败的日志(不阻断启动;未来版本另给明确文案) */
|
|
157
|
+
function logStartupOpenFailure(root, err) {
|
|
158
|
+
if (err instanceof SchemaVersionError) {
|
|
159
|
+
console.error(`[server] 启动自动打开被拒绝(data.db 版本高于程序版本,已回待命): ${root} — ${err.message}`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
console.error(`[server] 启动自动打开失败(已回待命,下次 open/重启重试): ${root}`, err);
|
|
163
|
+
}
|
|
91
164
|
/**
|
|
92
165
|
* 显式初始化新项目(S1.2 create 路由专用,「首次初始化三文件」语义):
|
|
93
166
|
* 建目录(mkdir recursive,含嵌套不存在的父目录)→ 写 project.json(id=proj- 前缀 nanoid、
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project.js","sourceRoot":"","sources":["../../src/middleware/project.ts"],"names":[],"mappings":"AAAA,wBAAwB;AACxB,EAAE;AACF,iDAAiD;AACjD,4DAA4D;AAC5D,8DAA8D;AAC9D,6DAA6D;AAC7D,wDAAwD;AACxD,mEAAmE;AACnE,2CAA2C;AAC3C,oDAAoD;AACpD,sDAAsD;AACtD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAGpC,OAAO,EAAE,iBAAiB,EAAE,gCAAgC,EAAE,MAAM,iCAAiC,CAAC;AACtG,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,cAAc,EAAW,MAAM,6BAA6B,CAAC;AACnG,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAChF,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,MAAM,EAAE,MAAM,6BAA6B,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,IAAI,EAAqB,MAAM,YAAY,CAAC;AAChE,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"project.js","sourceRoot":"","sources":["../../src/middleware/project.ts"],"names":[],"mappings":"AAAA,wBAAwB;AACxB,EAAE;AACF,iDAAiD;AACjD,4DAA4D;AAC5D,8DAA8D;AAC9D,8DAA8D;AAC9D,2CAA2C;AAC3C,6DAA6D;AAC7D,wDAAwD;AACxD,mEAAmE;AACnE,2CAA2C;AAC3C,oDAAoD;AACpD,sDAAsD;AACtD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAGpC,OAAO,EAAE,iBAAiB,EAAE,gCAAgC,EAAE,MAAM,iCAAiC,CAAC;AACtG,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,cAAc,EAAW,MAAM,6BAA6B,CAAC;AACnG,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAAwB,MAAM,6BAA6B,CAAC;AAC/G,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAChF,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,MAAM,EAAE,MAAM,6BAA6B,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,IAAI,EAAqB,MAAM,YAAY,CAAC;AAChE,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC7H,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC7E,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,yBAAyB;AACzB,MAAM,CAAC,MAAM,iBAAiB,GAAG,SAAS,CAAC;AAE3C,0CAA0C;AAC1C,MAAM,aAAa,GAAG,CAAC,WAAW,EAAE,WAAW,EAAE,KAAK,CAAC,CAAC;AAcxD,8BAA8B;AAC9B,MAAM,UAAU,UAAU,CAAC,CAA2C;IACpE,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;AAC1B,CAAC;AAED,sDAAsD;AACtD,EAAE;AACF,4CAA4C;AAC5C,uDAAuD;AACvD,oCAAoC;AAEpC,wBAAwB;AACxB,IAAI,cAAc,GAA0B,IAAI,CAAC;AAEjD;;;;;;;;;GASG;AACH,cAAc,CAAC;IACb,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,aAAa,CAAC,OAAO,CAAC,EAAE,qBAAqB;IACvE,sDAAsD;IACtD,qDAAqD;IACrD,kBAAkB,EAAE,GAAG,EAAE,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,IAAI,CAAC;CAC1E,CAAC,CAAC;AAEH;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAA8B;IAC9D,6CAA6C;IAC7C,2DAA2D;IAC3D,qBAAqB,EAAE,CAAC;IACxB,cAAc,GAAG,OAAO,CAAC;IACzB,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACxB,8CAA8C;QAC9C,sDAAsD;QACtD,sBAAsB;QACnB,qBAAqB,CAAC,OAAO,CAAC,CAAC;QAC/B,eAAe,CAAC,OAAO,CAAC,CAAC;IAC3B,CAAC;SAAM,CAAC;QACN,cAAc,EAAE,CAAC;IACnB,CAAC;AACH,CAAC;AAED,uBAAuB;AACvB,MAAM,UAAU,iBAAiB;IAC/B,OAAO,cAAc,CAAC;AACxB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB;IACnC,IAAI,cAAc,KAAK,IAAI,EAAE,CAAC;QAC5B,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,iBAAiC,EAAE,wCAAwC,CAAC,CAAC;IACxG,CAAC;IACD,OAAO,cAAc,CAAC;AACxB,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC7C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;IAC5C,MAAM,EAAE,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IAChC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,sBAAsB,CAAC,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;QACpD,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC;IAC5C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QAChB,uCAAuC;QACpC,aAAa,CAAC,EAAE,CAAC,CAAC;QAClB,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;IACvC,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,EAAM,CAAC;IACX,IAAI,CAAC;QACH,CAAC,EAAE,EAAE,EAAE,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC,CAAC;IACvC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,qBAAqB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QACjC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,OAAO,GAAmB,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IAChE,2DAA2D;IAC3D,yEAAyE;IACxE,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAC/B,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,oCAAoC;AACpC,SAAS,qBAAqB,CAAC,IAAY,EAAE,GAAY;IACvD,IAAI,GAAG,YAAY,kBAAkB,EAAE,CAAC;QACtC,OAAO,CAAC,KAAK,CAAC,8CAA8C,IAAI,MAAM,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QACrF,OAAO;IACT,CAAC;IACD,OAAO,CAAC,KAAK,CAAC,yCAAyC,IAAI,EAAE,EAAE,GAAG,CAAC,CAAC;AACtE,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,WAAW,CACzB,IAAY,EACZ,cAAsE;IAEtE,SAAS,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACrC,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC;IACrB,MAAM,MAAM,GAAsB;QAChC,EAAE,EAAE,iBAAiB,EAAE,EAAE,kBAAkB;QAC3C,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC;QACpB,QAAQ,EAAE,IAAI;QACd,cAAc,EAAE,cAAc;QAC9B,gBAAgB,EAAE,IAAI;QACzB,8BAA8B;QAC3B,wBAAwB,EAAE,gCAAgC;QAC1D,UAAU,EAAE,GAAG;QACf,UAAU,EAAE,GAAG;QACf,GAAG,cAAc,EAAE,6BAA6B;KACjD,CAAC;IACF,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC/B,gBAAgB,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,cAAc,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;IACnG,MAAM,EAAE,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC,kBAAkB;IAC1E,cAAc,CAAC,EAAE,EAAE,cAAc,CAAC,CAAC,CAAC,kDAAkD;IACtF,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;AAC9B,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,YAAY,CAAC,OAAuB;IAClD,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;AAC5B,CAAC;AAED,kDAAkD;AAClD,SAAS,eAAe,CAAC,KAAa;IACpC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,KAAK,EAAE,CAAC,CAAC;QACvE,OAAO,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB;IACnC,OAAO,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACvB,MAAM,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACtC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACnD,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACpD,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,yCAAyC,CAAC,EAAE,GAAG,CAAC,CAAC;QACnF,CAAC;QACD,MAAM,IAAI,EAAE,CAAC;IACf,CAAC,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB;IAC/B,OAAO,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACvB,MAAM,OAAO,GAAG,iBAAiB,EAAE,CAAC;QACpC,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACrB,CAAC,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QAC5B,CAAC;QACD,MAAM,IAAI,EAAE,CAAC;IACf,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
/** 云端存档路由(挂载于 /api/v1/cloud) */
|
|
3
|
+
export declare const cloudRoutes: Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
|
|
4
|
+
/** 读写探测用的临时文件名(沿用 `.tmp-` 约定:推送前清理流程会顺带回收遗留文件) */
|
|
5
|
+
export declare const WEBDAV_WRITE_TEST_FILE = ".tmp-ai-editor-writetest";
|
|
6
|
+
/** 探针用的工作子目录:**坚果云等云盘不允许在根目录直接建文件**(PUT 根 → 404 ObjectNotFound),
|
|
7
|
+
* 但允许建子目录 ⇒ 探针写进它里面;建不了子目录(不支持/权限不足)时退回根目录写法 */
|
|
8
|
+
export declare const WEBDAV_WRITE_TEST_DIR = ".tmp-ai-editor-writetest-dir";
|
|
9
|
+
//# sourceMappingURL=cloud.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../../src/routes/cloud.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAY5B,gCAAgC;AAChC,eAAO,MAAM,WAAW,4EAAa,CAAC;AAEtC,kDAAkD;AAClD,eAAO,MAAM,sBAAsB,6BAA6B,CAAC;AACjE;gDACgD;AAChD,eAAO,MAAM,qBAAqB,iCAAiC,CAAC"}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// 云端存档路由:GET /status、PUT /config、POST /test(挂载于 /api/v1/cloud)
|
|
2
|
+
//
|
|
3
|
+
// 契约 = `docs/api/100-api-cloud.md`;语义与不变式 = `docs/design/40-cloud-sync.md`。
|
|
4
|
+
// 卡 2 只做**配置段**:账号读写 + 连通性/读写权限测试;云端的列表、推送、拉取在后续卡片,
|
|
5
|
+
// 那时 status 会补上 remote/local/state(现在不填,避免「字段存在但永远为 null」的假契约)。
|
|
6
|
+
//
|
|
7
|
+
// 三个端点均**不要求**项目已打开(设置页需先能配账号;projectId 仅作回显)。
|
|
8
|
+
// 凭据纪律:password 永不进响应——不是脱敏展示,而是根本不回传。
|
|
9
|
+
import { Hono } from "hono";
|
|
10
|
+
import { cloudConfigPutReqSchema, cloudPullReqSchema, cloudPushReqSchema } from "@whispering233/ai-editor-shared/schemas";
|
|
11
|
+
import { sanitizeDeviceName } from "@whispering233/ai-editor-shared";
|
|
12
|
+
import { HttpError, ok } from "../middleware/error.js";
|
|
13
|
+
import { getCurrentProject, requireCurrentProject } from "../middleware/project.js";
|
|
14
|
+
import { currentDeviceName } from "../cloud/device.js";
|
|
15
|
+
import { configuredDeviceName, readAutoPush, readBookState, readWebdavConfig, writeCloudConfig } from "../cloud/state.js";
|
|
16
|
+
import { startAutoBackup } from "../backup.js";
|
|
17
|
+
import { cloudFileSet, computeCloudSync, findExistingCloudDir, pullBackup, pushBackup, toCloudBackups } from "../cloud/sync.js";
|
|
18
|
+
import { createWebdavClient } from "../cloud/webdav.js";
|
|
19
|
+
/** 云端存档路由(挂载于 /api/v1/cloud) */
|
|
20
|
+
export const cloudRoutes = new Hono();
|
|
21
|
+
/** 读写探测用的临时文件名(沿用 `.tmp-` 约定:推送前清理流程会顺带回收遗留文件) */
|
|
22
|
+
export const WEBDAV_WRITE_TEST_FILE = ".tmp-ai-editor-writetest";
|
|
23
|
+
/** 探针用的工作子目录:**坚果云等云盘不允许在根目录直接建文件**(PUT 根 → 404 ObjectNotFound),
|
|
24
|
+
* 但允许建子目录 ⇒ 探针写进它里面;建不了子目录(不支持/权限不足)时退回根目录写法 */
|
|
25
|
+
export const WEBDAV_WRITE_TEST_DIR = ".tmp-ai-editor-writetest-dir";
|
|
26
|
+
/**
|
|
27
|
+
* 归一化 WebDAV 根 URL:必须是 `http(s)` 绝对地址;去尾斜杠(路径拼接由客户端逐段编码)。
|
|
28
|
+
* 非法 → 400 VALIDATION_ERROR(文案面向用户,指出期望形态与示例)。
|
|
29
|
+
*/
|
|
30
|
+
function normalizeWebdavUrl(raw) {
|
|
31
|
+
let parsed;
|
|
32
|
+
try {
|
|
33
|
+
parsed = new URL(raw);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new HttpError(400, "VALIDATION_ERROR",
|
|
37
|
+
// 不回显原始输入(用户可能把 ftp://用户名:密码@host 这类串粘进来;卡 D)
|
|
38
|
+
"WebDAV 地址不是合法 URL(示例:https://dav.jianguoyun.com/dav/ai-editor)");
|
|
39
|
+
}
|
|
40
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
41
|
+
// 只回显协议段(`parsed.protocol`),不回显原始输入(同上;卡 D)
|
|
42
|
+
throw new HttpError(400, "VALIDATION_ERROR", `WebDAV 地址只接受 http/https(当前:${parsed.protocol})`);
|
|
43
|
+
}
|
|
44
|
+
// 拒绝 URL 内嵌凭据(oracle 卡 2 验证 F1):undici 拒绍带 credentials 的 URL(保存后永远不通),
|
|
45
|
+
// 且该串会被原样回显进响应(凭据泄露)。**不静默剥离**——静默会让用户以为填对了。
|
|
46
|
+
if (parsed.username !== "" || parsed.password !== "") {
|
|
47
|
+
throw new HttpError(400, "VALIDATION_ERROR", "WebDAV 地址不得内嵌用户名/密码(如 https://user:pw@host/dav)——请分别填到用户名与应用密码字段");
|
|
48
|
+
}
|
|
49
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
50
|
+
}
|
|
51
|
+
// GET /api/v1/cloud/status —— 配置段 + 云端段(remote:目录与备份列表)+ 本机段与三态(local/state)
|
|
52
|
+
// 已配置且打开了项目时发起 PROPFIND(缓存命中 2 次、需回退扫描时 3 次:书目录 + 目录列举 [+ 云根]);
|
|
53
|
+
// 失败不影响本端点成功返回(remote=null + errorCode,state="unreachable")
|
|
54
|
+
cloudRoutes.get("/status", async (c) => {
|
|
55
|
+
const webdav = readWebdavConfig();
|
|
56
|
+
const project = getCurrentProject();
|
|
57
|
+
let remote = null;
|
|
58
|
+
let rawEntries = [];
|
|
59
|
+
let errorCode;
|
|
60
|
+
if (webdav !== null && project !== null) {
|
|
61
|
+
try {
|
|
62
|
+
const client = createWebdavClient(webdav);
|
|
63
|
+
const state = readBookState(project.config.id);
|
|
64
|
+
const dirName = await findExistingCloudDir(client, project, state?.dirName);
|
|
65
|
+
rawEntries = dirName === null ? [] : ((await client.list(dirName)) ?? []);
|
|
66
|
+
remote = { dirName, backups: toCloudBackups(rawEntries) };
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
// 云端检查失败:只反映在状态里(remote=null + errorCode),不阻塞本地功能
|
|
70
|
+
errorCode = err instanceof HttpError ? err.code : "CLOUD_UNREACHABLE";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const { local, state } = computeCloudSync(project, webdav !== null, remote === null ? null : { files: cloudFileSet(rawEntries) });
|
|
74
|
+
const payload = {
|
|
75
|
+
configured: webdav !== null,
|
|
76
|
+
url: webdav?.url ?? null,
|
|
77
|
+
username: webdav?.username ?? null,
|
|
78
|
+
device: currentDeviceName(),
|
|
79
|
+
// 「用户显式设过」与「生效值」分开(卡 (a)):面板只预填设过的值,避免保存时把派生值钉进配置
|
|
80
|
+
deviceConfigured: configuredDeviceName() !== null,
|
|
81
|
+
autoPush: readAutoPush(),
|
|
82
|
+
projectId: project?.config.id ?? null,
|
|
83
|
+
remote,
|
|
84
|
+
local,
|
|
85
|
+
state,
|
|
86
|
+
...(errorCode !== undefined ? { errorCode } : {}),
|
|
87
|
+
};
|
|
88
|
+
return c.json(ok(payload));
|
|
89
|
+
});
|
|
90
|
+
// POST /api/v1/cloud/pull —— 从云端拉取一份备份应用到当前项目(缺省 = 云端 head)
|
|
91
|
+
// 语义:三文件覆盖 + references/ 与 sessions/ **并集合并**(基线三方比较、删除优先);
|
|
92
|
+
// 覆盖前自动快照本机当前状态(restore 管道既有行为)
|
|
93
|
+
cloudRoutes.post("/pull", async (c) => {
|
|
94
|
+
const project = requireCurrentProject(); // 409 NO_PROJECT_OPEN
|
|
95
|
+
const body = await c.req.json().catch(() => ({}));
|
|
96
|
+
const parsed = cloudPullReqSchema.parse(body);
|
|
97
|
+
const result = await pullBackup(project, {
|
|
98
|
+
...(parsed.file_name !== undefined ? { fileName: parsed.file_name } : {}),
|
|
99
|
+
});
|
|
100
|
+
return c.json(ok(result));
|
|
101
|
+
});
|
|
102
|
+
// PUT /api/v1/cloud/config —— 写入账号配置/设备名/自动推送开关(合并写;响应不含凭据)
|
|
103
|
+
cloudRoutes.put("/config", async (c) => {
|
|
104
|
+
const parsed = cloudConfigPutReqSchema.parse(await c.req.json());
|
|
105
|
+
// url:空串 = 清空;非空 → 归一化(校验失败 400)
|
|
106
|
+
const url = parsed.url === undefined ? undefined : parsed.url.trim() === "" ? null : normalizeWebdavUrl(parsed.url.trim());
|
|
107
|
+
// username:空串 = 清空
|
|
108
|
+
const username = parsed.username === undefined ? undefined : parsed.username.trim();
|
|
109
|
+
// password:**缺省或空串 = 保留原值**(响应从不回传 → 表单留空即不改)
|
|
110
|
+
const password = parsed.password === undefined || parsed.password === "" ? undefined : parsed.password;
|
|
111
|
+
// device:空串 = 回到缺省;非空 → sanitize(非法 400)
|
|
112
|
+
let device;
|
|
113
|
+
if (parsed.device !== undefined) {
|
|
114
|
+
if (parsed.device.trim() === "") {
|
|
115
|
+
device = null;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
const sanitized = sanitizeDeviceName(parsed.device);
|
|
119
|
+
if (sanitized === null) {
|
|
120
|
+
throw new HttpError(400, "VALIDATION_ERROR", "设备名非法(trim 后 1-16 字符,禁连字符 `-` 与路径分隔符/保留字符)");
|
|
121
|
+
}
|
|
122
|
+
device = sanitized;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
writeCloudConfig({
|
|
126
|
+
...(url !== undefined ? { url } : {}),
|
|
127
|
+
...(username !== undefined ? { username } : {}),
|
|
128
|
+
...(password !== undefined ? { password } : {}),
|
|
129
|
+
...(device !== undefined ? { device } : {}),
|
|
130
|
+
...(parsed.autoPush !== undefined ? { autoPush: parsed.autoPush } : {}),
|
|
131
|
+
});
|
|
132
|
+
// 自动推送的排程条件依赖自动备份频率:本机开关可能在本次保存中改变——立即重排一次 tick
|
|
133
|
+
//(幂等;只在「备份频率关闭 + autoPush」时才真的排上 2h 兜底)——否则要等下次 open/restore
|
|
134
|
+
const project = getCurrentProject();
|
|
135
|
+
if (project !== null)
|
|
136
|
+
startAutoBackup(project);
|
|
137
|
+
const payload = { saved: true };
|
|
138
|
+
return c.json(ok(payload));
|
|
139
|
+
});
|
|
140
|
+
// POST /api/v1/cloud/push —— 推送一份本地备份到云端(缺省取最新一份;force = 冲突时用本机覆盖云端)
|
|
141
|
+
cloudRoutes.post("/push", async (c) => {
|
|
142
|
+
const project = requireCurrentProject(); // 409 NO_PROJECT_OPEN
|
|
143
|
+
const body = await c.req.json().catch(() => ({}));
|
|
144
|
+
const parsed = cloudPushReqSchema.parse(body);
|
|
145
|
+
const result = await pushBackup(project, {
|
|
146
|
+
...(parsed.file_name !== undefined ? { fileName: parsed.file_name } : {}),
|
|
147
|
+
...(parsed.force !== undefined ? { force: parsed.force } : {}),
|
|
148
|
+
});
|
|
149
|
+
return c.json(ok(result));
|
|
150
|
+
});
|
|
151
|
+
// POST /api/v1/cloud/test —— 连通性 + 读/写权限探测(认证通过但无写权限是最常见的误配)
|
|
152
|
+
cloudRoutes.post("/test", async (c) => {
|
|
153
|
+
const webdav = readWebdavConfig();
|
|
154
|
+
if (webdav === null) {
|
|
155
|
+
throw new HttpError(409, "CLOUD_NOT_CONFIGURED", "云盘未配置:请先填写 WebDAV 地址与用户名/应用密码");
|
|
156
|
+
}
|
|
157
|
+
const client = createWebdavClient(webdav);
|
|
158
|
+
// 1) 根目录可达性:列不出来(404)→ 幂等创建 → **复核**(卡:MKCOL 可能被服务器以
|
|
159
|
+
// 「已存在」等状态敷衍而不真正创建;不复核的话错误会错位到后面的 PUT 上,报 404 让人以为
|
|
160
|
+
// 是写权限问题)。复核仍不存在 → 报「目录不存在且创建失败」并给出地址与建议。
|
|
161
|
+
const entries = await client.list("");
|
|
162
|
+
let created = false;
|
|
163
|
+
if (entries === null) {
|
|
164
|
+
created = await client.ensureWorkingRoot(); // 云盘根 + 工作根都幂等创建(地址还不存在也能自愈)
|
|
165
|
+
if ((await client.list("")) === null) {
|
|
166
|
+
throw new HttpError(502, "CLOUD_UNREACHABLE", `云盘目录不存在且创建失败:${webdav.url}(请确认地址指向你自己的 WebDAV 根目录,坚果云为 https://dav.jianguoyun.com/dav;若地址多带了一层不存在的子目录,先删掉它再测)`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// 2) 写权限探测:**优先写进工作子目录**(坚果云等云盘根目录不可写文件:PUT `/dav/x` → 404
|
|
170
|
+
// ObjectNotFound,而建子目录是允许的);子目录建不了(不支持/权限不足)→ 退回根目录写法。
|
|
171
|
+
// **可写不可删的云盘(DELETE 403/405…)不降级为认证失败**——读 + 写都通过了,凭据有效,只是清理失败;
|
|
172
|
+
// 报 502 AUTH_FAILED 会误导用户去查凭据。这里改为成功 + 提示残留(具体状态码进日志)。
|
|
173
|
+
let probeDir = null;
|
|
174
|
+
try {
|
|
175
|
+
// 先清历史残留(上一次因删除失败留下的空目录)——顺带验证删除能力
|
|
176
|
+
await client.removeDir(WEBDAV_WRITE_TEST_DIR).catch(() => undefined);
|
|
177
|
+
await client.mkcol(WEBDAV_WRITE_TEST_DIR); // 幂等:已存在返回 false,同样可用
|
|
178
|
+
probeDir = WEBDAV_WRITE_TEST_DIR;
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
console.error("[cloud] 工作子目录创建失败,探针退回根目录写法:", err);
|
|
182
|
+
}
|
|
183
|
+
const probePath = probeDir === null ? WEBDAV_WRITE_TEST_FILE : `${probeDir}/${WEBDAV_WRITE_TEST_FILE}`;
|
|
184
|
+
await client.put(probePath, new TextEncoder().encode("ai-editor webdav write test"));
|
|
185
|
+
let leftover = false;
|
|
186
|
+
try {
|
|
187
|
+
await client.remove(probePath);
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
leftover = true;
|
|
191
|
+
console.error("[cloud] 测试文件删除失败(云盘可能不允许删除;凭据本身正常):", err);
|
|
192
|
+
}
|
|
193
|
+
if (probeDir !== null && !leftover) {
|
|
194
|
+
// 子目录一并清掉(用 removeDir:集合删除需要尾斜杠 + Depth: infinity,否则某些云盘不删、留空目录)
|
|
195
|
+
try {
|
|
196
|
+
await client.removeDir(probeDir);
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
console.error("[cloud] 测试目录删除失败(空目录残留,下次「测试连接」会再清一次):", err);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// baseUrl 回显**用户的云盘根**(配置值;工作根 `<根>/ai-editor` 由客户端层拼,不进配置语义)
|
|
203
|
+
const payload = { connected: true, baseUrl: webdav.url, created, ...(leftover ? { leftoverWriteTestFile: true } : {}) };
|
|
204
|
+
return c.json(ok(payload));
|
|
205
|
+
});
|
|
206
|
+
//# sourceMappingURL=cloud.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloud.js","sourceRoot":"","sources":["../../src/routes/cloud.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"delta.d.ts","sourceRoot":"","sources":["../../src/routes/delta.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"delta.d.ts","sourceRoot":"","sources":["../../src/routes/delta.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAQ5B,2CAA2C;AAC3C,eAAO,MAAM,WAAW,4EAAa,CAAC"}
|
package/dist/routes/delta.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
// 触发节点不存在或已软删 → 404 OUTLINE_NODE_NOT_FOUND(POST /delta 与 /delta/compute 路由层前置校验;
|
|
12
12
|
// POST /delta 虽未定义该错误码,但 db 层 insertDelta 不校验节点(delta.ts 注释:缺失节点记录
|
|
13
13
|
// 因可见性规则永久不可见),路由层拦截防死记录——oracle 建议)
|
|
14
|
+
// POST /delta 触发节点非章 → 400 VALIDATION_ERROR(锚点仅章,卷/场景拒绝;
|
|
15
|
+
// /delta/compute 的 at_node_id 不限层级——收窄仅写入侧)
|
|
14
16
|
// 目标实体不存在或已软删 → 404 ENTITY_NOT_FOUND(POST /delta/compute)
|
|
15
17
|
import { Hono } from "hono";
|
|
16
18
|
import { computeState, findOutlineNode, getEntity, insertDelta, listDeltasByNode, readOutlineFile } from "@whispering233/ai-editor-db";
|
|
@@ -26,6 +28,7 @@ export const deltaRoutes = new Hono();
|
|
|
26
28
|
* 缺失节点记录会因可见性规则(触发节点缺失视同不可见)永久不可见,
|
|
27
29
|
* 故路由层拦截并映射 404 OUTLINE_NODE_NOT_FOUND;computeState 的 getOutlinePathIds
|
|
28
30
|
* 对缺失节点抛错(视为调用方 bug),同样由本校验先行兜底。
|
|
31
|
+
* **返回节点**:存在性与层级是两档校验,调用方(POST / 仅章)需拿到节点继续收紧。
|
|
29
32
|
*/
|
|
30
33
|
function assertOutlineNode(project, nodeId) {
|
|
31
34
|
const tree = readOutlineFile(project.root);
|
|
@@ -33,6 +36,20 @@ function assertOutlineNode(project, nodeId) {
|
|
|
33
36
|
if (node === undefined || node.deleted === true) {
|
|
34
37
|
throw new HttpError(404, "OUTLINE_NODE_NOT_FOUND", `大纲节点不存在: ${nodeId}`);
|
|
35
38
|
}
|
|
39
|
+
return node;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 变更记录锚点层级校验(卡片 1.2 收紧):触发节点只能是**章**——卷太粗、场景太碎,
|
|
43
|
+
* 一章一个状态变化点才是叙事粒度(见 `docs/db/schema.md` delta_records 节「锚点仅章」
|
|
44
|
+
* 与 `docs/design/10-data-model.md` §14.7)。
|
|
45
|
+
* **只限写入侧**:`/delta/compute` 的 `at_node_id` 不限层级(「第 3 章第 2 场时他什么状态」
|
|
46
|
+
* 是合法查询)。与 S13.3 target_type 白名单同模式:shared schema 不动、路由层 400;
|
|
47
|
+
* AI 提案通道(propose_add_delta)在 tools 层独立拒绝。
|
|
48
|
+
*/
|
|
49
|
+
function assertDeltaAnchorChapter(node, nodeId) {
|
|
50
|
+
if (node.type !== "chapter") {
|
|
51
|
+
throw new HttpError(400, "VALIDATION_ERROR", `变更记录的触发节点须为章(卷/场景不承载变更记录): ${nodeId}`);
|
|
52
|
+
}
|
|
36
53
|
}
|
|
37
54
|
/**
|
|
38
55
|
* per-op 必填字段校验:
|
|
@@ -90,7 +107,7 @@ function assertDeltaTargetType(targetType) {
|
|
|
90
107
|
}
|
|
91
108
|
// POST /api/v1/delta —— 追加属性变更(201;order 服务端全局单调生成)
|
|
92
109
|
// 校验顺序(oracle 建议):schema → target_type 白名单(400,S13.3 请求形状校验,无 DB 读)→
|
|
93
|
-
// 触发节点存在性(404)→ per-op 必填(400)→ insert
|
|
110
|
+
// 触发节点存在性(404)→ 触发节点层级仅章(400,卡片 1.2)→ per-op 必填(400)→ insert
|
|
94
111
|
deltaRoutes.post("/", async (c) => {
|
|
95
112
|
const project = requireCurrentProject();
|
|
96
113
|
const raw = await c.req.json().catch(() => null);
|
|
@@ -100,7 +117,8 @@ deltaRoutes.post("/", async (c) => {
|
|
|
100
117
|
}
|
|
101
118
|
const { node_id, target_type, target_id, changes, description } = parsed.data;
|
|
102
119
|
assertDeltaTargetType(target_type); // S13.3:变更目标仅实体类型(shared schema 不动,路由层收紧)
|
|
103
|
-
assertOutlineNode(project, node_id); // 触发节点必须存在且未软删(防死记录)
|
|
120
|
+
const node = assertOutlineNode(project, node_id); // 触发节点必须存在且未软删(防死记录)
|
|
121
|
+
assertDeltaAnchorChapter(node, node_id); // 卡片 1.2:触发节点仅章(卷/场景拒绝)
|
|
104
122
|
validateChangesByOp(changes); // per-op 必填字段
|
|
105
123
|
const row = insertDelta(project.db, {
|
|
106
124
|
nodeId: node_id,
|
package/dist/routes/delta.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"delta.js","sourceRoot":"","sources":["../../src/routes/delta.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,EAAE;AACF,4CAA4C;AAC5C,oCAAoC;AACpC,gBAAgB;AAChB,oEAAoE;AACpE,qCAAqC;AACrC,uEAAuE;AACvE,oEAAoE;AACpE,iDAAiD;AACjD,iFAAiF;AACjF,mEAAmE;AACnE,qCAAqC;AACrC,0DAA0D;AAC1D,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,WAAW,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAEvI,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAC9E,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,yCAAyC,CAAC;AACtG,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAuB,MAAM,0BAA0B,CAAC;AAEtF,2CAA2C;AAC3C,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,IAAI,EAAE,CAAC;AAEtC
|
|
1
|
+
{"version":3,"file":"delta.js","sourceRoot":"","sources":["../../src/routes/delta.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../src/routes/project.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,IAAI,EAAgB,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../src/routes/project.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,IAAI,EAAgB,MAAM,MAAM,CAAC;AAsD1C,oDAAoD;AACpD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAExD;AAED,wCAAwC;AACxC,eAAO,MAAM,cAAc,UAAU,CAAC;AAEtC;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAkBzD;AAED,yCAAyC;AACzC,eAAO,MAAM,aAAa,4EAAa,CAAC"}
|