@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
package/dist/server.js
CHANGED
|
@@ -6,7 +6,6 @@ import { stat } from "node:fs/promises";
|
|
|
6
6
|
import { createServer as createHttpServer } from "node:http";
|
|
7
7
|
import { createServer as createHttpsServer } from "node:https";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
|
-
import os from "node:os";
|
|
10
9
|
import path from "node:path";
|
|
11
10
|
import process from "node:process";
|
|
12
11
|
import { WebSocketServer } from "ws";
|
|
@@ -22,6 +21,9 @@ import { resolveSessionAiContext, resolveSystemAiContext } from "./session-ai-co
|
|
|
22
21
|
import { StructuredSessionManager } from "./structured-session-manager.js";
|
|
23
22
|
import { recordRecentPath, registerFileRoutes } from "./server-file-routes.js";
|
|
24
23
|
import { registerSettingsRoutes } from "./server-settings-routes.js";
|
|
24
|
+
import { appTokenLoginPayload, buildStructuredChatPersonaPayload, isBrowserExtensionOrigin, resolveAppConnectCode, resolveStructuredChatAvatarPath, verifyAppToken, } from "./server-app-connect.js";
|
|
25
|
+
import { firstHeaderValue, sendRouteError } from "./server-request.js";
|
|
26
|
+
import { registerVaultRoutes } from "./server-vault-routes.js";
|
|
25
27
|
import { registerGithubRoutes } from "./server-github-routes.js";
|
|
26
28
|
import { registerTaskRoutes } from "./server-task-routes.js";
|
|
27
29
|
import { getGithubConnectorStatus } from "./github-connector.js";
|
|
@@ -44,7 +46,6 @@ import { toSessionDetailDTO } from "./session-transport.js";
|
|
|
44
46
|
import { registerUploadRoutes } from "./upload-routes.js";
|
|
45
47
|
import { optimizePrompt, PromptOptimizeError } from "./prompt-optimizer.js";
|
|
46
48
|
import { resolveDatabasePath, WandStorage } from "./storage.js";
|
|
47
|
-
import { buildPasswordSecurityReport, generatePassword, generateTotpCode, normalizePasswordItemType, } from "./password-manager.js";
|
|
48
49
|
import { deepRepairRuntimePath, formatPathRepairSummary, repairRuntimePath } from "./path-repair.js";
|
|
49
50
|
import { DistributionManager } from "./distribution-manager.js";
|
|
50
51
|
import { isLogBusActive, wandTuiLog } from "./tui/log-bus.js";
|
|
@@ -100,83 +101,6 @@ function readBuildInfo() {
|
|
|
100
101
|
const BUILD_INFO = readBuildInfo();
|
|
101
102
|
const DISPLAY_VERSION = BUILD_INFO.version || PKG_VERSION;
|
|
102
103
|
const SERVER_INSTANCE_ID = crypto.randomUUID();
|
|
103
|
-
function isExternalAvatarSource(value) {
|
|
104
|
-
return /^(https?:|data:)/i.test(value);
|
|
105
|
-
}
|
|
106
|
-
function normalizePersonaName(value) {
|
|
107
|
-
if (typeof value !== "string")
|
|
108
|
-
return undefined;
|
|
109
|
-
const trimmed = value.trim();
|
|
110
|
-
return trimmed || undefined;
|
|
111
|
-
}
|
|
112
|
-
function normalizePersonaAvatar(value) {
|
|
113
|
-
if (typeof value !== "string")
|
|
114
|
-
return undefined;
|
|
115
|
-
const trimmed = value.trim();
|
|
116
|
-
return trimmed || undefined;
|
|
117
|
-
}
|
|
118
|
-
function resolveStructuredChatPersona(config) {
|
|
119
|
-
const persona = config.structuredChatPersona;
|
|
120
|
-
if (!persona)
|
|
121
|
-
return undefined;
|
|
122
|
-
const userName = normalizePersonaName(persona.user?.name);
|
|
123
|
-
const userAvatar = normalizePersonaAvatar(persona.user?.avatar);
|
|
124
|
-
const assistantName = normalizePersonaName(persona.assistant?.name);
|
|
125
|
-
const assistantAvatar = normalizePersonaAvatar(persona.assistant?.avatar);
|
|
126
|
-
if (!userName && !userAvatar && !assistantName && !assistantAvatar) {
|
|
127
|
-
return undefined;
|
|
128
|
-
}
|
|
129
|
-
return {
|
|
130
|
-
user: userName || userAvatar ? { name: userName, avatar: userAvatar } : undefined,
|
|
131
|
-
assistant: assistantName || assistantAvatar ? { name: assistantName, avatar: assistantAvatar } : undefined,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
function resolveStructuredChatAvatarPath(configPath, config, role) {
|
|
135
|
-
const avatar = role === "user"
|
|
136
|
-
? config.structuredChatPersona?.user?.avatar
|
|
137
|
-
: config.structuredChatPersona?.assistant?.avatar;
|
|
138
|
-
if (!avatar || isExternalAvatarSource(avatar)) {
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
const configDir = resolveConfigDir(configPath);
|
|
142
|
-
return path.isAbsolute(avatar) ? avatar : path.resolve(configDir, avatar);
|
|
143
|
-
}
|
|
144
|
-
async function buildStructuredChatPersonaPayload(configPath, config) {
|
|
145
|
-
const persona = resolveStructuredChatPersona(config);
|
|
146
|
-
if (!persona)
|
|
147
|
-
return undefined;
|
|
148
|
-
const buildRole = async (role) => {
|
|
149
|
-
const roleConfig = role === "user" ? persona.user : persona.assistant;
|
|
150
|
-
if (!roleConfig)
|
|
151
|
-
return undefined;
|
|
152
|
-
let avatar = roleConfig.avatar;
|
|
153
|
-
if (avatar && !isExternalAvatarSource(avatar)) {
|
|
154
|
-
const resolvedPath = resolveStructuredChatAvatarPath(configPath, config, role);
|
|
155
|
-
if (!resolvedPath) {
|
|
156
|
-
avatar = undefined;
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
try {
|
|
160
|
-
const fileStat = await stat(resolvedPath);
|
|
161
|
-
avatar = fileStat.isFile() ? `/api/structured-chat-avatar/${role}` : undefined;
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
avatar = undefined;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
if (!roleConfig.name && !avatar)
|
|
169
|
-
return undefined;
|
|
170
|
-
return {
|
|
171
|
-
name: roleConfig.name,
|
|
172
|
-
avatar,
|
|
173
|
-
};
|
|
174
|
-
};
|
|
175
|
-
const [user, assistant] = await Promise.all([buildRole("user"), buildRole("assistant")]);
|
|
176
|
-
if (!user && !assistant)
|
|
177
|
-
return undefined;
|
|
178
|
-
return { user, assistant };
|
|
179
|
-
}
|
|
180
104
|
// ── Auth helpers ──
|
|
181
105
|
const requestPrincipals = new WeakMap();
|
|
182
106
|
function buildRequireAuth(useHttps, storage, config, authService) {
|
|
@@ -259,167 +183,6 @@ function authenticateBearerAppToken(req, storage, config) {
|
|
|
259
183
|
return null;
|
|
260
184
|
}
|
|
261
185
|
}
|
|
262
|
-
function resolveRequestServerUrl(req, config, useHttps) {
|
|
263
|
-
// 配置了公开 origin 就优先用它:TLS 在 L4 反代(nginx stream / NPM TCP stream)终止时
|
|
264
|
-
// node 只看到明文 HTTP,既没有 X-Forwarded-Proto 也没有正确的 scheme,猜不出来。
|
|
265
|
-
const configuredOrigin = normalizePublicOrigin(config.publicOrigin);
|
|
266
|
-
if (configuredOrigin)
|
|
267
|
-
return resolveAppConnectOrigin(configuredOrigin, config);
|
|
268
|
-
const requestProtocol = getPublicRequestProtocol(req, useHttps ? "https" : "http");
|
|
269
|
-
const requestHost = getPublicRequestHost(req, config);
|
|
270
|
-
const originHeader = firstHeaderValue(req.headers.origin);
|
|
271
|
-
const browserOrigin = isBrowserExtensionOrigin(originHeader)
|
|
272
|
-
? undefined
|
|
273
|
-
: normalizePublicOrigin(originHeader);
|
|
274
|
-
return resolveAppConnectOrigin(browserOrigin ?? `${requestProtocol}://${requestHost}`, config);
|
|
275
|
-
}
|
|
276
|
-
function appTokenLoginPayload(req, storage, config, useHttps) {
|
|
277
|
-
return {
|
|
278
|
-
appToken: generateAppToken(getEffectivePassword(storage, config), config.appSecret ?? ""),
|
|
279
|
-
serverUrl: resolveRequestServerUrl(req, config, useHttps),
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
// ── App connection token helpers ──
|
|
283
|
-
function generateAppToken(password, secret) {
|
|
284
|
-
return crypto.createHmac("sha256", secret).update(password).digest("hex");
|
|
285
|
-
}
|
|
286
|
-
function verifyAppToken(token, password, secret) {
|
|
287
|
-
const expected = generateAppToken(password, secret);
|
|
288
|
-
return crypto.timingSafeEqual(Buffer.from(token, "hex"), Buffer.from(expected, "hex"));
|
|
289
|
-
}
|
|
290
|
-
function encodeConnectCode(url, token) {
|
|
291
|
-
return Buffer.from(`${url}#${token}`).toString("base64");
|
|
292
|
-
}
|
|
293
|
-
function firstHeaderValue(value) {
|
|
294
|
-
if (Array.isArray(value))
|
|
295
|
-
return value[0];
|
|
296
|
-
return value;
|
|
297
|
-
}
|
|
298
|
-
function firstHeaderListValue(value) {
|
|
299
|
-
return firstHeaderValue(value)?.split(",")[0]?.trim();
|
|
300
|
-
}
|
|
301
|
-
function unquoteHeaderValue(value) {
|
|
302
|
-
const trimmed = value.trim();
|
|
303
|
-
if (trimmed.length >= 2 && trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
|
|
304
|
-
return trimmed.slice(1, -1);
|
|
305
|
-
}
|
|
306
|
-
return trimmed;
|
|
307
|
-
}
|
|
308
|
-
function getForwardedParam(req, key) {
|
|
309
|
-
const forwarded = firstHeaderListValue(req.headers.forwarded);
|
|
310
|
-
if (!forwarded)
|
|
311
|
-
return undefined;
|
|
312
|
-
const targetKey = key.toLowerCase();
|
|
313
|
-
for (const part of forwarded.split(";")) {
|
|
314
|
-
const eqIdx = part.indexOf("=");
|
|
315
|
-
if (eqIdx < 1)
|
|
316
|
-
continue;
|
|
317
|
-
const partKey = part.slice(0, eqIdx).trim().toLowerCase();
|
|
318
|
-
if (partKey !== targetKey)
|
|
319
|
-
continue;
|
|
320
|
-
return unquoteHeaderValue(part.slice(eqIdx + 1));
|
|
321
|
-
}
|
|
322
|
-
return undefined;
|
|
323
|
-
}
|
|
324
|
-
function normalizePublicProtocol(value) {
|
|
325
|
-
const proto = value?.trim().toLowerCase();
|
|
326
|
-
if (proto === "http" || proto === "https")
|
|
327
|
-
return proto;
|
|
328
|
-
return undefined;
|
|
329
|
-
}
|
|
330
|
-
function getPublicRequestProtocol(req, fallback) {
|
|
331
|
-
return (normalizePublicProtocol(firstHeaderListValue(req.headers["x-forwarded-proto"]))
|
|
332
|
-
?? normalizePublicProtocol(getForwardedParam(req, "proto"))
|
|
333
|
-
?? (firstHeaderListValue(req.headers["x-forwarded-ssl"])?.toLowerCase() === "on" ? "https" : undefined)
|
|
334
|
-
?? (firstHeaderListValue(req.headers["x-forwarded-scheme"])?.toLowerCase() === "https" ? "https" : undefined)
|
|
335
|
-
?? fallback);
|
|
336
|
-
}
|
|
337
|
-
function getPublicRequestHost(req, config) {
|
|
338
|
-
return (firstHeaderListValue(req.headers["x-forwarded-host"])
|
|
339
|
-
?? getForwardedParam(req, "host")
|
|
340
|
-
?? req.headers.host
|
|
341
|
-
?? `${config.host}:${config.port}`);
|
|
342
|
-
}
|
|
343
|
-
function firstQueryStringValue(value) {
|
|
344
|
-
if (typeof value === "string")
|
|
345
|
-
return value;
|
|
346
|
-
if (Array.isArray(value))
|
|
347
|
-
return firstQueryStringValue(value[0]);
|
|
348
|
-
return undefined;
|
|
349
|
-
}
|
|
350
|
-
function normalizePublicOrigin(value) {
|
|
351
|
-
const raw = value?.trim();
|
|
352
|
-
if (!raw)
|
|
353
|
-
return undefined;
|
|
354
|
-
try {
|
|
355
|
-
const parsed = new URL(raw);
|
|
356
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
357
|
-
return undefined;
|
|
358
|
-
if (!parsed.hostname || parsed.username || parsed.password)
|
|
359
|
-
return undefined;
|
|
360
|
-
return parsed.origin;
|
|
361
|
-
}
|
|
362
|
-
catch {
|
|
363
|
-
return undefined;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
function isBrowserExtensionOrigin(value) {
|
|
367
|
-
if (!value)
|
|
368
|
-
return false;
|
|
369
|
-
return /^chrome-extension:\/\/[a-z]{32}$/i.test(value)
|
|
370
|
-
|| /^moz-extension:\/\/[0-9a-f-]+$/i.test(value)
|
|
371
|
-
|| /^safari-web-extension:\/\//i.test(value);
|
|
372
|
-
}
|
|
373
|
-
function isPrivateIpv4(address) {
|
|
374
|
-
if (address.startsWith("10.") || address.startsWith("192.168."))
|
|
375
|
-
return true;
|
|
376
|
-
const match = address.match(/^172\.(\d+)\./);
|
|
377
|
-
return match ? Number(match[1]) >= 16 && Number(match[1]) <= 31 : false;
|
|
378
|
-
}
|
|
379
|
-
function preferredLanIpv4() {
|
|
380
|
-
const candidates = [];
|
|
381
|
-
for (const [name, entries] of Object.entries(os.networkInterfaces())) {
|
|
382
|
-
for (const entry of entries ?? []) {
|
|
383
|
-
const family = entry.family;
|
|
384
|
-
if (entry.internal || (family !== "IPv4" && family !== 4))
|
|
385
|
-
continue;
|
|
386
|
-
let score = isPrivateIpv4(entry.address) ? 10 : 0;
|
|
387
|
-
if (/^(en|eth|wlan)\d+$/i.test(name))
|
|
388
|
-
score += 10;
|
|
389
|
-
candidates.push({ address: entry.address, score });
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
candidates.sort((a, b) => b.score - a.score || a.address.localeCompare(b.address));
|
|
393
|
-
return candidates[0]?.address;
|
|
394
|
-
}
|
|
395
|
-
/**
|
|
396
|
-
* App 连接码用于另一台设备。设置页若从 localhost 打开,直接编码浏览器 origin
|
|
397
|
-
* 会让客户端连接它自己;监听 0.0.0.0 时自动换成本机优先 LAN IPv4。
|
|
398
|
-
*/
|
|
399
|
-
function resolveAppConnectOrigin(origin, config) {
|
|
400
|
-
if (config.host !== "0.0.0.0")
|
|
401
|
-
return origin;
|
|
402
|
-
try {
|
|
403
|
-
const parsed = new URL(origin);
|
|
404
|
-
const hostname = parsed.hostname.toLowerCase();
|
|
405
|
-
const isLocalOnly = hostname === "localhost"
|
|
406
|
-
|| hostname === "127.0.0.1"
|
|
407
|
-
|| hostname === "[::1]"
|
|
408
|
-
|| hostname === "::1"
|
|
409
|
-
|| hostname === "0.0.0.0"
|
|
410
|
-
|| hostname === "[::]";
|
|
411
|
-
if (!isLocalOnly)
|
|
412
|
-
return parsed.origin;
|
|
413
|
-
const lanIp = preferredLanIpv4();
|
|
414
|
-
if (!lanIp)
|
|
415
|
-
return parsed.origin;
|
|
416
|
-
parsed.hostname = lanIp;
|
|
417
|
-
return parsed.origin;
|
|
418
|
-
}
|
|
419
|
-
catch {
|
|
420
|
-
return origin;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
186
|
// ── Startup error handling ──
|
|
424
187
|
process.on("uncaughtException", (err) => {
|
|
425
188
|
wandError("服务器异常", err.message, "请检查配置是否正确,或尝试重启服务。");
|
|
@@ -703,7 +466,9 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
703
466
|
res.json({
|
|
704
467
|
ok: true,
|
|
705
468
|
principal,
|
|
706
|
-
...(client === "browser-extension"
|
|
469
|
+
...(client === "browser-extension"
|
|
470
|
+
? appTokenLoginPayload(req, config, useHttps, getEffectivePassword(storage, config))
|
|
471
|
+
: {}),
|
|
707
472
|
});
|
|
708
473
|
});
|
|
709
474
|
app.post("/api/logout", (req, res) => {
|
|
@@ -824,128 +589,17 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
824
589
|
serverInstanceId: SERVER_INSTANCE_ID,
|
|
825
590
|
});
|
|
826
591
|
}));
|
|
827
|
-
// ──
|
|
592
|
+
// ── Claude skills & browser extension vault ──
|
|
828
593
|
app.get("/api/claude-skills", (req, res) => {
|
|
829
594
|
try {
|
|
830
595
|
const cwd = resolveSessionCwd(typeof req.query.cwd === "string" ? req.query.cwd : undefined, config.defaultCwd);
|
|
831
596
|
res.json({ skills: listClaudeSkills(cwd) });
|
|
832
597
|
}
|
|
833
598
|
catch (error) {
|
|
834
|
-
res
|
|
835
|
-
}
|
|
836
|
-
});
|
|
837
|
-
app.get("/api/browser-extension/status", (req, res) => {
|
|
838
|
-
res.json({
|
|
839
|
-
ok: true,
|
|
840
|
-
serverUrl: resolveRequestServerUrl(req, config, useHttps),
|
|
841
|
-
features: {
|
|
842
|
-
loginAutofill: true,
|
|
843
|
-
saveLogins: true,
|
|
844
|
-
federatedLoginMemory: true,
|
|
845
|
-
passwordGenerator: true,
|
|
846
|
-
totp: true,
|
|
847
|
-
cardsAndIdentities: true,
|
|
848
|
-
vaults: true,
|
|
849
|
-
securityReport: true,
|
|
850
|
-
passkeys: "webauthn-proxy",
|
|
851
|
-
},
|
|
852
|
-
});
|
|
853
|
-
});
|
|
854
|
-
app.get("/api/browser-extension/vaults", (_req, res) => {
|
|
855
|
-
res.json({ vaults: storage.listPasswordVaults() });
|
|
856
|
-
});
|
|
857
|
-
app.post("/api/browser-extension/vaults", (req, res) => {
|
|
858
|
-
try {
|
|
859
|
-
const vault = storage.createPasswordVault(req.body.name);
|
|
860
|
-
res.status(201).json({ vault });
|
|
861
|
-
}
|
|
862
|
-
catch (error) {
|
|
863
|
-
res.status(400).json({ error: getErrorMessage(error, "无法创建 vault。") });
|
|
599
|
+
sendRouteError(res, error, "无法读取 skills。");
|
|
864
600
|
}
|
|
865
601
|
});
|
|
866
|
-
app
|
|
867
|
-
const filter = {
|
|
868
|
-
q: firstQueryStringValue(req.query.q),
|
|
869
|
-
url: firstQueryStringValue(req.query.url),
|
|
870
|
-
vaultId: firstQueryStringValue(req.query.vaultId),
|
|
871
|
-
type: req.query.type ? normalizePasswordItemType(firstQueryStringValue(req.query.type)) : undefined,
|
|
872
|
-
limit: req.query.limit ? Number(firstQueryStringValue(req.query.limit)) : undefined,
|
|
873
|
-
};
|
|
874
|
-
res.json({ items: storage.listPasswordItems(filter) });
|
|
875
|
-
});
|
|
876
|
-
app.post("/api/browser-extension/items", (req, res) => {
|
|
877
|
-
try {
|
|
878
|
-
const item = storage.createPasswordItem(req.body);
|
|
879
|
-
res.status(201).json({ item });
|
|
880
|
-
}
|
|
881
|
-
catch (error) {
|
|
882
|
-
res.status(400).json({ error: getErrorMessage(error, "无法保存条目。") });
|
|
883
|
-
}
|
|
884
|
-
});
|
|
885
|
-
app.get("/api/browser-extension/items/:id", (req, res) => {
|
|
886
|
-
const item = storage.getPasswordItem(req.params.id);
|
|
887
|
-
if (!item) {
|
|
888
|
-
res.status(404).json({ error: "条目不存在。" });
|
|
889
|
-
return;
|
|
890
|
-
}
|
|
891
|
-
res.json({ item });
|
|
892
|
-
});
|
|
893
|
-
app.put("/api/browser-extension/items/:id", (req, res) => {
|
|
894
|
-
try {
|
|
895
|
-
const item = storage.updatePasswordItem(req.params.id, req.body);
|
|
896
|
-
if (!item) {
|
|
897
|
-
res.status(404).json({ error: "条目不存在。" });
|
|
898
|
-
return;
|
|
899
|
-
}
|
|
900
|
-
res.json({ item });
|
|
901
|
-
}
|
|
902
|
-
catch (error) {
|
|
903
|
-
res.status(400).json({ error: getErrorMessage(error, "无法更新条目。") });
|
|
904
|
-
}
|
|
905
|
-
});
|
|
906
|
-
app.delete("/api/browser-extension/items/:id", (req, res) => {
|
|
907
|
-
if (!storage.deletePasswordItem(req.params.id)) {
|
|
908
|
-
res.status(404).json({ error: "条目不存在。" });
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
|
-
res.json({ ok: true });
|
|
912
|
-
});
|
|
913
|
-
app.post("/api/browser-extension/items/:id/use", (req, res) => {
|
|
914
|
-
const item = storage.touchPasswordItem(req.params.id);
|
|
915
|
-
if (!item) {
|
|
916
|
-
res.status(404).json({ error: "条目不存在。" });
|
|
917
|
-
return;
|
|
918
|
-
}
|
|
919
|
-
res.json({ item });
|
|
920
|
-
});
|
|
921
|
-
app.get("/api/browser-extension/generator/password", (req, res) => {
|
|
922
|
-
res.json({
|
|
923
|
-
password: generatePassword({
|
|
924
|
-
length: Number(firstQueryStringValue(req.query.length)),
|
|
925
|
-
digits: firstQueryStringValue(req.query.digits) !== "false",
|
|
926
|
-
symbols: firstQueryStringValue(req.query.symbols) !== "false",
|
|
927
|
-
}),
|
|
928
|
-
});
|
|
929
|
-
});
|
|
930
|
-
app.post("/api/browser-extension/totp/preview", (req, res) => {
|
|
931
|
-
try {
|
|
932
|
-
const { secret, digits, period } = req.body;
|
|
933
|
-
if (!secret) {
|
|
934
|
-
res.status(400).json({ error: "缺少 TOTP secret。" });
|
|
935
|
-
return;
|
|
936
|
-
}
|
|
937
|
-
res.json({
|
|
938
|
-
code: generateTotpCode(secret, Date.now(), digits ?? 6, period ?? 30),
|
|
939
|
-
period: period ?? 30,
|
|
940
|
-
});
|
|
941
|
-
}
|
|
942
|
-
catch (error) {
|
|
943
|
-
res.status(400).json({ error: getErrorMessage(error, "无法生成 TOTP。") });
|
|
944
|
-
}
|
|
945
|
-
});
|
|
946
|
-
app.get("/api/browser-extension/security-report", (_req, res) => {
|
|
947
|
-
res.json({ report: buildPasswordSecurityReport(storage.listPasswordItems({ includeArchived: false, limit: 200 })) });
|
|
948
|
-
});
|
|
602
|
+
registerVaultRoutes(app, { storage, config, useHttps });
|
|
949
603
|
// ── Settings endpoints ──
|
|
950
604
|
const getDistributionSettings = () => distributionManager.getSettings();
|
|
951
605
|
registerSettingsRoutes(app, {
|
|
@@ -963,16 +617,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
963
617
|
getDistributionSettings,
|
|
964
618
|
getGithubConnector: () => getGithubConnectorStatus(storage),
|
|
965
619
|
modelCatalog,
|
|
966
|
-
resolveAppConnectCode: (req) =>
|
|
967
|
-
const effectivePassword = getEffectivePassword(storage, config);
|
|
968
|
-
const configuredOrigin = normalizePublicOrigin(config.publicOrigin);
|
|
969
|
-
const requestProtocol = getPublicRequestProtocol(req, useHttps ? "https" : "http");
|
|
970
|
-
const requestHost = getPublicRequestHost(req, config);
|
|
971
|
-
const browserOrigin = normalizePublicOrigin(firstQueryStringValue(req.query.origin));
|
|
972
|
-
const serverUrl = resolveAppConnectOrigin(configuredOrigin ?? browserOrigin ?? `${requestProtocol}://${requestHost}`, config);
|
|
973
|
-
const token = generateAppToken(effectivePassword, config.appSecret ?? "");
|
|
974
|
-
return { code: encodeConnectCode(serverUrl, token), url: serverUrl };
|
|
975
|
-
},
|
|
620
|
+
resolveAppConnectCode: (req) => resolveAppConnectCode(req, config, useHttps, getEffectivePassword(storage, config)),
|
|
976
621
|
});
|
|
977
622
|
registerGithubRoutes(app, { storage, requireAdmin, sessions: sessionRegistry });
|
|
978
623
|
// 任务管理与 Missions 一样只需登录:原生 connected-app 也要能列/建/派发。
|
|
@@ -1042,7 +687,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
1042
687
|
res.status(status).json({ error: error.message, errorCode: error.code });
|
|
1043
688
|
return;
|
|
1044
689
|
}
|
|
1045
|
-
res
|
|
690
|
+
sendRouteError(res, error, "提示词优化失败。", 500);
|
|
1046
691
|
}
|
|
1047
692
|
}));
|
|
1048
693
|
registerFileRoutes(app, { storage, defaultCwd: config.defaultCwd });
|
|
@@ -1112,7 +757,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
1112
757
|
res.status(201).json(toSessionDetailDTO(snapshot));
|
|
1113
758
|
}
|
|
1114
759
|
catch (error) {
|
|
1115
|
-
res
|
|
760
|
+
sendRouteError(res, error, "无法启动命令。请检查命令是否安装。");
|
|
1116
761
|
}
|
|
1117
762
|
}));
|
|
1118
763
|
// ── WebSocket broadcast layer ──
|
package/dist/storage.d.ts
CHANGED
|
@@ -84,6 +84,7 @@ export declare class WandStorage {
|
|
|
84
84
|
cwd: string;
|
|
85
85
|
worktree: SessionSnapshot["worktree"];
|
|
86
86
|
}>;
|
|
87
|
+
/** 侧栏任务顺序:新创建在前。GET /api/tasks 按此顺序填充目录组。 */
|
|
87
88
|
listWorkspaceTasks(workspaceId: string): WorkspaceTask[];
|
|
88
89
|
getWorkspaceTask(id: string): WorkspaceTask | null;
|
|
89
90
|
createWorkspaceTask(input: {
|
|
@@ -92,11 +93,13 @@ export declare class WandStorage {
|
|
|
92
93
|
worktree?: WorkspaceTaskWorktree | null;
|
|
93
94
|
cwd?: string | null;
|
|
94
95
|
status?: WorkspaceTaskStatus;
|
|
96
|
+
milestoneId?: string | null;
|
|
95
97
|
}): WorkspaceTask;
|
|
96
98
|
updateWorkspaceTask(id: string, patch: {
|
|
97
99
|
name?: string;
|
|
98
100
|
status?: WorkspaceTaskStatus;
|
|
99
101
|
worktree?: WorkspaceTaskWorktree | null;
|
|
102
|
+
milestoneId?: string | null;
|
|
100
103
|
}): void;
|
|
101
104
|
saveWorkspaceTaskLayout(id: string, layout: TaskWindowLayout | null, expectedRevision?: number): {
|
|
102
105
|
ok: true;
|
|
@@ -112,6 +115,7 @@ export declare class WandStorage {
|
|
|
112
115
|
cascade?: boolean;
|
|
113
116
|
}): void;
|
|
114
117
|
tasksAggregateFingerprint(): string;
|
|
118
|
+
/** GET /api/wand-tasks 的顺序:新创建在前。客户端按此顺序渲染,不再本地排序。 */
|
|
115
119
|
listWandTasks(workspaceId?: string | null): import("./task-types.js").WandTask[];
|
|
116
120
|
private mapWandTaskRow;
|
|
117
121
|
getWandTask(id: string): import("./task-types.js").WandTask | null;
|
|
@@ -127,10 +131,25 @@ export declare class WandStorage {
|
|
|
127
131
|
priority?: import("./task-types.js").WandTaskPriority;
|
|
128
132
|
labels?: string[];
|
|
129
133
|
dueDate?: string | null;
|
|
134
|
+
milestoneId?: string | null;
|
|
130
135
|
agent?: import("./task-types.js").WandTaskAgent | null;
|
|
131
136
|
}): import("./task-types.js").WandTask;
|
|
132
|
-
updateWandTask(id: string, patch: Partial<Pick<import("./task-types.js").WandTask, "workspaceId" | "workspaceTaskId" | "title" | "titleSource" | "description" | "status" | "priority" | "labels" | "dueDate" | "sortOrder" | "agent">>): import("./task-types.js").WandTask | null;
|
|
137
|
+
updateWandTask(id: string, patch: Partial<Pick<import("./task-types.js").WandTask, "workspaceId" | "workspaceTaskId" | "title" | "titleSource" | "description" | "status" | "priority" | "labels" | "dueDate" | "milestoneId" | "sortOrder" | "agent">>): import("./task-types.js").WandTask | null;
|
|
133
138
|
deleteWandTask(id: string): void;
|
|
139
|
+
listWandMilestones(): import("./task-types.js").WandTaskMilestone[];
|
|
140
|
+
getWandMilestone(id: string): import("./task-types.js").WandTaskMilestone | null;
|
|
141
|
+
createWandMilestone(input: {
|
|
142
|
+
name: string;
|
|
143
|
+
dueDate?: string | null;
|
|
144
|
+
}): import("./task-types.js").WandTaskMilestone;
|
|
145
|
+
updateWandMilestone(id: string, patch: {
|
|
146
|
+
name?: string;
|
|
147
|
+
dueDate?: string | null;
|
|
148
|
+
}): import("./task-types.js").WandTaskMilestone | null;
|
|
149
|
+
/** 删除里程碑只解绑任务(milestone_id 置空),绝不删除任务。 */
|
|
150
|
+
deleteWandMilestone(id: string): boolean;
|
|
151
|
+
/** 各里程碑下的任务数(看板卡片),用于下拉里的数量提示。 */
|
|
152
|
+
countWandTasksByMilestone(): Record<string, number>;
|
|
134
153
|
listWandTaskSessionIds(taskId: string): string[];
|
|
135
154
|
listBoundWandTaskSessionIds(): string[];
|
|
136
155
|
bindWandTaskSession(taskId: string, sessionId: string): void;
|