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.
@@ -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 badRequest(reason) {
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 badRequest(target.reason);
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 new Response(JSON.stringify({ error: err.message }), {
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 badRequest2("email and password are required");
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 badRequest2("refreshToken is required");
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 badRequest2("email and password are required");
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 badRequest2("open is required (boolean)");
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 badRequest2("X-Wf-App-Id / X-Wf-App-Key required");
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 badRequest2("conversation input invalid");
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 badRequest2("content is required");
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 badRequest2("content is required");
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 badRequest2("message not found");
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 badRequest2("message not found");
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 badRequest2("message not found");
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
- badRequest2 as badRequest,
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。