@co0ontty/wand 4.60.0 → 4.61.0
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/dist/build-info.json +3 -3
- package/dist/claude-pty-bridge.js +4 -0
- package/dist/mission-types.d.ts +4 -0
- package/dist/missions.js +4 -0
- package/dist/server-app-connect.d.ts +40 -0
- package/dist/server-app-connect.js +241 -0
- package/dist/server-file-routes.js +11 -10
- package/dist/server-github-routes.js +12 -30
- package/dist/server-mission-routes.js +4 -2
- package/dist/server-request.d.ts +34 -0
- package/dist/server-request.js +61 -0
- package/dist/server-resume-routes.d.ts +25 -0
- package/dist/server-resume-routes.js +124 -0
- package/dist/server-session-routes.js +37 -182
- package/dist/server-settings-routes.js +6 -5
- package/dist/server-task-routes.js +93 -15
- package/dist/server-update-routes.js +23 -16
- package/dist/server-vault-routes.d.ts +16 -0
- package/dist/server-vault-routes.js +125 -0
- package/dist/server-workspace-routes.js +44 -8
- package/dist/server.js +12 -367
- package/dist/storage.d.ts +20 -1
- package/dist/storage.js +112 -16
- package/dist/structured-session-manager.js +33 -44
- package/dist/task-types.d.ts +19 -1
- package/dist/task-types.js +6 -1
- package/dist/types.d.ts +6 -0
- package/dist/wand-task-sync.d.ts +5 -3
- package/dist/wand-task-sync.js +30 -12
- package/dist/web-ui/content/scripts.js +76 -73
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/workspace-binding.d.ts +8 -1
- package/dist/workspace-binding.js +38 -0
- package/package.json +1 -1
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { getDefaultModelForProvider } from "./config.js";
|
|
2
2
|
import { asyncRoute } from "./express-async.js";
|
|
3
3
|
import { getErrorMessage } from "./error-utils.js";
|
|
4
|
+
import { bodyObject, sendRouteError, text } from "./server-request.js";
|
|
4
5
|
import { parseWandTaskAgent } from "./storage.js";
|
|
5
6
|
import { generateWandTaskTitle, provisionalTaskTitleFromDescription, TASK_TITLE_MAX_LENGTH } from "./task-title.js";
|
|
6
7
|
import { resolveSystemAiContext } from "./session-ai-context.js";
|
|
7
|
-
import {
|
|
8
|
-
|
|
8
|
+
import { WAND_MILESTONE_NAME_MAX_LENGTH } from "./task-types.js";
|
|
9
|
+
import { archiveBoardTask, syncClosedBoardTask, syncUngroupedSessionsToBoard } from "./wand-task-sync.js";
|
|
10
|
+
const STATUSES = new Set(["todo", "doing", "done", "archived"]);
|
|
9
11
|
const PRIORITIES = new Set(["none", "low", "medium", "high", "urgent"]);
|
|
10
12
|
const AGENT_PROVIDERS = new Set(["claude", "codex", "opencode", "grok", "qoder", "pi"]);
|
|
11
13
|
const AGENT_EFFORTS = new Set(["off", "standard", "deep", "max"]);
|
|
@@ -42,19 +44,29 @@ function dateValue(value) {
|
|
|
42
44
|
}
|
|
43
45
|
return value.trim();
|
|
44
46
|
}
|
|
45
|
-
function bodyObject(value) {
|
|
46
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
47
|
-
throw new Error("请求体必须是对象。");
|
|
48
|
-
return value;
|
|
49
|
-
}
|
|
50
|
-
function text(value) {
|
|
51
|
-
return typeof value === "string" ? value.trim() : "";
|
|
52
|
-
}
|
|
53
47
|
function labelsFrom(value) {
|
|
54
48
|
if (!Array.isArray(value))
|
|
55
49
|
return [];
|
|
56
50
|
return value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean).slice(0, 20);
|
|
57
51
|
}
|
|
52
|
+
/** 里程碑名:必填、去首尾空白、限长。 */
|
|
53
|
+
function milestoneNameFrom(value) {
|
|
54
|
+
const name = text(value).slice(0, WAND_MILESTONE_NAME_MAX_LENGTH);
|
|
55
|
+
if (!name)
|
|
56
|
+
throw new Error("请填写里程碑名称。");
|
|
57
|
+
return name;
|
|
58
|
+
}
|
|
59
|
+
/** 只有已存在的里程碑才能挂到任务上;null / 空串表示解绑。 */
|
|
60
|
+
function milestoneIdFrom(storage, value) {
|
|
61
|
+
if (value === null || value === undefined)
|
|
62
|
+
return null;
|
|
63
|
+
const id = text(value);
|
|
64
|
+
if (!id)
|
|
65
|
+
return null;
|
|
66
|
+
if (!storage.getWandMilestone(id))
|
|
67
|
+
throw new Error("未找到该里程碑。");
|
|
68
|
+
return id;
|
|
69
|
+
}
|
|
58
70
|
/** 任务派发配置:provider / model / thinkingEffort 三者必须同时给出且合法。 */
|
|
59
71
|
export function parseTaskAgent(value) {
|
|
60
72
|
if (value === null)
|
|
@@ -78,6 +90,7 @@ function taskDto({ storage, sessions }, task) {
|
|
|
78
90
|
return null;
|
|
79
91
|
const workspace = task.workspaceId ? storage.getWorkspace(task.workspaceId) : null;
|
|
80
92
|
const sessionIds = storage.listWandTaskSessionIds(task.id);
|
|
93
|
+
const milestone = task.milestoneId ? storage.getWandMilestone(task.milestoneId) : null;
|
|
81
94
|
return {
|
|
82
95
|
...task,
|
|
83
96
|
sessionIds,
|
|
@@ -88,6 +101,8 @@ function taskDto({ storage, sessions }, task) {
|
|
|
88
101
|
workspace: workspace
|
|
89
102
|
? { id: workspace.id, name: workspace.name, cwd: workspace.cwd }
|
|
90
103
|
: null,
|
|
104
|
+
// 卡片上要显示里程碑名字,直接在这里解出,免去前端再查一次列表。
|
|
105
|
+
milestone: milestone ? { id: milestone.id, name: milestone.name } : null,
|
|
91
106
|
};
|
|
92
107
|
}
|
|
93
108
|
function taskSessionSummary(session) {
|
|
@@ -103,7 +118,7 @@ function taskSessionSummary(session) {
|
|
|
103
118
|
};
|
|
104
119
|
}
|
|
105
120
|
function sendTaskError(res, error, fallback) {
|
|
106
|
-
res
|
|
121
|
+
sendRouteError(res, error, fallback);
|
|
107
122
|
}
|
|
108
123
|
/**
|
|
109
124
|
* 用户没写标题时,先用描述首行当占位标题,再由后台模型总结一个更好的。
|
|
@@ -167,11 +182,68 @@ export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy
|
|
|
167
182
|
console.error("[WandTask] Failed to sync ungrouped sessions onto the board:", getErrorMessage(error));
|
|
168
183
|
}
|
|
169
184
|
const workspaceId = typeof req.query.workspaceId === "string" ? req.query.workspaceId : undefined;
|
|
185
|
+
// 数组顺序即展示顺序:created_at 倒序,新创建的在上面。
|
|
170
186
|
res.json(storage.listWandTasks(workspaceId).map((task) => dto(task)));
|
|
171
187
|
});
|
|
172
188
|
app.get("/api/wand-task-agent-defaults", guard, (_req, res) => {
|
|
173
189
|
res.json(readTaskBoardLastAgent(storage));
|
|
174
190
|
});
|
|
191
|
+
// ── 里程碑:跨项目全局列表,所有任务面板的「新建任务」共用 ──
|
|
192
|
+
app.get("/api/wand-milestones", guard, (_req, res) => {
|
|
193
|
+
const counts = storage.countWandTasksByMilestone();
|
|
194
|
+
res.json({
|
|
195
|
+
milestones: storage.listWandMilestones().map((milestone) => ({
|
|
196
|
+
...milestone,
|
|
197
|
+
taskCount: counts[milestone.id] ?? 0,
|
|
198
|
+
})),
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
app.post("/api/wand-milestones", guard, (req, res) => {
|
|
202
|
+
try {
|
|
203
|
+
const body = bodyObject(req.body);
|
|
204
|
+
const name = milestoneNameFrom(body.name);
|
|
205
|
+
const duplicate = storage.listWandMilestones()
|
|
206
|
+
.find((milestone) => milestone.name.toLowerCase() === name.toLowerCase());
|
|
207
|
+
if (duplicate)
|
|
208
|
+
throw new Error(`里程碑「${duplicate.name}」已存在。`);
|
|
209
|
+
const created = storage.createWandMilestone({ name, dueDate: dateValue(body.dueDate) ?? null });
|
|
210
|
+
res.status(201).json({ ...created, taskCount: 0 });
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
sendTaskError(res, error, "无法创建里程碑。");
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
app.patch("/api/wand-milestones/:id", guard, (req, res) => {
|
|
217
|
+
try {
|
|
218
|
+
const body = bodyObject(req.body);
|
|
219
|
+
const current = storage.getWandMilestone(req.params.id);
|
|
220
|
+
if (!current) {
|
|
221
|
+
res.status(404).json({ error: "未找到该里程碑。" });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const patch = {};
|
|
225
|
+
if (body.name !== undefined) {
|
|
226
|
+
const name = milestoneNameFrom(body.name);
|
|
227
|
+
const duplicate = storage.listWandMilestones()
|
|
228
|
+
.find((milestone) => milestone.id !== current.id && milestone.name.toLowerCase() === name.toLowerCase());
|
|
229
|
+
if (duplicate)
|
|
230
|
+
throw new Error(`里程碑「${duplicate.name}」已存在。`);
|
|
231
|
+
patch.name = name;
|
|
232
|
+
}
|
|
233
|
+
if (body.dueDate !== undefined)
|
|
234
|
+
patch.dueDate = dateValue(body.dueDate) ?? null;
|
|
235
|
+
const updated = storage.updateWandMilestone(current.id, patch);
|
|
236
|
+
res.json({ ...updated, taskCount: storage.countWandTasksByMilestone()[current.id] ?? 0 });
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
sendTaskError(res, error, "无法更新里程碑。");
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
app.delete("/api/wand-milestones/:id", guard, (req, res) => {
|
|
243
|
+
// 只解绑任务,不删任务;已经删过的 id 幂等返回 ok。
|
|
244
|
+
storage.deleteWandMilestone(req.params.id);
|
|
245
|
+
res.json({ ok: true });
|
|
246
|
+
});
|
|
175
247
|
app.put("/api/wand-task-agent-defaults", guard, (req, res) => {
|
|
176
248
|
try {
|
|
177
249
|
const agent = parseTaskAgent(req.body);
|
|
@@ -202,6 +274,9 @@ export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy
|
|
|
202
274
|
const agent = body.agent === undefined ? null : parseTaskAgent(body.agent);
|
|
203
275
|
if (agent)
|
|
204
276
|
writeTaskBoardLastAgent(storage, agent);
|
|
277
|
+
const labels = labelsFrom(body.labels);
|
|
278
|
+
const dueDate = dateValue(body.dueDate) ?? null;
|
|
279
|
+
const milestoneId = milestoneIdFrom(storage, body.milestoneId);
|
|
205
280
|
const task = storage.createWandTask({
|
|
206
281
|
workspaceId,
|
|
207
282
|
title,
|
|
@@ -209,8 +284,9 @@ export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy
|
|
|
209
284
|
description,
|
|
210
285
|
status,
|
|
211
286
|
priority,
|
|
212
|
-
labels
|
|
213
|
-
dueDate
|
|
287
|
+
labels,
|
|
288
|
+
dueDate,
|
|
289
|
+
milestoneId,
|
|
214
290
|
agent,
|
|
215
291
|
});
|
|
216
292
|
if (titleSource === "auto") {
|
|
@@ -254,6 +330,8 @@ export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy
|
|
|
254
330
|
patch.labels = labelsFrom(body.labels);
|
|
255
331
|
if (body.dueDate !== undefined)
|
|
256
332
|
patch.dueDate = dateValue(body.dueDate) ?? null;
|
|
333
|
+
if (body.milestoneId !== undefined)
|
|
334
|
+
patch.milestoneId = milestoneIdFrom(storage, body.milestoneId);
|
|
257
335
|
if (body.workspaceId !== undefined) {
|
|
258
336
|
const workspaceId = body.workspaceId === null ? null : text(body.workspaceId);
|
|
259
337
|
if (workspaceId && !storage.getWorkspace(workspaceId))
|
|
@@ -273,8 +351,8 @@ export function registerTaskRoutes(app, deps, sessionsLegacy, requireAdminLegacy
|
|
|
273
351
|
res.status(404).json({ error: "未找到该任务。" });
|
|
274
352
|
return;
|
|
275
353
|
}
|
|
276
|
-
if (task.status === "done")
|
|
277
|
-
|
|
354
|
+
if (task.status === "done" || task.status === "archived")
|
|
355
|
+
syncClosedBoardTask(storage, task);
|
|
278
356
|
res.json(dto(storage.getWandTask(task.id) ?? task));
|
|
279
357
|
}
|
|
280
358
|
catch (error) {
|
|
@@ -2,6 +2,7 @@ import { Readable } from "node:stream";
|
|
|
2
2
|
import { pipeline } from "node:stream/promises";
|
|
3
3
|
import { getErrorMessage } from "./error-utils.js";
|
|
4
4
|
import { asyncRoute } from "./express-async.js";
|
|
5
|
+
import { sendRouteError, stringQuery } from "./server-request.js";
|
|
5
6
|
import { checkProviderCliUpdates, updateProviderClis, verifyProviderCliUpdateResults, } from "./provider-cli-updater.js";
|
|
6
7
|
import { absoluteUrl, buildIosInstallPage, buildIosOtaManifest, buildItmsServicesUrl, collectIosOtaBlockers, formatIosAssetSize, inspectIpa, normalizePublicOrigin, publicOriginFromRequest, } from "./ios-ota.js";
|
|
7
8
|
import { streamFileWithRange } from "./server-file-routes.js";
|
|
@@ -48,13 +49,23 @@ async function proxyGitHubApk(_req, res, asset, fetchImpl = fetch) {
|
|
|
48
49
|
res.destroy();
|
|
49
50
|
}
|
|
50
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* 三个 update 检查接口都要求 `currentVersion`。缺参数是客户端 bug,不是服务端异常,
|
|
54
|
+
* 用 400 + 固定的英文文案(老客户端按字符串匹配)。
|
|
55
|
+
*/
|
|
56
|
+
function requireCurrentVersion(req, res) {
|
|
57
|
+
const currentVersion = stringQuery(req.query.currentVersion);
|
|
58
|
+
if (!currentVersion) {
|
|
59
|
+
res.status(400).json({ error: "Missing currentVersion query parameter." });
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return currentVersion;
|
|
63
|
+
}
|
|
51
64
|
export function registerPublicUpdateRoutes(app, deps) {
|
|
52
65
|
app.get("/api/android-apk-update", asyncRoute(async (req, res) => {
|
|
53
|
-
const currentVersion =
|
|
54
|
-
if (
|
|
55
|
-
res.status(400).json({ error: "Missing currentVersion query parameter." });
|
|
66
|
+
const currentVersion = requireCurrentVersion(req, res);
|
|
67
|
+
if (currentVersion === null)
|
|
56
68
|
return;
|
|
57
|
-
}
|
|
58
69
|
const channel = req.query.channel === "beta" ? "beta" : "stable";
|
|
59
70
|
const latest = await deps.resolveLatestApk(channel);
|
|
60
71
|
if (!latest) {
|
|
@@ -126,11 +137,9 @@ export function registerPublicUpdateRoutes(app, deps) {
|
|
|
126
137
|
});
|
|
127
138
|
}));
|
|
128
139
|
app.get("/api/macos-dmg-update", asyncRoute(async (req, res) => {
|
|
129
|
-
const currentVersion =
|
|
130
|
-
if (
|
|
131
|
-
res.status(400).json({ error: "Missing currentVersion query parameter." });
|
|
140
|
+
const currentVersion = requireCurrentVersion(req, res);
|
|
141
|
+
if (currentVersion === null)
|
|
132
142
|
return;
|
|
133
|
-
}
|
|
134
143
|
const latest = await deps.resolveLatestDmg();
|
|
135
144
|
if (!latest) {
|
|
136
145
|
res.json({ updateAvailable: false, currentVersion, latestVersion: null, downloadUrl: null, source: null });
|
|
@@ -162,11 +171,9 @@ export function registerPublicUpdateRoutes(app, deps) {
|
|
|
162
171
|
});
|
|
163
172
|
}));
|
|
164
173
|
app.get("/api/ios-ipa-update", asyncRoute(async (req, res) => {
|
|
165
|
-
const currentVersion =
|
|
166
|
-
if (
|
|
167
|
-
res.status(400).json({ error: "Missing currentVersion query parameter." });
|
|
174
|
+
const currentVersion = requireCurrentVersion(req, res);
|
|
175
|
+
if (currentVersion === null)
|
|
168
176
|
return;
|
|
169
|
-
}
|
|
170
177
|
const payload = await resolveIosOtaPayload(req, deps);
|
|
171
178
|
if (!payload) {
|
|
172
179
|
res.json({
|
|
@@ -344,7 +351,7 @@ export function registerAdminUpdateRoutes(app, deps) {
|
|
|
344
351
|
});
|
|
345
352
|
}
|
|
346
353
|
catch (error) {
|
|
347
|
-
res
|
|
354
|
+
sendRouteError(res, error, "检查 CLI 更新失败。", 500);
|
|
348
355
|
}
|
|
349
356
|
}));
|
|
350
357
|
app.post("/api/provider-cli-updates", requireAdmin, asyncRoute(async (req, res) => {
|
|
@@ -369,7 +376,7 @@ export function registerAdminUpdateRoutes(app, deps) {
|
|
|
369
376
|
res.json({ ok: results.every((item) => item.ok), results, ...after, autoUpdate: storage.getConfigValue("autoUpdateProviderClis") === "true" });
|
|
370
377
|
}
|
|
371
378
|
catch (error) {
|
|
372
|
-
res
|
|
379
|
+
sendRouteError(res, error, "更新 CLI 失败。", 500);
|
|
373
380
|
}
|
|
374
381
|
finally {
|
|
375
382
|
state.providerCliUpdateInFlight = false;
|
|
@@ -387,7 +394,7 @@ export function registerAdminUpdateRoutes(app, deps) {
|
|
|
387
394
|
});
|
|
388
395
|
}
|
|
389
396
|
catch (error) {
|
|
390
|
-
res
|
|
397
|
+
sendRouteError(res, error, "检查更新失败。", 500);
|
|
391
398
|
}
|
|
392
399
|
}));
|
|
393
400
|
app.post("/api/update", requireAdmin, asyncRoute(async (_req, res) => {
|
|
@@ -437,7 +444,7 @@ export function registerAdminUpdateRoutes(app, deps) {
|
|
|
437
444
|
});
|
|
438
445
|
}
|
|
439
446
|
catch (error) {
|
|
440
|
-
res
|
|
447
|
+
sendRouteError(res, error, "更新失败。", 500);
|
|
441
448
|
}
|
|
442
449
|
finally {
|
|
443
450
|
state.updateInFlight = false;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 浏览器扩展密码库 HTTP 路由(`/api/browser-extension/*`)。
|
|
3
|
+
*
|
|
4
|
+
* 挂在 `requireAuth` + `requirePasswordVault` scope 之后,因此这里的 handler
|
|
5
|
+
* 不再自己鉴权。原来它们挤在 `server.ts` 组合根中间,把启动逻辑切成了两半;
|
|
6
|
+
* 独立成模块后组合根只剩下顺序清晰的装配。
|
|
7
|
+
*/
|
|
8
|
+
import type { Express } from "express";
|
|
9
|
+
import type { WandStorage } from "./storage.js";
|
|
10
|
+
import type { WandConfig } from "./types.js";
|
|
11
|
+
export interface VaultRoutesDependencies {
|
|
12
|
+
storage: WandStorage;
|
|
13
|
+
config: WandConfig;
|
|
14
|
+
useHttps: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function registerVaultRoutes(app: Express, deps: VaultRoutesDependencies): void;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 浏览器扩展密码库 HTTP 路由(`/api/browser-extension/*`)。
|
|
3
|
+
*
|
|
4
|
+
* 挂在 `requireAuth` + `requirePasswordVault` scope 之后,因此这里的 handler
|
|
5
|
+
* 不再自己鉴权。原来它们挤在 `server.ts` 组合根中间,把启动逻辑切成了两半;
|
|
6
|
+
* 独立成模块后组合根只剩下顺序清晰的装配。
|
|
7
|
+
*/
|
|
8
|
+
import { buildPasswordSecurityReport, generatePassword, generateTotpCode, normalizePasswordItemType, } from "./password-manager.js";
|
|
9
|
+
import { firstQueryStringValue, sendRouteError } from "./server-request.js";
|
|
10
|
+
import { resolveRequestServerUrl } from "./server-app-connect.js";
|
|
11
|
+
export function registerVaultRoutes(app, deps) {
|
|
12
|
+
const { storage, config, useHttps } = deps;
|
|
13
|
+
app.get("/api/browser-extension/status", (req, res) => {
|
|
14
|
+
res.json({
|
|
15
|
+
ok: true,
|
|
16
|
+
serverUrl: resolveRequestServerUrl(req, config, useHttps),
|
|
17
|
+
features: {
|
|
18
|
+
loginAutofill: true,
|
|
19
|
+
saveLogins: true,
|
|
20
|
+
federatedLoginMemory: true,
|
|
21
|
+
passwordGenerator: true,
|
|
22
|
+
totp: true,
|
|
23
|
+
cardsAndIdentities: true,
|
|
24
|
+
vaults: true,
|
|
25
|
+
securityReport: true,
|
|
26
|
+
passkeys: "webauthn-proxy",
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
app.get("/api/browser-extension/vaults", (_req, res) => {
|
|
31
|
+
res.json({ vaults: storage.listPasswordVaults() });
|
|
32
|
+
});
|
|
33
|
+
app.post("/api/browser-extension/vaults", (req, res) => {
|
|
34
|
+
try {
|
|
35
|
+
const vault = storage.createPasswordVault(req.body.name);
|
|
36
|
+
res.status(201).json({ vault });
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
sendRouteError(res, error, "无法创建 vault。");
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
app.get("/api/browser-extension/items", (req, res) => {
|
|
43
|
+
const filter = {
|
|
44
|
+
q: firstQueryStringValue(req.query.q),
|
|
45
|
+
url: firstQueryStringValue(req.query.url),
|
|
46
|
+
vaultId: firstQueryStringValue(req.query.vaultId),
|
|
47
|
+
type: req.query.type ? normalizePasswordItemType(firstQueryStringValue(req.query.type)) : undefined,
|
|
48
|
+
limit: req.query.limit ? Number(firstQueryStringValue(req.query.limit)) : undefined,
|
|
49
|
+
};
|
|
50
|
+
res.json({ items: storage.listPasswordItems(filter) });
|
|
51
|
+
});
|
|
52
|
+
app.post("/api/browser-extension/items", (req, res) => {
|
|
53
|
+
try {
|
|
54
|
+
const item = storage.createPasswordItem(req.body);
|
|
55
|
+
res.status(201).json({ item });
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
sendRouteError(res, error, "无法保存条目。");
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
app.get("/api/browser-extension/items/:id", (req, res) => {
|
|
62
|
+
const item = storage.getPasswordItem(req.params.id);
|
|
63
|
+
if (!item) {
|
|
64
|
+
res.status(404).json({ error: "条目不存在。" });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
res.json({ item });
|
|
68
|
+
});
|
|
69
|
+
app.put("/api/browser-extension/items/:id", (req, res) => {
|
|
70
|
+
try {
|
|
71
|
+
const item = storage.updatePasswordItem(req.params.id, req.body);
|
|
72
|
+
if (!item) {
|
|
73
|
+
res.status(404).json({ error: "条目不存在。" });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
res.json({ item });
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
sendRouteError(res, error, "无法更新条目。");
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
app.delete("/api/browser-extension/items/:id", (req, res) => {
|
|
83
|
+
if (!storage.deletePasswordItem(req.params.id)) {
|
|
84
|
+
res.status(404).json({ error: "条目不存在。" });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
res.json({ ok: true });
|
|
88
|
+
});
|
|
89
|
+
app.post("/api/browser-extension/items/:id/use", (req, res) => {
|
|
90
|
+
const item = storage.touchPasswordItem(req.params.id);
|
|
91
|
+
if (!item) {
|
|
92
|
+
res.status(404).json({ error: "条目不存在。" });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
res.json({ item });
|
|
96
|
+
});
|
|
97
|
+
app.get("/api/browser-extension/generator/password", (req, res) => {
|
|
98
|
+
res.json({
|
|
99
|
+
password: generatePassword({
|
|
100
|
+
length: Number(firstQueryStringValue(req.query.length)),
|
|
101
|
+
digits: firstQueryStringValue(req.query.digits) !== "false",
|
|
102
|
+
symbols: firstQueryStringValue(req.query.symbols) !== "false",
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
app.post("/api/browser-extension/totp/preview", (req, res) => {
|
|
107
|
+
try {
|
|
108
|
+
const { secret, digits, period } = req.body;
|
|
109
|
+
if (!secret) {
|
|
110
|
+
res.status(400).json({ error: "缺少 TOTP secret。" });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
res.json({
|
|
114
|
+
code: generateTotpCode(secret, Date.now(), digits ?? 6, period ?? 30),
|
|
115
|
+
period: period ?? 30,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
sendRouteError(res, error, "无法生成 TOTP。");
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
app.get("/api/browser-extension/security-report", (_req, res) => {
|
|
123
|
+
res.json({ report: buildPasswordSecurityReport(storage.listPasswordItems({ includeArchived: false, limit: 200 })) });
|
|
124
|
+
});
|
|
125
|
+
}
|
|
@@ -2,13 +2,14 @@ import path from "node:path";
|
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import { existsSync, statSync } from "node:fs";
|
|
4
4
|
import { asyncRoute } from "./express-async.js";
|
|
5
|
+
import { sendRouteError } from "./server-request.js";
|
|
5
6
|
import { getErrorMessage } from "./error-utils.js";
|
|
6
7
|
import { expandHomePath } from "./middleware/path-safety.js";
|
|
7
8
|
import { checkSessionWorktreeMergeabilityAsync, cleanupWorktreeSync, prepareSessionWorktree, resolveWorktreeTargetBranchAsync, } from "./git-worktree.js";
|
|
8
9
|
import { collectSessionTopicBlocklist } from "./session-topic.js";
|
|
9
10
|
import { resolveSessionDisplayTitle } from "./session-transport.js";
|
|
10
11
|
import { GLOBAL_WORKSPACE_ID } from "./types.js";
|
|
11
|
-
import { attachUnboundSessionsToWorkspace, backfillSessionWorkspaces, normalizeProjectCwd, projectCwdForSession, } from "./workspace-binding.js";
|
|
12
|
+
import { attachUnboundSessionsToWorkspace, backfillSessionWorkspaces, normalizeProjectCwd, projectCwdForSession, syncDirectoryNameForWorkspace, } from "./workspace-binding.js";
|
|
12
13
|
import { archiveBoardTaskForWorkspaceTask, ensureBoardTaskForWorkspaceTask } from "./wand-task-sync.js";
|
|
13
14
|
const PROVIDERS = new Set(["claude", "codex", "opencode", "grok", "qoder", "pi"]);
|
|
14
15
|
function workspaceSessionTitle(session, names = {}) {
|
|
@@ -104,6 +105,12 @@ function taskRuntimeCwd(task, workspace) {
|
|
|
104
105
|
function createTaskForWorkspace(storage, workspace, body) {
|
|
105
106
|
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
106
107
|
const taskName = name || "未命名任务";
|
|
108
|
+
// 里程碑是全局列表;建任务时可选,非法 id 直接报错而不是静默丢掉。
|
|
109
|
+
const requestedMilestoneId = typeof body.milestoneId === "string" ? body.milestoneId.trim() : "";
|
|
110
|
+
if (requestedMilestoneId && !storage.getWandMilestone(requestedMilestoneId)) {
|
|
111
|
+
throw new Error("未找到该里程碑。");
|
|
112
|
+
}
|
|
113
|
+
const milestoneId = requestedMilestoneId || null;
|
|
107
114
|
let mountedCwd;
|
|
108
115
|
if (body.cwd !== undefined && body.cwd !== null && String(body.cwd).trim()) {
|
|
109
116
|
mountedCwd = resolveWorkspaceCwd(body.cwd);
|
|
@@ -140,6 +147,7 @@ function createTaskForWorkspace(storage, workspace, body) {
|
|
|
140
147
|
name: taskName,
|
|
141
148
|
cwd: storedCwd,
|
|
142
149
|
worktree,
|
|
150
|
+
milestoneId,
|
|
143
151
|
});
|
|
144
152
|
ensureBoardTaskForWorkspaceTask(storage, task, workspace);
|
|
145
153
|
return {
|
|
@@ -289,11 +297,13 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
289
297
|
cwd = resolveWorkspaceCwd(body.cwd);
|
|
290
298
|
}
|
|
291
299
|
catch (error) {
|
|
292
|
-
res
|
|
300
|
+
sendRouteError(res, error, "目录无效。");
|
|
293
301
|
return;
|
|
294
302
|
}
|
|
295
303
|
const defaultProvider = parseDefaultProvider(body.defaultProvider);
|
|
296
304
|
const workspace = storage.createWorkspace({ name, cwd, defaultProvider });
|
|
305
|
+
// 项目名就是工作区显示名:目录自定义名一起写,保证各端显示一致。
|
|
306
|
+
storage.setSessionDirectoryName(cwd, name);
|
|
297
307
|
const attached = attachUnboundSessionsToWorkspace(storage, workspace);
|
|
298
308
|
res.status(201).json({ ...workspace, worktreeCount: 0, sessionCount: attached });
|
|
299
309
|
}));
|
|
@@ -321,6 +331,11 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
321
331
|
return;
|
|
322
332
|
}
|
|
323
333
|
const body = req.body;
|
|
334
|
+
// 全局任务空间不是用户目录,不允许改名。
|
|
335
|
+
if (isGlobalWorkspace(existing) && typeof body.name === "string" && body.name.trim()) {
|
|
336
|
+
res.status(400).json({ error: "不能重命名全局任务空间。" });
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
324
339
|
const patch = {};
|
|
325
340
|
if (typeof body.name === "string" && body.name.trim())
|
|
326
341
|
patch.name = body.name.trim();
|
|
@@ -329,7 +344,7 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
329
344
|
patch.cwd = resolveWorkspaceCwd(body.cwd);
|
|
330
345
|
}
|
|
331
346
|
catch (error) {
|
|
332
|
-
res
|
|
347
|
+
sendRouteError(res, error, "目录无效。");
|
|
333
348
|
return;
|
|
334
349
|
}
|
|
335
350
|
}
|
|
@@ -343,6 +358,9 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
343
358
|
}
|
|
344
359
|
storage.updateWorkspace(existing.id, patch);
|
|
345
360
|
const updated = storage.getWorkspace(existing.id);
|
|
361
|
+
// 改名时同步目录自定义名,否则其它端(会话列表 / 项目视图)仍是旧名字。
|
|
362
|
+
if (updated)
|
|
363
|
+
syncDirectoryNameForWorkspace(storage, updated);
|
|
346
364
|
res.json(updated ? workspaceWithCounts(storage, updated) : null);
|
|
347
365
|
});
|
|
348
366
|
// 删除项目;cascade=true 连带删会话,否则仅解绑
|
|
@@ -404,7 +422,7 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
404
422
|
});
|
|
405
423
|
}
|
|
406
424
|
catch (error) {
|
|
407
|
-
res
|
|
425
|
+
sendRouteError(res, error, "创建任务失败。");
|
|
408
426
|
}
|
|
409
427
|
}));
|
|
410
428
|
// 目录组为一级容器的任务聚合列表,供侧栏「任务」视图一次拉全。
|
|
@@ -430,6 +448,12 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
430
448
|
backfillSessionWorkspaces(storage);
|
|
431
449
|
const workspaces = storage.listWorkspaces();
|
|
432
450
|
const summarize = (session, names = {}) => workspaceSessionSummary(session, names, sessions);
|
|
451
|
+
// 目录自定义名(工作区名称):没有项目实体的合成目录也按这个名字显示。
|
|
452
|
+
const directoryNames = storage.listSessionDirectoryNames();
|
|
453
|
+
const customNameForCwd = (cwd) => {
|
|
454
|
+
const name = directoryNames.get(normalizeProjectCwd(cwd))?.trim();
|
|
455
|
+
return name || undefined;
|
|
456
|
+
};
|
|
433
457
|
const visibleWorkspaces = workspaceFilter
|
|
434
458
|
? workspaces.filter((workspace) => workspace.id === workspaceFilter)
|
|
435
459
|
: workspaces;
|
|
@@ -458,7 +482,9 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
458
482
|
return existing;
|
|
459
483
|
const created = {
|
|
460
484
|
workspaceId: id,
|
|
461
|
-
workspaceName: normalized
|
|
485
|
+
workspaceName: customNameForCwd(normalized)
|
|
486
|
+
?? normalized.split("/").filter(Boolean).at(-1)
|
|
487
|
+
?? normalized,
|
|
462
488
|
workspaceCwd: normalized,
|
|
463
489
|
synthetic: true,
|
|
464
490
|
tasks: [],
|
|
@@ -529,7 +555,9 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
529
555
|
if (!synthetic) {
|
|
530
556
|
synthetic = {
|
|
531
557
|
workspaceId: id,
|
|
532
|
-
workspaceName: resolved
|
|
558
|
+
workspaceName: customNameForCwd(resolved)
|
|
559
|
+
?? resolved.split("/").filter(Boolean).at(-1)
|
|
560
|
+
?? resolved,
|
|
533
561
|
workspaceCwd: resolved,
|
|
534
562
|
synthetic: true,
|
|
535
563
|
tasks: [],
|
|
@@ -594,7 +622,7 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
594
622
|
target = await resolveWorktreeTargetBranchAsync(workspace.cwd);
|
|
595
623
|
}
|
|
596
624
|
catch (error) {
|
|
597
|
-
res
|
|
625
|
+
sendRouteError(res, error, "无法识别项目默认分支。");
|
|
598
626
|
return;
|
|
599
627
|
}
|
|
600
628
|
const worktrees = await Promise.all(tasks.map(async (task) => {
|
|
@@ -671,7 +699,7 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
671
699
|
});
|
|
672
700
|
}
|
|
673
701
|
catch (error) {
|
|
674
|
-
res
|
|
702
|
+
sendRouteError(res, error, "创建任务失败。");
|
|
675
703
|
}
|
|
676
704
|
}));
|
|
677
705
|
// 任务详情:meta + 该任务下的会话;访问即更新 lastOpenedAt
|
|
@@ -708,6 +736,14 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
708
736
|
patch.name = body.name.trim();
|
|
709
737
|
if (body.status === "active" || body.status === "done")
|
|
710
738
|
patch.status = body.status;
|
|
739
|
+
if (body.milestoneId !== undefined) {
|
|
740
|
+
const milestoneId = typeof body.milestoneId === "string" ? body.milestoneId.trim() : "";
|
|
741
|
+
if (milestoneId && !storage.getWandMilestone(milestoneId)) {
|
|
742
|
+
sendRouteError(res, new Error("未找到该里程碑。"), "无法更新任务。");
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
patch.milestoneId = milestoneId || null;
|
|
746
|
+
}
|
|
711
747
|
storage.updateWorkspaceTask(existing.id, patch);
|
|
712
748
|
if (patch.status === "done")
|
|
713
749
|
archiveBoardTaskForWorkspaceTask(storage, existing.id);
|