dsh-task-ledger 0.0.113 → 0.0.114

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.
Files changed (43) hide show
  1. package/dist/client.js +50739 -1529
  2. package/dist/client.js.map +1 -1
  3. package/dist/host/errors.d.ts +2 -2
  4. package/dist/host/repositories/cache.d.ts +2 -3
  5. package/dist/host/repositories/cache.js +1 -2
  6. package/dist/host/repositories/identity.repository.d.ts +8 -2
  7. package/dist/host/repositories/identity.repository.js +9 -29
  8. package/dist/host/repositories/ledger.repository.d.ts +12 -22
  9. package/dist/host/repositories/ledger.repository.js +39 -58
  10. package/dist/host/repositories/records.d.ts +1 -11
  11. package/dist/host/repositories/records.js +16 -42
  12. package/dist/host/repositories/runner.d.ts +0 -30
  13. package/dist/host/repositories/runner.js +0 -23
  14. package/dist/host/repositories/stats.repository.d.ts +36 -0
  15. package/dist/host/repositories/stats.repository.js +86 -0
  16. package/dist/host/routes.d.ts +4 -25
  17. package/dist/host/routes.js +7 -384
  18. package/dist/host/services/dates.d.ts +3 -24
  19. package/dist/host/services/dates.js +3 -63
  20. package/dist/host/services/stats.service.d.ts +7 -0
  21. package/dist/host/services/stats.service.js +134 -0
  22. package/dist/host/skill/registration.d.ts +0 -3
  23. package/dist/host/skill/registration.js +0 -3
  24. package/dist/host/types.d.ts +70 -126
  25. package/dist/host/workspace.d.ts +3 -4
  26. package/dist/host/workspace.js +5 -5
  27. package/dist/index.js +2 -33
  28. package/package.json +2 -2
  29. package/skill/SKILL.md +1 -0
  30. package/dist/host/board.d.ts +0 -20
  31. package/dist/host/board.js +0 -29
  32. package/dist/host/services/history.service.d.ts +0 -27
  33. package/dist/host/services/history.service.js +0 -72
  34. package/dist/host/services/home.service.d.ts +0 -23
  35. package/dist/host/services/home.service.js +0 -91
  36. package/dist/host/services/ledger-write.service.d.ts +0 -10
  37. package/dist/host/services/ledger-write.service.js +0 -32
  38. package/dist/host/services/session.service.d.ts +0 -110
  39. package/dist/host/services/session.service.js +0 -155
  40. package/dist/host/services/trend.service.d.ts +0 -36
  41. package/dist/host/services/trend.service.js +0 -162
  42. package/dist/host/services/uuid.d.ts +0 -11
  43. package/dist/host/services/uuid.js +0 -15
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * 数据面路由薄壳:只做协议(参数解析 + JSON 响应 + 错误转 500),
3
- * 一行业务逻辑都不多写——全部调 board / ledger-read 的方法(2026-10-17 定稿)。
3
+ * 一行业务逻辑都不多写——全部调 service(2026-10-17 定稿)。
4
4
  * 宿主事实(handler = 原始 Node req/res、发头后不抛错、重复 path 注册直接 throw)
5
5
  * 见 external/host-webserver.md。
6
+ *
7
+ * 2026-10-28 收敛:界面只有 8 个统计卡 + 在办列表 ⇒ 只注册 `/workspaces` 与 `/board/snapshot`。
8
+ * 详情 / 历史 / 筛选项 / 趋势 / 写动作的端点与实现已删除(要接回按 skill 方法重写,不复用旧直读逻辑)。
6
9
  */
7
10
  import type { IncomingMessage, ServerResponse } from "node:http";
8
- import { type AgentsLike } from "./services/session.service.js";
9
11
  import { type RegistryWorkspace } from "./workspace.js";
10
12
  /** 宿主 webServer.register 的最小面(WebRoute 形状见 external/host-webserver.md)。 */
11
13
  export interface WebServerLike {
@@ -23,26 +25,3 @@ export interface RouteDeps {
23
25
  readonly registry: WorkspaceRegistryLike;
24
26
  }
25
27
  export declare function registerRoutes(webServer: WebServerLike, deps: RouteDeps): void;
26
- /**
27
- * 注册认领 / 释放端点(2026-10-18 B 混合制:没有判断量的动作由 host 直跑脚本)。
28
- *
29
- * 这两条**会改磁盘上的台账文件**,所以和 `/close` 一样必须过同源守卫;
30
- * 成功后就地清该工作区读缓存,避免按钮点完还看到上一屏的 owner。
31
- */
32
- export declare function registerWriteRoutes(webServer: WebServerLike, deps: RouteDeps): void;
33
- export interface SessionRouteDeps {
34
- readonly registry: WorkspaceRegistryLike;
35
- /** 起会话的服务面;宿主缺就回 503 人话(端点照常注册,前端不用按版本分支)。 */
36
- readonly agents: AgentsLike | undefined;
37
- /** 惰性服务读取:`agentPresets` / `agentDefaultModel` 可能整条不存在,或被 ctx 的 inject 门挡住。 */
38
- readonly getService: (name: string) => unknown;
39
- }
40
- /**
41
- * 注册「结束任务」端点:`POST /close`。
42
- *
43
- * 为什么只有这一条走会话(2026-10-18 用户拍板):认领 / 释放没有判断量,host 直写即可;
44
- * **结束**要带用户留言、要模型读过程文档后写真摘要 —— 只能交给会话里的 Agent 做。
45
- *
46
- * 请求体:`{ ws, id, message?, provider?, model? }`;响应:`{ sessionId, warning? }`。
47
- */
48
- export declare function registerSessionRoutes(webServer: WebServerLike, deps: SessionRouteDeps): void;
@@ -1,10 +1,5 @@
1
- import { getFilterOptions, getHomeAggregate, queryClosed } from "./board.js";
2
1
  import { invalidate } from "./repositories/cache.js";
3
- import { getTask, readTasks, readWorklog, } from "./repositories/ledger.repository.js";
4
- import { getSnapshot } from "./services/home.service.js";
5
- import { claimTask, releaseTask } from "./services/ledger-write.service.js";
6
- import { HandoffError, startCloseSession, } from "./services/session.service.js";
7
- import { getTrend } from "./services/trend.service.js";
2
+ import { getBoardHome } from "./services/stats.service.js";
8
3
  import { resolveWorkspaces } from "./workspace.js";
9
4
  /** API 前缀(唯一真源就在这一处)。 */
10
5
  const API_PREFIX = "/api/task-ledger";
@@ -12,75 +7,14 @@ function sendJson(res, status, body) {
12
7
  res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
13
8
  res.end(JSON.stringify(body));
14
9
  }
15
- /** 参数用错(不是台账坏):回 400,不落到 500,也不下发给脚本去撞。 */
16
- class UsageError extends Error {
17
- }
18
- /** 身份形态(与脚本 `core/identity.UUID_RE` 同一口径)。 */
19
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
20
- /**
21
- * URL query → LedgerFilter(只认白名单键;limit/months 非正数忽略)。
22
- *
23
- * 两条硬口径:
24
- * - `owner` **只收 uuid**,传名字直接 400 —— 名字筛走 `owner_name=<片段>`(脚本同一条规则);
25
- * - `months` 与 `since/until` 互斥:给了 `months` 就忽略后两者(脚本会 Fail,别让 UI 一次踩两个)。
26
- */
27
- function filterFromQuery(url) {
28
- const filter = {};
29
- const str = (key) => {
30
- const value = url.searchParams.get(key);
31
- return value !== null && value !== "" ? value : null;
32
- };
33
- const int = (key) => {
34
- const raw = str(key);
35
- if (raw === null)
36
- return undefined;
37
- const value = Number.parseInt(raw, 10);
38
- return Number.isFinite(value) && value > 0 ? value : undefined;
39
- };
40
- const status = str("status");
41
- if (status !== null)
42
- filter.status = status;
43
- const owner = str("owner");
44
- if (owner !== null) {
45
- const key = owner.trim();
46
- if (!UUID_RE.test(key))
47
- throw new UsageError("owner 只收身份 uuid;按显示名模糊筛请用 owner_name=<片段>");
48
- filter.owner = key;
49
- }
50
- const ownerName = str("owner_name");
51
- if (ownerName !== null)
52
- filter.ownerName = ownerName;
53
- const category = str("category");
54
- if (category !== null)
55
- filter.category = category;
56
- const tag = str("tag");
57
- if (tag !== null)
58
- filter.tag = tag;
59
- const limit = int("limit");
60
- if (limit !== undefined)
61
- filter.limit = limit;
62
- const months = int("months");
63
- if (months !== undefined) {
64
- filter.months = months;
65
- }
66
- else {
67
- const since = str("since");
68
- if (since !== null)
69
- filter.since = since;
70
- const until = str("until");
71
- if (until !== null)
72
- filter.until = until;
73
- }
74
- return filter;
75
- }
76
- /** 每请求重新解析工作区(registry 可能刚变;探测走 hasLedger(),微秒级,不缓存)。 */
10
+ /** 每请求重新解析工作区(registry 可能刚变;`root` 探测是本地结构判定,微秒级)。 */
77
11
  function workspacesOf(deps) {
78
12
  return resolveWorkspaces(deps.registry.list());
79
13
  }
80
14
  export function registerRoutes(webServer, deps) {
81
15
  /**
82
16
  * 注册一个**读**端点:统一先看 `?fresh=1`(手动刷新 = 清缓存再算,别让用户按了刷新还看旧数)。
83
- * 缓存 TTL 只有 1.5s,正常浏览照样吃缓存;写端点将来自己走写路径 + `invalidate()`,不走这里。
17
+ * 缓存 TTL 只有 1.5s,正常浏览照样吃缓存。
84
18
  */
85
19
  const registerRead = (path, handler) => {
86
20
  webServer.register({
@@ -93,19 +27,9 @@ export function registerRoutes(webServer, deps) {
93
27
  },
94
28
  });
95
29
  };
96
- registerRead("/workspaces", (_req, res) => {
97
- sendJson(res, 200, { workspaces: workspacesOf(deps) });
98
- });
99
- // 首页一次拿全:在办快照 + 近 8 周趋势 + 四个自然周期计数(host 每个工作区只读一次)。
100
- registerRead("/board", async (_req, res) => {
30
+ registerRead("/workspaces", async (_req, res) => {
101
31
  try {
102
- const home = await getHomeAggregate(workspacesOf(deps));
103
- sendJson(res, 200, {
104
- ...home.snapshot,
105
- closed: home.closed,
106
- trend: home.trend,
107
- degraded: home.degraded,
108
- });
32
+ sendJson(res, 200, { workspaces: await workspacesOf(deps) });
109
33
  }
110
34
  catch (error) {
111
35
  sendJson(res, 500, {
@@ -113,69 +37,10 @@ export function registerRoutes(webServer, deps) {
113
37
  });
114
38
  }
115
39
  });
116
- // ── 拆端点(2026-10-18 步骤 4a:纯加法,旧 /board 全量原样保留)──
117
- // 快路径:首屏。只读 progress.json + 本机身份,**不碰 history** ⇒ 耗时与历史总量解耦
118
- // (20 万条量级实测 527ms vs 合并端点 4.4s)。
40
+ // 首屏一次拿全:8 卡(skill `stats` + 两次 `history --count-only`)+ 在办列表(`list --json`)。
119
41
  registerRead("/board/snapshot", async (_req, res) => {
120
42
  try {
121
- const result = await getSnapshot(workspacesOf(deps));
122
- sendJson(res, 200, { ...result.snapshot, degraded: result.degraded });
123
- }
124
- catch (error) {
125
- sendJson(res, 500, {
126
- error: error instanceof Error ? error.message : String(error),
127
- });
128
- }
129
- });
130
- // 慢路径:趋势(近 8 周 + 四个自然周期)。读 history ⇒ 异步/按需拉,首屏不等它。
131
- registerRead("/board/trend", async (_req, res) => {
132
- try {
133
- const result = await getTrend(workspacesOf(deps));
134
- sendJson(res, 200, {
135
- closed: result.closed,
136
- trend: result.trend,
137
- degraded: result.degraded,
138
- warnings: result.warnings,
139
- });
140
- }
141
- catch (error) {
142
- sendJson(res, 500, {
143
- error: error instanceof Error ? error.message : String(error),
144
- });
145
- }
146
- });
147
- registerRead("/tasks", async (req, res) => {
148
- try {
149
- const filter = filterFromQuery(new URL(req.url ?? "/", "http://localhost"));
150
- const perWorkspace = await Promise.all(workspacesOf(deps).map((workspace) => readTasks(workspace, filter)));
151
- sendJson(res, 200, { tasks: perWorkspace.flat() });
152
- }
153
- catch (error) {
154
- if (error instanceof UsageError) {
155
- sendJson(res, 400, { error: error.message });
156
- return;
157
- }
158
- sendJson(res, 500, {
159
- error: error instanceof Error ? error.message : String(error),
160
- });
161
- }
162
- });
163
- // 任务详情 + 过程文档正文(query 传 ws/id,不做路径段解析——任务号含中文和连字符)。
164
- registerRead("/task", async (req, res) => {
165
- try {
166
- const url = new URL(req.url ?? "/", "http://localhost");
167
- const workspace = workspacesOf(deps).find((item) => item.id === url.searchParams.get("ws"));
168
- const taskId = url.searchParams.get("id");
169
- if (workspace === undefined || taskId === null) {
170
- sendJson(res, 400, { error: "缺少 ws / id 参数" });
171
- return;
172
- }
173
- const task = await getTask(workspace, taskId);
174
- if (task === null) {
175
- sendJson(res, 404, { error: "任务不存在" });
176
- return;
177
- }
178
- sendJson(res, 200, { task, worklog: await readWorklog(workspace, task) });
43
+ sendJson(res, 200, await getBoardHome(await workspacesOf(deps)));
179
44
  }
180
45
  catch (error) {
181
46
  sendJson(res, 500, {
@@ -183,246 +48,4 @@ export function registerRoutes(webServer, deps) {
183
48
  });
184
49
  }
185
50
  });
186
- registerRead("/history", async (req, res) => {
187
- try {
188
- const filter = filterFromQuery(new URL(req.url ?? "/", "http://localhost"));
189
- const result = await queryClosed(workspacesOf(deps), filter);
190
- sendJson(res, 200, {
191
- records: result.records,
192
- degraded: result.degraded,
193
- });
194
- }
195
- catch (error) {
196
- if (error instanceof UsageError) {
197
- sendJson(res, 400, { error: error.message });
198
- return;
199
- }
200
- sendJson(res, 500, {
201
- error: error instanceof Error ? error.message : String(error),
202
- });
203
- }
204
- });
205
- registerRead("/filters", async (_req, res) => {
206
- try {
207
- const result = await getFilterOptions(workspacesOf(deps));
208
- sendJson(res, 200, { ...result.options, degraded: result.degraded });
209
- }
210
- catch (error) {
211
- sendJson(res, 500, {
212
- error: error instanceof Error ? error.message : String(error),
213
- });
214
- }
215
- });
216
- }
217
- // ───────────────────────── 台账写端点(host 直写 · 不开会话) ─────────────────────────
218
- /**
219
- * 注册认领 / 释放端点(2026-10-18 B 混合制:没有判断量的动作由 host 直跑脚本)。
220
- *
221
- * 这两条**会改磁盘上的台账文件**,所以和 `/close` 一样必须过同源守卫;
222
- * 成功后就地清该工作区读缓存,避免按钮点完还看到上一屏的 owner。
223
- */
224
- export function registerWriteRoutes(webServer, deps) {
225
- const registerWrite = (path, action) => {
226
- webServer.register({
227
- kind: "exact",
228
- path: `${API_PREFIX}${path}`,
229
- handler: async (req, res) => {
230
- if (req.method !== "POST") {
231
- sendJson(res, 405, { error: "只支持 POST" });
232
- return;
233
- }
234
- if (!isTrustedRequest(req)) {
235
- sendJson(res, 403, { error: "只接受同源请求" });
236
- return;
237
- }
238
- let body;
239
- try {
240
- body = await readJsonBody(req);
241
- }
242
- catch (error) {
243
- const tooLarge = error instanceof Error && error.message === "body-too-large";
244
- sendJson(res, tooLarge ? 413 : 400, {
245
- error: tooLarge ? "请求体过大" : "请求体不是合法 JSON",
246
- });
247
- return;
248
- }
249
- const fields = typeof body === "object" && body !== null
250
- ? body
251
- : {};
252
- const str = (key) => typeof fields[key] === "string" ? fields[key] : "";
253
- const workspaceId = str("ws");
254
- const taskId = str("id");
255
- if (workspaceId === "" || taskId === "") {
256
- sendJson(res, 400, { error: "缺少 ws / id 参数" });
257
- return;
258
- }
259
- const workspace = workspacesOf(deps).find((item) => item.id === workspaceId);
260
- if (workspace === undefined) {
261
- sendJson(res, 404, { error: "工作区不存在或不含台账" });
262
- return;
263
- }
264
- try {
265
- const notice = await action(workspace, taskId);
266
- invalidate(workspace.id);
267
- // notice = 写成功但同步失败的人话:必须让界面看见,不能当成功静默吞掉
268
- sendJson(res, 200, notice ? { ok: true, notice } : { ok: true });
269
- }
270
- catch (error) {
271
- sendJson(res, 500, {
272
- error: error instanceof Error ? error.message : String(error),
273
- });
274
- }
275
- },
276
- });
277
- };
278
- registerWrite("/claim", claimTask);
279
- registerWrite("/release", releaseTask);
280
- }
281
- // ───────────────────────── 会话交接(按钮侧 · 写类端点) ─────────────────────────
282
- /** 写端点请求体上限:只收一小段用户留言,别让人往这塞文件。 */
283
- const BODY_LIMIT = 64 * 1024;
284
- /** 读请求体(宿主 handler 收原始 req,没有 body parser;超上限直接拒,不缓冲垃圾)。 */
285
- async function readJsonBody(req) {
286
- const chunks = [];
287
- let size = 0;
288
- for await (const chunk of req) {
289
- const buffer = chunk;
290
- size += buffer.length;
291
- if (size > BODY_LIMIT)
292
- throw new Error("body-too-large");
293
- chunks.push(buffer);
294
- }
295
- const text = Buffer.concat(chunks).toString("utf8");
296
- if (text.trim() === "")
297
- return {};
298
- return JSON.parse(text);
299
- }
300
- /**
301
- * 同源守卫(architecture 不变量 6 的落地):浏览器 fetch 必带与 Host 同源的 `Origin`;
302
- * 没有 `Origin` 的(本机脚本 / Desktop IPC)只放行回环地址。
303
- * 读端点保持现状(只读无副作用);**会起会话的写端点必须过这道**。
304
- */
305
- function isTrustedRequest(req) {
306
- const origin = typeof req.headers.origin === "string" ? req.headers.origin : undefined;
307
- const host = typeof req.headers.host === "string" ? req.headers.host : undefined;
308
- if (origin === undefined) {
309
- const address = req.socket.remoteAddress;
310
- return (address === "127.0.0.1" ||
311
- address === "::1" ||
312
- address === "::ffff:127.0.0.1");
313
- }
314
- if (host === undefined)
315
- return false;
316
- try {
317
- return new URL(origin).host === host;
318
- }
319
- catch {
320
- return false;
321
- }
322
- }
323
- /**
324
- * 注册「结束任务」端点:`POST /close`。
325
- *
326
- * 为什么只有这一条走会话(2026-10-18 用户拍板):认领 / 释放没有判断量,host 直写即可;
327
- * **结束**要带用户留言、要模型读过程文档后写真摘要 —— 只能交给会话里的 Agent 做。
328
- *
329
- * 请求体:`{ ws, id, message?, provider?, model? }`;响应:`{ sessionId, warning? }`。
330
- */
331
- export function registerSessionRoutes(webServer, deps) {
332
- webServer.register({
333
- kind: "exact",
334
- path: `${API_PREFIX}/close`,
335
- handler: async (req, res) => {
336
- if (req.method !== "POST") {
337
- sendJson(res, 405, { error: "「结束任务」只支持 POST" });
338
- return;
339
- }
340
- if (!isTrustedRequest(req)) {
341
- sendJson(res, 403, { error: "只接受同源请求" });
342
- return;
343
- }
344
- let body;
345
- try {
346
- body = await readJsonBody(req);
347
- }
348
- catch (error) {
349
- const tooLarge = error instanceof Error && error.message === "body-too-large";
350
- sendJson(res, tooLarge ? 413 : 400, {
351
- error: tooLarge ? "请求体过大" : "请求体不是合法 JSON",
352
- });
353
- return;
354
- }
355
- const fields = typeof body === "object" && body !== null
356
- ? body
357
- : {};
358
- const str = (key) => typeof fields[key] === "string" ? fields[key] : "";
359
- const workspaceId = str("ws");
360
- const taskId = str("id");
361
- if (workspaceId === "" || taskId === "") {
362
- sendJson(res, 400, { error: "缺少 ws / id 参数" });
363
- return;
364
- }
365
- const provider = str("provider");
366
- const model = str("model");
367
- if ((provider === "") !== (model === "")) {
368
- sendJson(res, 400, { error: "provider 与 model 必须成对给出" });
369
- return;
370
- }
371
- const workspace = workspacesOf(deps).find((item) => item.id === workspaceId);
372
- if (workspace === undefined) {
373
- sendJson(res, 404, { error: "工作区不存在或不含台账" });
374
- return;
375
- }
376
- try {
377
- const result = await startCloseSession({
378
- agents: deps.agents,
379
- presets: deps.getService("agentPresets"),
380
- defaultModel: deps.getService("agentDefaultModel"),
381
- }, {
382
- workspace,
383
- taskId,
384
- message: str("message"),
385
- model: provider === "" ? undefined : { provider, model },
386
- });
387
- const warning = (await attachSession(deps, workspaceId, result.sessionId)) ??
388
- result.warning;
389
- sendJson(res, 200, warning === undefined
390
- ? { sessionId: result.sessionId }
391
- : { sessionId: result.sessionId, warning });
392
- }
393
- catch (error) {
394
- if (error instanceof HandoffError) {
395
- const status = error.code === "task-missing"
396
- ? 404
397
- : error.code === "no-agents" ||
398
- error.code === "no-preset" ||
399
- error.code === "no-model"
400
- ? 503
401
- : 500;
402
- sendJson(res, status, { error: error.message, code: error.code });
403
- return;
404
- }
405
- sendJson(res, 500, {
406
- error: error instanceof Error ? error.message : String(error),
407
- });
408
- }
409
- },
410
- });
411
- }
412
- /**
413
- * 会话归组(best effort):登记进工作区 `sessionIds`,否则侧栏里找不到这个会话。
414
- * 缺失 / 失败都不算致命——会话本身已经建好、跳转可用,只如实回一条 warning。
415
- */
416
- async function attachSession(deps, workspaceId, sessionId) {
417
- const entity = deps.registry.list().find((item) => item.id === workspaceId);
418
- if (entity === undefined || typeof entity.attachSession !== "function") {
419
- return "会话已建,但宿主该工作区没有 attachSession 面:会话不会出现在工作区列表里";
420
- }
421
- try {
422
- await entity.attachSession(sessionId);
423
- return undefined;
424
- }
425
- catch (error) {
426
- return `会话已建,但没能挂进工作区列表:${error instanceof Error ? error.message : String(error)}`;
427
- }
428
51
  }
@@ -2,15 +2,12 @@
2
2
  * 日期口径(纯函数层,零 IO):本机时区、周一为一周之始(与 skill 脚本同口径)。
3
3
  * 所有函数显式收 `now`——不自己读时钟,便于单测与跨时区复现。
4
4
  *
5
- * ⚠️ **台账存盘时间是 UTC**(skill `core/time.py::now`),`--json` 原样吐 UTC,
6
- * 只有人话输出才转查看者本地(`fmt_local`)。看板是给人看的 ⇒ 一律先按 UTC 解析出
7
- * 真实时刻,再按本机时区取日期(等价于脚本的 `fmt_local`)。否则会出现
8
- * 「脚本表格里是 10-17、看板却算进 10-16 那周」—— 两个界面同一条记录差一天。
5
+ * 现在只有「本周 / 上周结案数」用得到(`history --count-only` 的时间窗)。
6
+ * 台账时刻的解析与天数派生**已全部删除**:那些是 skill 算好的(`age_days` / `idle_days` /
7
+ * `waiting_days` 随 `list --json` 每行下发),插件不再保留第二套算法。
9
8
  */
10
9
  /** Date → `YYYY-MM-DD`(本机时区)。 */
11
10
  export declare function ymd(date: Date): string;
12
- /** 时刻串(`YYYY-MM-DD HH:mm:ss` / `YYYY-MM-DD`)→ 本机时区的日期;解析不了回 null。 */
13
- export declare function dayOf(stamp: string | null | undefined): string | null;
14
11
  /** 某日期所在周的周一(00:00)。 */
15
12
  export declare function mondayOf(day: string): Date;
16
13
  /**
@@ -22,21 +19,3 @@ export declare function weekWindow(offset: number, now: Date): {
22
19
  from: string;
23
20
  to: string;
24
21
  };
25
- /** 第 offset 月的起止(offset 0 = 本月,-1 = 上月;含边界)。
26
- * 同 {@link weekWindow}:本月只到今天,上月才是完整自然月。 */
27
- export declare function monthWindow(offset: number, now: Date): {
28
- from: string;
29
- to: string;
30
- };
31
- /** 日期是否落在 [from, to](含边界,字符串比较即可——ISO 字典序即时间序)。 */
32
- export declare function inWindow(day: string | null, window: {
33
- from: string;
34
- to: string;
35
- }): boolean;
36
- /** 「YYYY-MM-DD HH:mm:ss」→ 距今天数(解析不了 / 未来都按 0;不编造)。 */
37
- export declare function daysSince(stamp: string | null | undefined, now: Date): number | null;
38
- /** 近 N 周的桶骨架(`{week, end}`,历史周 end=周日、当前周 end=今天)。 */
39
- export declare function buildWeeks(count: number, now: Date): readonly {
40
- week: string;
41
- end: string;
42
- }[];
@@ -2,10 +2,9 @@
2
2
  * 日期口径(纯函数层,零 IO):本机时区、周一为一周之始(与 skill 脚本同口径)。
3
3
  * 所有函数显式收 `now`——不自己读时钟,便于单测与跨时区复现。
4
4
  *
5
- * ⚠️ **台账存盘时间是 UTC**(skill `core/time.py::now`),`--json` 原样吐 UTC,
6
- * 只有人话输出才转查看者本地(`fmt_local`)。看板是给人看的 ⇒ 一律先按 UTC 解析出
7
- * 真实时刻,再按本机时区取日期(等价于脚本的 `fmt_local`)。否则会出现
8
- * 「脚本表格里是 10-17、看板却算进 10-16 那周」—— 两个界面同一条记录差一天。
5
+ * 现在只有「本周 / 上周结案数」用得到(`history --count-only` 的时间窗)。
6
+ * 台账时刻的解析与天数派生**已全部删除**:那些是 skill 算好的(`age_days` / `idle_days` /
7
+ * `waiting_days` 随 `list --json` 每行下发),插件不再保留第二套算法。
9
8
  */
10
9
  function pad(value) {
11
10
  return String(value).padStart(2, "0");
@@ -14,31 +13,6 @@ function pad(value) {
14
13
  export function ymd(date) {
15
14
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
16
15
  }
17
- /**
18
- * 台账时刻串 → Date(带时刻的按 **UTC** 解析;纯日期串按本地日历,不涉及时区)。
19
- * 解析不了回 null,不猜。
20
- */
21
- function parseStamp(stamp) {
22
- const text = stamp.trim().replace(" ", "T");
23
- // 已带时区后缀(`Z` / `+08:00`)的不重复加,加了会二次偏移
24
- const iso = /\d{1,2}:\d{2}/.test(text)
25
- ? /(?:[zZ]|[+-]\d{2}:?\d{2})$/.test(text)
26
- ? text
27
- : `${text}Z`
28
- : text;
29
- const parsed = new Date(iso);
30
- return Number.isNaN(parsed.getTime()) ? null : parsed;
31
- }
32
- /** 时刻串(`YYYY-MM-DD HH:mm:ss` / `YYYY-MM-DD`)→ 本机时区的日期;解析不了回 null。 */
33
- export function dayOf(stamp) {
34
- if (stamp === null || stamp === undefined || stamp === "")
35
- return null;
36
- const text = String(stamp);
37
- if (/^\d{4}-\d{2}-\d{2}$/.test(text.trim()))
38
- return text.trim();
39
- const parsed = parseStamp(text);
40
- return parsed === null ? null : ymd(parsed);
41
- }
42
16
  /** 某日期所在周的周一(00:00)。 */
43
17
  export function mondayOf(day) {
44
18
  const date = new Date(`${day}T00:00:00`);
@@ -58,37 +32,3 @@ export function weekWindow(offset, now) {
58
32
  const last = offset === 0 ? now : sunday;
59
33
  return { from: ymd(monday), to: ymd(last < sunday ? last : sunday) };
60
34
  }
61
- /** 第 offset 月的起止(offset 0 = 本月,-1 = 上月;含边界)。
62
- * 同 {@link weekWindow}:本月只到今天,上月才是完整自然月。 */
63
- export function monthWindow(offset, now) {
64
- const first = new Date(now.getFullYear(), now.getMonth() + offset, 1);
65
- const last = new Date(now.getFullYear(), now.getMonth() + offset + 1, 0);
66
- const end = offset === 0 ? now : last;
67
- return { from: ymd(first), to: ymd(end < last ? end : last) };
68
- }
69
- /** 日期是否落在 [from, to](含边界,字符串比较即可——ISO 字典序即时间序)。 */
70
- export function inWindow(day, window) {
71
- return day !== null && day >= window.from && day <= window.to;
72
- }
73
- /** 「YYYY-MM-DD HH:mm:ss」→ 距今天数(解析不了 / 未来都按 0;不编造)。 */
74
- export function daysSince(stamp, now) {
75
- if (stamp === null || stamp === undefined || stamp === "")
76
- return null;
77
- const parsed = parseStamp(String(stamp));
78
- if (parsed === null)
79
- return null;
80
- const days = Math.floor((now.getTime() - parsed.getTime()) / 86_400_000);
81
- return days > 0 ? days : 0;
82
- }
83
- /** 近 N 周的桶骨架(`{week, end}`,历史周 end=周日、当前周 end=今天)。 */
84
- export function buildWeeks(count, now) {
85
- const weeks = [];
86
- for (let offset = count - 1; offset >= 0; offset -= 1)
87
- weeks.push(weekWindow(-offset, now).from);
88
- return weeks.map((week, i) => {
89
- const monday = new Date(`${week}T00:00:00`);
90
- const sunday = new Date(monday);
91
- sunday.setDate(sunday.getDate() + 6);
92
- return { week, end: i === weeks.length - 1 ? ymd(now) : ymd(sunday) };
93
- });
94
- }
@@ -0,0 +1,7 @@
1
+ import type { BoardHomePayload, LedgerWorkspace, StatsPayload } from "../types.js";
2
+ /**
3
+ * 首屏一次拿全:8 卡 + 在办列表 + 工作区 + 身份 + 各工作区花名册。
4
+ * 三条读并行(`stats` / `list` / `owners`),各自缓存;一个工作区坏只进 degraded,其余照常。
5
+ */
6
+ export declare function getBoardHome(workspaces: readonly LedgerWorkspace[], now?: Date): Promise<BoardHomePayload>;
7
+ export declare function getBoardStats(workspaces: readonly LedgerWorkspace[], now?: Date): Promise<StatsPayload>;