weifuwu 0.91.0 → 0.92.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/client/components/index.d.ts +3 -0
- package/dist/client/vdom/core/protocol.d.ts +62 -7
- package/dist/client/vdom/index.js +3 -3
- package/dist/server/db/http.d.ts +4 -4
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +112 -106
- package/dist/server/response.d.ts +17 -0
- package/docs/client.md +59 -0
- package/docs/server.md +229 -9
- package/package.json +5 -3
package/dist/server/index.js
CHANGED
|
@@ -457,6 +457,101 @@ function serve(router, options) {
|
|
|
457
457
|
// src/server/core/router.ts
|
|
458
458
|
import { WebSocketServer } from "ws";
|
|
459
459
|
|
|
460
|
+
// src/server/db/errors.ts
|
|
461
|
+
var DbError = class extends Error {
|
|
462
|
+
kind;
|
|
463
|
+
code;
|
|
464
|
+
cause;
|
|
465
|
+
constructor(kind, message, options) {
|
|
466
|
+
super(message);
|
|
467
|
+
this.name = "DbError";
|
|
468
|
+
this.kind = kind;
|
|
469
|
+
this.code = options?.code;
|
|
470
|
+
this.cause = options?.cause;
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
var ProtocolError = class extends DbError {
|
|
474
|
+
constructor(feature, message) {
|
|
475
|
+
super("protocol", message ?? `${feature} is not supported by weifuwu/db`, { code: "UNSUPPORTED" });
|
|
476
|
+
this.name = "ProtocolError";
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
var ConnectionError = class extends DbError {
|
|
480
|
+
attempts;
|
|
481
|
+
constructor(message, attempts = 1, cause) {
|
|
482
|
+
super("connection", message, { cause });
|
|
483
|
+
this.name = "ConnectionError";
|
|
484
|
+
this.attempts = attempts;
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
var TimeoutError = class extends DbError {
|
|
488
|
+
operation;
|
|
489
|
+
ms;
|
|
490
|
+
constructor(operation, ms) {
|
|
491
|
+
super("timeout", `${operation} timed out after ${ms}ms`);
|
|
492
|
+
this.name = "TimeoutError";
|
|
493
|
+
this.operation = operation;
|
|
494
|
+
this.ms = ms;
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
var ValidationError = class extends DbError {
|
|
498
|
+
constructor(message) {
|
|
499
|
+
super("validation", message);
|
|
500
|
+
this.name = "ValidationError";
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
// src/server/response.ts
|
|
505
|
+
function ok(data, init) {
|
|
506
|
+
return Response.json(data, { status: 200, ...init });
|
|
507
|
+
}
|
|
508
|
+
function created(data, init) {
|
|
509
|
+
return Response.json(data, { status: 201, ...init });
|
|
510
|
+
}
|
|
511
|
+
function noContent(init) {
|
|
512
|
+
return new Response(null, { status: 204, ...init });
|
|
513
|
+
}
|
|
514
|
+
function badRequest(message) {
|
|
515
|
+
return Response.json({ error: message ?? "Bad Request" }, { status: 400 });
|
|
516
|
+
}
|
|
517
|
+
function unauthorized(message) {
|
|
518
|
+
return Response.json({ error: message ?? "Unauthorized" }, { status: 401 });
|
|
519
|
+
}
|
|
520
|
+
function forbidden(message) {
|
|
521
|
+
return Response.json({ error: message ?? "Forbidden" }, { status: 403 });
|
|
522
|
+
}
|
|
523
|
+
function notFound(message) {
|
|
524
|
+
return Response.json({ error: message ?? "Not Found" }, { status: 404 });
|
|
525
|
+
}
|
|
526
|
+
function conflict(message) {
|
|
527
|
+
return Response.json({ error: message ?? "Conflict" }, { status: 409 });
|
|
528
|
+
}
|
|
529
|
+
function unprocessable(message) {
|
|
530
|
+
return Response.json({ error: message ?? "Unprocessable Entity" }, { status: 422 });
|
|
531
|
+
}
|
|
532
|
+
function tooManyRequests(message) {
|
|
533
|
+
return Response.json({ error: message ?? "Too Many Requests" }, { status: 429 });
|
|
534
|
+
}
|
|
535
|
+
function serverError(message) {
|
|
536
|
+
return Response.json({ error: message ?? "Internal Server Error" }, { status: 500 });
|
|
537
|
+
}
|
|
538
|
+
function redirect(url, status = 302) {
|
|
539
|
+
return new Response(null, { status, headers: { Location: url } });
|
|
540
|
+
}
|
|
541
|
+
function errorResponse(e, status) {
|
|
542
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
543
|
+
let code;
|
|
544
|
+
if (e instanceof HttpError) {
|
|
545
|
+
return Response.json({ error: msg }, { status: status ?? e.status });
|
|
546
|
+
}
|
|
547
|
+
if (e instanceof DbError) {
|
|
548
|
+
const st = status ?? (e.code === "23505" ? 409 : 400);
|
|
549
|
+
code = e.code === "23505" ? "conflict" : e.kind;
|
|
550
|
+
return Response.json({ error: msg, code }, { status: st });
|
|
551
|
+
}
|
|
552
|
+
return Response.json({ error: msg }, { status: status ?? 400 });
|
|
553
|
+
}
|
|
554
|
+
|
|
460
555
|
// src/server/core/ws.ts
|
|
461
556
|
function safeHook(hookName, fn, onError) {
|
|
462
557
|
if (!fn) return () => {
|
|
@@ -995,12 +1090,12 @@ async function runChain(mws, finalHandler, req, ctx) {
|
|
|
995
1090
|
}
|
|
996
1091
|
|
|
997
1092
|
// src/shared/router/pipeline.ts
|
|
998
|
-
function
|
|
1093
|
+
function badRequest2(reason) {
|
|
999
1094
|
return Response.json({ error: "Bad Request", reason }, { status: 400 });
|
|
1000
1095
|
}
|
|
1001
1096
|
async function dispatchRouter(root, pipeline, req, ctx) {
|
|
1002
1097
|
const target = parseRequestTarget(req);
|
|
1003
|
-
if (!target.ok) return
|
|
1098
|
+
if (!target.ok) return badRequest2(target.reason);
|
|
1004
1099
|
const m = trieMatch(root, target.segments);
|
|
1005
1100
|
const match = m ? { value: m.value, params: freshParams(m), wildcard: m.wildcard } : null;
|
|
1006
1101
|
pipeline.enrichCtx?.(ctx, match, target.pathname, target.query);
|
|
@@ -1356,11 +1451,8 @@ var Router = class _Router {
|
|
|
1356
1451
|
async handleError(e, req, ctx, path) {
|
|
1357
1452
|
const err = e instanceof Error ? e : new Error(String(e));
|
|
1358
1453
|
if (this.errorHandler) return this.errorHandler(err, req, ctx);
|
|
1359
|
-
if (err instanceof HttpError) {
|
|
1360
|
-
return
|
|
1361
|
-
status: err.status,
|
|
1362
|
-
headers: { "Content-Type": "application/json" }
|
|
1363
|
-
});
|
|
1454
|
+
if (err instanceof HttpError || err instanceof DbError || err instanceof ValidationError) {
|
|
1455
|
+
return errorResponse(err);
|
|
1364
1456
|
}
|
|
1365
1457
|
noteHandlerError(`${req.method} ${path ?? new URL(req.url).pathname}`, e);
|
|
1366
1458
|
return Response.json({ error: "Internal Server Error" }, { status: 500 });
|
|
@@ -2521,44 +2613,6 @@ function hashRefreshToken(token) {
|
|
|
2521
2613
|
return createHash("sha256").update(token).digest("hex");
|
|
2522
2614
|
}
|
|
2523
2615
|
|
|
2524
|
-
// src/server/response.ts
|
|
2525
|
-
function ok(data, init) {
|
|
2526
|
-
return Response.json(data, { status: 200, ...init });
|
|
2527
|
-
}
|
|
2528
|
-
function created(data, init) {
|
|
2529
|
-
return Response.json(data, { status: 201, ...init });
|
|
2530
|
-
}
|
|
2531
|
-
function noContent(init) {
|
|
2532
|
-
return new Response(null, { status: 204, ...init });
|
|
2533
|
-
}
|
|
2534
|
-
function badRequest2(message) {
|
|
2535
|
-
return Response.json({ error: message ?? "Bad Request" }, { status: 400 });
|
|
2536
|
-
}
|
|
2537
|
-
function unauthorized(message) {
|
|
2538
|
-
return Response.json({ error: message ?? "Unauthorized" }, { status: 401 });
|
|
2539
|
-
}
|
|
2540
|
-
function forbidden(message) {
|
|
2541
|
-
return Response.json({ error: message ?? "Forbidden" }, { status: 403 });
|
|
2542
|
-
}
|
|
2543
|
-
function notFound(message) {
|
|
2544
|
-
return Response.json({ error: message ?? "Not Found" }, { status: 404 });
|
|
2545
|
-
}
|
|
2546
|
-
function conflict(message) {
|
|
2547
|
-
return Response.json({ error: message ?? "Conflict" }, { status: 409 });
|
|
2548
|
-
}
|
|
2549
|
-
function unprocessable(message) {
|
|
2550
|
-
return Response.json({ error: message ?? "Unprocessable Entity" }, { status: 422 });
|
|
2551
|
-
}
|
|
2552
|
-
function tooManyRequests(message) {
|
|
2553
|
-
return Response.json({ error: message ?? "Too Many Requests" }, { status: 429 });
|
|
2554
|
-
}
|
|
2555
|
-
function serverError(message) {
|
|
2556
|
-
return Response.json({ error: message ?? "Internal Server Error" }, { status: 500 });
|
|
2557
|
-
}
|
|
2558
|
-
function redirect(url, status = 302) {
|
|
2559
|
-
return new Response(null, { status, headers: { Location: url } });
|
|
2560
|
-
}
|
|
2561
|
-
|
|
2562
2616
|
// src/server/user/index.ts
|
|
2563
2617
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
2564
2618
|
var USER_TABLES = {
|
|
@@ -3116,13 +3170,13 @@ function userSystem(options) {
|
|
|
3116
3170
|
});
|
|
3117
3171
|
if (!excluded.has("login")) app.post(`${p}/login`, async (req, ctx) => {
|
|
3118
3172
|
const body = await req.json().catch(() => ({}));
|
|
3119
|
-
if (!body.email || !body.password) return
|
|
3173
|
+
if (!body.email || !body.password) return badRequest("email and password are required");
|
|
3120
3174
|
const result = await ctx.auth.login(body.email, body.password);
|
|
3121
3175
|
return ok(result);
|
|
3122
3176
|
});
|
|
3123
3177
|
if (!excluded.has("logout")) app.post(`${p}/logout`, async (req, ctx) => {
|
|
3124
3178
|
const body = await req.json().catch(() => ({}));
|
|
3125
|
-
if (!body.refreshToken) return
|
|
3179
|
+
if (!body.refreshToken) return badRequest("refreshToken is required");
|
|
3126
3180
|
await ctx.auth.logout(body.refreshToken);
|
|
3127
3181
|
return noContent();
|
|
3128
3182
|
});
|
|
@@ -3224,13 +3278,13 @@ function userSystem(options) {
|
|
|
3224
3278
|
});
|
|
3225
3279
|
app.post(`${p}/apps/:appSlug/auth/login`, async (req, ctx) => {
|
|
3226
3280
|
const body = await req.json().catch(() => ({}));
|
|
3227
|
-
if (!body.email || !body.password) return
|
|
3281
|
+
if (!body.email || !body.password) return badRequest("email and password are required");
|
|
3228
3282
|
const result = await ctx.auth.loginApp(ctx.params.appSlug, body.email, body.password);
|
|
3229
3283
|
return ok(result);
|
|
3230
3284
|
});
|
|
3231
3285
|
app.patch(`${p}/apps/:appSlug/auth/registration`, async (req, ctx) => {
|
|
3232
3286
|
const body = await req.json().catch(() => ({}));
|
|
3233
|
-
if (typeof body.open !== "boolean") return
|
|
3287
|
+
if (typeof body.open !== "boolean") return badRequest("open is required (boolean)");
|
|
3234
3288
|
const appId = await findAppIdBySlug(ctx.params.appSlug);
|
|
3235
3289
|
if (!appId) throw new HttpError("Application not found", 404);
|
|
3236
3290
|
await ctx.auth.setOpenRegistration(appId, body.open);
|
|
@@ -3239,7 +3293,7 @@ function userSystem(options) {
|
|
|
3239
3293
|
app.post(`${p}/apps/:appSlug/auth/verify`, async (req, ctx) => {
|
|
3240
3294
|
const appId = req.headers.get("x-wf-app-id");
|
|
3241
3295
|
const appKey = req.headers.get("x-wf-app-key");
|
|
3242
|
-
if (!appId || !appKey) return
|
|
3296
|
+
if (!appId || !appKey) return badRequest("X-Wf-App-Id / X-Wf-App-Key required");
|
|
3243
3297
|
const rows = await A.select("id", "slug", "name").where({ id: { eq: appId }, app_key: { eq: appKey } }).run();
|
|
3244
3298
|
if (!rows.length) throw new HttpError("Invalid app credentials", 403);
|
|
3245
3299
|
const a = rows[0];
|
|
@@ -3390,50 +3444,6 @@ function appAuth(options) {
|
|
|
3390
3444
|
return mw;
|
|
3391
3445
|
}
|
|
3392
3446
|
|
|
3393
|
-
// src/server/db/errors.ts
|
|
3394
|
-
var DbError = class extends Error {
|
|
3395
|
-
kind;
|
|
3396
|
-
code;
|
|
3397
|
-
cause;
|
|
3398
|
-
constructor(kind, message, options) {
|
|
3399
|
-
super(message);
|
|
3400
|
-
this.name = "DbError";
|
|
3401
|
-
this.kind = kind;
|
|
3402
|
-
this.code = options?.code;
|
|
3403
|
-
this.cause = options?.cause;
|
|
3404
|
-
}
|
|
3405
|
-
};
|
|
3406
|
-
var ProtocolError = class extends DbError {
|
|
3407
|
-
constructor(feature, message) {
|
|
3408
|
-
super("protocol", message ?? `${feature} is not supported by weifuwu/db`, { code: "UNSUPPORTED" });
|
|
3409
|
-
this.name = "ProtocolError";
|
|
3410
|
-
}
|
|
3411
|
-
};
|
|
3412
|
-
var ConnectionError = class extends DbError {
|
|
3413
|
-
attempts;
|
|
3414
|
-
constructor(message, attempts = 1, cause) {
|
|
3415
|
-
super("connection", message, { cause });
|
|
3416
|
-
this.name = "ConnectionError";
|
|
3417
|
-
this.attempts = attempts;
|
|
3418
|
-
}
|
|
3419
|
-
};
|
|
3420
|
-
var TimeoutError = class extends DbError {
|
|
3421
|
-
operation;
|
|
3422
|
-
ms;
|
|
3423
|
-
constructor(operation, ms) {
|
|
3424
|
-
super("timeout", `${operation} timed out after ${ms}ms`);
|
|
3425
|
-
this.name = "TimeoutError";
|
|
3426
|
-
this.operation = operation;
|
|
3427
|
-
this.ms = ms;
|
|
3428
|
-
}
|
|
3429
|
-
};
|
|
3430
|
-
var ValidationError = class extends DbError {
|
|
3431
|
-
constructor(message) {
|
|
3432
|
-
super("validation", message);
|
|
3433
|
-
this.name = "ValidationError";
|
|
3434
|
-
}
|
|
3435
|
-
};
|
|
3436
|
-
|
|
3437
3447
|
// src/server/db/body.ts
|
|
3438
3448
|
function shapeOf(s) {
|
|
3439
3449
|
const sh = s.__shape;
|
|
@@ -3510,11 +3520,6 @@ function listQuery(url, shapeDef, opts = {}) {
|
|
|
3510
3520
|
const offset = Math.max(0, parseInt(search.get("offset") ?? "0", 10) || 0);
|
|
3511
3521
|
return { filter: filter2, sort, limit, offset };
|
|
3512
3522
|
}
|
|
3513
|
-
function errorResponse(e, status) {
|
|
3514
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
3515
|
-
const st = status ?? (e instanceof DbError ? e.code === "23505" ? 409 : 400 : 400);
|
|
3516
|
-
return Response.json({ error: msg }, { status: st });
|
|
3517
|
-
}
|
|
3518
3523
|
|
|
3519
3524
|
// src/server/db/schema.ts
|
|
3520
3525
|
function innerOf(t) {
|
|
@@ -5462,7 +5467,7 @@ function messager(options) {
|
|
|
5462
5467
|
const conv = await createConversation(ctx.user.id, { type: "group", memberIds: body.memberIds });
|
|
5463
5468
|
return created(conv);
|
|
5464
5469
|
}
|
|
5465
|
-
return
|
|
5470
|
+
return badRequest("conversation input invalid");
|
|
5466
5471
|
});
|
|
5467
5472
|
app.get(`${p}/conversations`, async (_req, ctx) => {
|
|
5468
5473
|
if (!ctx.user) throw new HttpError("Unauthorized", 401);
|
|
@@ -5482,7 +5487,7 @@ function messager(options) {
|
|
|
5482
5487
|
const convId = ctx.params.id;
|
|
5483
5488
|
await requireMember(convId, ctx);
|
|
5484
5489
|
const body = await req.json().catch(() => ({}));
|
|
5485
|
-
if (!body.content?.trim()) return
|
|
5490
|
+
if (!body.content?.trim()) return badRequest("content is required");
|
|
5486
5491
|
const message = await sendMessage(convId, {
|
|
5487
5492
|
senderType: "user",
|
|
5488
5493
|
senderId: ctx.user.id,
|
|
@@ -5503,15 +5508,15 @@ function messager(options) {
|
|
|
5503
5508
|
if (!ctx.user) throw new HttpError("Unauthorized", 401);
|
|
5504
5509
|
const messageId = ctx.params.id;
|
|
5505
5510
|
const body = await req.json().catch(() => ({}));
|
|
5506
|
-
if (!body.content?.trim()) return
|
|
5511
|
+
if (!body.content?.trim()) return badRequest("content is required");
|
|
5507
5512
|
const rows = await orm.query.from(`${MESSAGES} m`).join(`${MEMBERS} mem`, { "mem.conversation_id": { col: "m.conversation_id" } }).where({ "m.id": { eq: messageId }, "mem.user_id": { eq: String(ctx.user.id) } }).select("m.conversation_id", "m.sender_type", "m.sender_id").run();
|
|
5508
|
-
if (!rows.length) return
|
|
5513
|
+
if (!rows.length) return badRequest("message not found");
|
|
5509
5514
|
const row = rows[0];
|
|
5510
5515
|
if (row.sender_type === "user" && row.sender_id !== ctx.user.id) {
|
|
5511
5516
|
throw new HttpError("Forbidden: not your message", 403);
|
|
5512
5517
|
}
|
|
5513
5518
|
const edited = await editMessage(messageId, body.content);
|
|
5514
|
-
if (!edited) return
|
|
5519
|
+
if (!edited) return badRequest("message not found");
|
|
5515
5520
|
broadcast(`conv:${edited.conversation_id}`, { type: "message_edited", message: edited });
|
|
5516
5521
|
return ok(edited);
|
|
5517
5522
|
});
|
|
@@ -5519,7 +5524,7 @@ function messager(options) {
|
|
|
5519
5524
|
if (!ctx.user) throw new HttpError("Unauthorized", 401);
|
|
5520
5525
|
const messageId = ctx.params.id;
|
|
5521
5526
|
const rows = await orm.query.from(`${MESSAGES} m`).join(`${MEMBERS} mem`, { "mem.conversation_id": { col: "m.conversation_id" } }).where({ "m.id": { eq: messageId }, "mem.user_id": { eq: String(ctx.user.id) } }).select("m.conversation_id", "m.sender_type", "m.sender_id").run();
|
|
5522
|
-
if (!rows.length) return
|
|
5527
|
+
if (!rows.length) return badRequest("message not found");
|
|
5523
5528
|
const row = rows[0];
|
|
5524
5529
|
if (row.sender_type === "user" && row.sender_id !== ctx.user.id) {
|
|
5525
5530
|
throw new HttpError("Forbidden: not your message", 403);
|
|
@@ -8230,6 +8235,7 @@ function createScheduler(orm, execute, opts = {}) {
|
|
|
8230
8235
|
start: () => {
|
|
8231
8236
|
if (timer) return;
|
|
8232
8237
|
timer = setInterval(() => void tick(), intervalMs);
|
|
8238
|
+
timer.unref?.();
|
|
8233
8239
|
void tick();
|
|
8234
8240
|
},
|
|
8235
8241
|
stop: () => {
|
|
@@ -18729,7 +18735,7 @@ export {
|
|
|
18729
18735
|
WEIFUWU_WORKFLOW_SCHEMA,
|
|
18730
18736
|
and,
|
|
18731
18737
|
appAuth,
|
|
18732
|
-
|
|
18738
|
+
badRequest,
|
|
18733
18739
|
between,
|
|
18734
18740
|
bodyOf,
|
|
18735
18741
|
buildQuery,
|
|
@@ -44,3 +44,20 @@ export declare function serverError(message?: string): Response;
|
|
|
44
44
|
* ```
|
|
45
45
|
*/
|
|
46
46
|
export declare function redirect(url: string, status?: number): Response;
|
|
47
|
+
/**
|
|
48
|
+
* 错误 → Response(总错误面——HTTP 链(router.onError/默认链)与 route 内
|
|
49
|
+
* catch 同语义——DbError/HttpError 映射单源)。
|
|
50
|
+
*
|
|
51
|
+
* 双层语义(有意分层——文档契约):
|
|
52
|
+
* - **链面(未捕获)**:普通 Error → 500(Internal Server Error——意外错误
|
|
53
|
+
* 诚实现形不泄漏——router.handleError 默认链消费);DbError/ValidationError
|
|
54
|
+
* → 400/409(orm 错误不该 500)
|
|
55
|
+
* - **route 内 catch(已知业务)**:普通 Error → 400(业务守卫/校验——
|
|
56
|
+
* 「我知道这是什么错」→ errorResponse 显式调用)
|
|
57
|
+
* - HttpError:双面一致(status 权威——403/404/409 同输出)
|
|
58
|
+
*
|
|
59
|
+
* code 面(`{ error, code }`——前端可 switch 而非解析 message 字符串):
|
|
60
|
+
* ValidationError → 'validation' · DbError 23505 → 'conflict' ·
|
|
61
|
+
* DbError.kind → kind · HttpError/普通 Error → 无 code(status 已是语义)。
|
|
62
|
+
*/
|
|
63
|
+
export declare function errorResponse(e: unknown, status?: number): Response;
|
package/docs/client.md
CHANGED
|
@@ -89,6 +89,10 @@ app.router(Router())
|
|
|
89
89
|
回调/类函数属性差异**不触发**(比较器自有豁免权——如 CodeEditor 忽略 onChange
|
|
90
90
|
闭包引用)。默认不挂 = 行为完全不变。典型:CodeEditor(value/lang/rows/readOnly
|
|
91
91
|
比较——高频输入页面零 diff 扰动)
|
|
92
|
+
**页面级范式(web W3 试点——AgentGrid 先例)**:列表区独立成组件 +
|
|
93
|
+
`shouldRender` 比较**数据引用**(`agents` 数组未变 = 输入键击不重渲列表段——
|
|
94
|
+
高频输入页的「静态邻居」零 diff);回调经闭包(onDm/onRemove 豁免——
|
|
95
|
+
CodeEditor 同款);单消费者页内组件(不违反「>1 消费者入库」——判负登记)
|
|
92
96
|
|
|
93
97
|
### 5.2 三件套
|
|
94
98
|
|
|
@@ -142,6 +146,27 @@ style-audit 文件数基线 +1 · 覆盖哨兵跑绿(`scripts/audit-component-
|
|
|
142
146
|
R-03:类名/结构变更**反查测试选择器**(`[class*="..."]` 定位器)·
|
|
143
147
|
行为变化**必带契约测试** · 机制化优先(能进审计不靠记忆)
|
|
144
148
|
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### 6.1 认证接线范式(fullstack W4——onRefresh 时序契约)
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
// uiServe options(v3-main.tsx)——auth 客户端接线(token 双源防线)
|
|
155
|
+
auth: auth({ storage, onAuth: (c) => {...}, onRefresh: async () => {
|
|
156
|
+
// onRefresh 闭包直引(2027-09 教训:间接层 authRef 曾致 401 死循环——
|
|
157
|
+
// refresh 后 token 未更新——闭包直引 source 是唯一的)
|
|
158
|
+
const ok = await api.post('/api/auth/refresh', undefined, { skipAuth: true })
|
|
159
|
+
...
|
|
160
|
+
}})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- **时序契约**:onRefresh 返回 true 语义 = token 已重写(storage 就绪)——
|
|
164
|
+
false/异常 = 401 踢登录——**未配置 onRefresh → refresh() 恒 false + warn**
|
|
165
|
+
(机制化防线——401 时 console.warn 提示「未接线」——不再静默)
|
|
166
|
+
- 401 → refresh 重试一次(ApiClient 层)→ 仍失败清 token 跳登录(兜底)
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
145
170
|
## 6. 前端架构导论
|
|
146
171
|
|
|
147
172
|
```
|
|
@@ -172,5 +197,39 @@ R-03:类名/结构变更**反查测试选择器**(`[class*="..."]` 定位器
|
|
|
172
197
|
|
|
173
198
|
---
|
|
174
199
|
|
|
200
|
+
### 7.1 页面数据世代(web W1——useAsyncData 首选手册)
|
|
201
|
+
|
|
202
|
+
**为什么迁(v2 段复用实录)**:老世代 `load() + ctx.render()` 在工厂期异步启动
|
|
203
|
+
——v2 段复用语义下**工厂不重跑 → 数据永不刷新**(导航返回/同会话停滞——
|
|
204
|
+
Templates 迁移先例)。**getter 快照 bug(Agents W1 实录)**:`const agents =
|
|
205
|
+
getAgents() ?? []` 在**工厂期**解构 = 渲染用旧快照(state$ 更新后不显示)——
|
|
206
|
+
**必须在 renderFn 内读**(getter 纪律的页面级体现)。
|
|
207
|
+
|
|
208
|
+
**怎么迁(5 行范本)**:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
export const Agents: Component = (_p, ctx) => {
|
|
212
|
+
const [getAgents, reloadAgents] = ctx.ui.useAsyncData(async () => {
|
|
213
|
+
return (await ctx.api.get(`/api/agents`)).agents ?? []
|
|
214
|
+
}, 'agents-page') // 同 key 并发合并 · key 稳定(页面级)
|
|
215
|
+
let qValue = '' // 搜索闭包(getter 纪律)
|
|
216
|
+
return () => {
|
|
217
|
+
const agents = getAgents() ?? [] // 渲染期读最新(不是工厂期!)
|
|
218
|
+
const loading = getAgents() === null // null = loading 态
|
|
219
|
+
return h('ul', {}, agents.map(...))
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**迁移后得到什么**:① 同 key 并发合并(多次挂载 1 次请求)② 竞态取消
|
|
225
|
+
(switchMap——快速搜索乱序响应丢弃)③ 缓存保留(重挂载零请求——导航返回
|
|
226
|
+
瞬时)④ reload 显式刷新(删除/变更后服务器权威)。搜索面:q 闭包 +
|
|
227
|
+
debounce(reload)——触发读最新 q。
|
|
228
|
+
|
|
229
|
+
**哨兵**:`apps/agent-platform/scripts/audit-page-generations.mjs`(async 工厂
|
|
230
|
+
= 红 exit 1——工厂同步契约;老世代标记 = 黄报——迁移进度可见)。
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
175
234
|
> **运行**:`npm run test:client`(428 契约)· `npm run test:scenario`(123 场景)·
|
|
176
235
|
> `npm run test:showcase`(324 组件测试)——全量防线见 AGENTS.md §1。
|