dsh-task-ledger 0.0.112 → 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 (47) 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 +5 -4
  30. package/skill/package.json +1 -1
  31. package/skill/scripts/bin/ledger-darwin-arm64 +0 -0
  32. package/skill/scripts/bin/ledger-linux-amd64 +0 -0
  33. package/skill/scripts/bin/ledger-windows-amd64.exe +0 -0
  34. package/dist/host/board.d.ts +0 -20
  35. package/dist/host/board.js +0 -29
  36. package/dist/host/services/history.service.d.ts +0 -27
  37. package/dist/host/services/history.service.js +0 -72
  38. package/dist/host/services/home.service.d.ts +0 -23
  39. package/dist/host/services/home.service.js +0 -91
  40. package/dist/host/services/ledger-write.service.d.ts +0 -10
  41. package/dist/host/services/ledger-write.service.js +0 -32
  42. package/dist/host/services/session.service.d.ts +0 -110
  43. package/dist/host/services/session.service.js +0 -155
  44. package/dist/host/services/trend.service.d.ts +0 -36
  45. package/dist/host/services/trend.service.js +0 -162
  46. package/dist/host/services/uuid.d.ts +0 -11
  47. package/dist/host/services/uuid.js +0 -15
@@ -20,7 +20,7 @@ export interface DegradedInfo {
20
20
  readonly workspaceId: string;
21
21
  readonly workspaceTitle: string;
22
22
  /** 哪个读挂了——trend 同时读 list + history,必须能区分。 */
23
- readonly op: "list" | "history";
23
+ readonly op: "list" | "history" | "owners";
24
24
  readonly code: LedgerErrorCode;
25
25
  /** 人话,可直接上界面(真源:脚本 stderr / records 校验文案)。 */
26
26
  readonly message: string;
@@ -36,7 +36,7 @@ export declare function messageOf(error: unknown): string;
36
36
  export declare function settleWorkspaces<W extends {
37
37
  id: string;
38
38
  title: string;
39
- }, T, R>(workspaces: readonly W[], op: "list" | "history", read: (workspace: W) => Promise<T>, merge: (rows: readonly T[]) => R): Promise<{
39
+ }, T, R>(workspaces: readonly W[], op: "list" | "history" | "owners", read: (workspace: W) => Promise<T>, merge: (rows: readonly T[]) => R): Promise<{
40
40
  data: R;
41
41
  degraded: readonly DegradedInfo[];
42
42
  }>;
@@ -5,14 +5,13 @@
5
5
  * ② 命中且 1.5s 内直接返回、**不起进程**;同 key 并发**单飞**(只跑一次脚本,其余等同一个 Promise);
6
6
  * ③ `fresh=1` 绕过(路由层先 `invalidate()` 再算);写路径成功后 `invalidate(workspaceId)` 钩子。
7
7
  *
8
- * 边界:只缓存 skill 脚本的两读(在办 list / 历史 history)。worklog 正文不缓存——它会被
9
- * 会话里的 Agent 实时改写,缓存 1.5s 也会让用户看到上一版。**失败不入缓存**(错就是错,重试要能重新跑)。
8
+ * 边界:只缓存 skill 脚本的读命令结果。**失败不入缓存**(错就是错,重试要能重新跑)。
10
9
  * 命中返回的是**同一个引用**:调用方只许读(上层 service 全是纯计算,不原地改数组)。
11
10
  */
12
11
  /** 缓存有效期(2026-10-18 拍板 1.5s:够挡同一屏的重复读,短到不会让人看到旧数据)。 */
13
12
  export declare const CACHE_TTL_MS = 1500;
14
13
  /** 缓存的操作种类(= skill 脚本子命令;worklog 不进缓存故不在此列)。 */
15
- export type CacheOp = "list" | "history";
14
+ export type CacheOp = "list" | "history" | "stats" | "owners";
16
15
  /** 缓存键:`op:workspace:filter` 三条信息都要,否则不同筛选 / 不同工作区会串味。 */
17
16
  export declare function cacheKey(workspaceId: string, op: CacheOp, detail: string): string;
18
17
  /**
@@ -5,8 +5,7 @@
5
5
  * ② 命中且 1.5s 内直接返回、**不起进程**;同 key 并发**单飞**(只跑一次脚本,其余等同一个 Promise);
6
6
  * ③ `fresh=1` 绕过(路由层先 `invalidate()` 再算);写路径成功后 `invalidate(workspaceId)` 钩子。
7
7
  *
8
- * 边界:只缓存 skill 脚本的两读(在办 list / 历史 history)。worklog 正文不缓存——它会被
9
- * 会话里的 Agent 实时改写,缓存 1.5s 也会让用户看到上一版。**失败不入缓存**(错就是错,重试要能重新跑)。
8
+ * 边界:只缓存 skill 脚本的读命令结果。**失败不入缓存**(错就是错,重试要能重新跑)。
10
9
  * 命中返回的是**同一个引用**:调用方只许读(上层 service 全是纯计算,不原地改数组)。
11
10
  */
12
11
  /** 缓存有效期(2026-10-18 拍板 1.5s:够挡同一屏的重复读,短到不会让人看到旧数据)。 */
@@ -1,4 +1,10 @@
1
1
  import type { LedgerWorkspace } from "../types.js";
2
- /** 机器级身份文件路径(与 skill 侧 `env.identity_file()` 同一口径)。 */
3
- export declare function identityPath(): string;
2
+ /**
3
+ * 本机身份 uuid —— 走 skill `whoami --json`(**不再自己读 identity.json**:
4
+ * `$TASK_LEDGER_HOME` / 默认目录 / 文件格式都是 skill 的知识,插件不复制第二份)。
5
+ *
6
+ * **只取 `uuid`**:`name` 是冗余显示字段,不参与任何判定(按名字比 owner 永远比不到)。
7
+ * 未初始化时 skill 回 `{"uuid":null}`(退 0),这里同样回 null ⇒ 「我的」按 0 计(不谎报)。
8
+ * 程序起不来看不出身份,也只回 null。
9
+ */
4
10
  export declare function getCurrentIdentity(_workspace?: LedgerWorkspace): Promise<string | null>;
@@ -1,36 +1,16 @@
1
- import { readFile } from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
1
+ import { runLedgerScript } from "./runner.js";
4
2
  /**
5
- * 本机身份 uuid —— 「我的待办 / 我的结案」判定的**唯一**来源。
6
- *
7
- * 身份是**机器级**的:skill 的 `init` 把 `{uuid, name}` 写进
8
- * `$TASK_LEDGER_HOME/.taskledger/identity.json`(默认 `~/.taskledger/identity.json`),
9
- * 与工作区、git 配置都无关;任务与历史里的 `owner` / `creator` 存的正是这个 uuid。
10
- * 参数保留是为了与其它 per-workspace 读法同形(同一个 uuid 对所有工作区都成立)。
3
+ * 本机身份 uuid —— 走 skill `whoami --json`(**不再自己读 identity.json**:
4
+ * `$TASK_LEDGER_HOME` / 默认目录 / 文件格式都是 skill 的知识,插件不复制第二份)。
11
5
  *
12
6
  * **只取 `uuid`**:`name` 是冗余显示字段,不参与任何判定(按名字比 owner 永远比不到)。
13
- * 读不到回 null(没 init / 文件坏 / 没 home 目录),该工作区的「我的」统计按 0 计(不谎报)。
7
+ * 未初始化时 skill 回 `{"uuid":null}`(退 0),这里同样回 null ⇒ 「我的」按 0 计(不谎报)。
8
+ * 程序起不来看不出身份,也只回 null。
14
9
  */
15
- const IDENTITY_HOME_ENV = "TASK_LEDGER_HOME";
16
- /** 机器级身份文件路径(与 skill 侧 `env.identity_file()` 同一口径)。 */
17
- export function identityPath() {
18
- const base = process.env[IDENTITY_HOME_ENV];
19
- // 空串按「没设」处理:否则 path.join("", …) 会变成跟着 host 进程 cwd 走的相对路径,
20
- // 与脚本侧 `if base: … else Path.home()` 的口径就对不上了
21
- const dir = base !== undefined && base.trim() !== "" ? base : os.homedir();
22
- return path.join(dir, ".taskledger", "identity.json");
23
- }
24
10
  export async function getCurrentIdentity(_workspace) {
25
- try {
26
- const raw = await readFile(identityPath(), "utf-8");
27
- const data = JSON.parse(raw);
28
- const uuid = data !== null && typeof data === "object"
29
- ? String(data.uuid ?? "").trim()
30
- : "";
31
- return uuid === "" ? null : uuid.toLowerCase();
32
- }
33
- catch {
11
+ const result = await runLedgerScript("whoami", ["--json"]);
12
+ if (!result.ok)
34
13
  return null;
35
- }
14
+ const uuid = typeof result.data.uuid === "string" ? result.data.uuid.trim() : "";
15
+ return uuid === "" ? null : uuid.toLowerCase();
36
16
  }
@@ -1,18 +1,18 @@
1
- import type { HistoryRecord, LedgerFilter, LedgerWorkspace, TaskRow } from "../types.js";
1
+ import type { LedgerFilter, LedgerWorkspace, TaskRow, WorkspaceOwners } from "../types.js";
2
2
  /**
3
3
  * LedgerFilter → skill 脚本参数(只拼调用方给了的;undefined 一律不拼,
4
4
  * 让脚本走自己的默认——插件不替脚本决定默认值)。
5
- *
6
- * **按子命令裁参数**:`list` 不认 `--limit` / `--months`,`history` 不认 `--status`,
7
- * 拼错一个就让 argparse 退 2、整块列表 500。口径以 `skill/shell/cli.py` 为准。
5
+ * 参数名以 `skill/shell/cli.go` 的 `list` 白名单为准,拼错一个就让 argparse 退 2、整块列表 500。
8
6
  */
9
- export declare function filterArgs(filter: LedgerFilter, sub: "list" | "history"): string[];
7
+ export declare function filterArgs(filter: LedgerFilter): string[];
10
8
  /**
11
- * 台账工作区探测:`<path>/ledger/pool.json` 在才算(分片布局的初始化标志;
12
- * 未初始化 = 后端不可见,2026-10-17 拍板)。全 host 对台账目录的 IO 只在本文件
13
- * ——别处再写 existsSync 即为分层倒退。
9
+ * 台账工作区探测:走 skill `root --json`(判据是「这个目录直接含 `ledger/`」,
10
+ * 与脚本定位工作区根同一口径;有没有 `pool.json` 不影响)。
11
+ *
12
+ * 曾经的 `<path>/ledger/pool.json` 直查已废——布局知识只留 skill 一份。
13
+ * 程序起不来时抛错(不静默把工作区藏掉)。
14
14
  */
15
- export declare function hasLedger(workspacePath: string): boolean;
15
+ export declare function hasLedger(workspacePath: string): Promise<boolean>;
16
16
  /**
17
17
  * 在办任务(包 `list --json`;结果过 records 入口校验后打上 workspace)。
18
18
  * 脚本失败 / 形状不符抛错——由 service 层捕获进 degraded,路由层转 500 + 人话,
@@ -20,17 +20,7 @@ export declare function hasLedger(workspacePath: string): boolean;
20
20
  */
21
21
  export declare function readTasks(workspace: LedgerWorkspace, filter?: LedgerFilter): Promise<TaskRow[]>;
22
22
  /**
23
- * 已结案记录(包 `history --json`;`records` 必须存在且为数组,缺了即坏数据)。同样过读缓存。
24
- * 一并带回 `warnings`(被脚本整行跳过的坏行原因)——调用方必须展示,不许吞。
25
- */
26
- export declare function readHistory(workspace: LedgerWorkspace, filter?: LedgerFilter): Promise<{
27
- records: HistoryRecord[];
28
- warnings: string[];
29
- }>;
30
- /** 单条在办(展开行 / 详情用)。取不到回 null,不抛。 */
31
- export declare function getTask(workspace: LedgerWorkspace, taskId: string): Promise<TaskRow | null>;
32
- /**
33
- * 过程文档正文(`task.worklog` 相对 `ledger/` 的路径)。
34
- * 读不到回 null(文件被清 / 路径坏都不炸——由调用方展示「过程文档缺失」)。
23
+ * 工作区花名册(skill `owners --json`,只读、不改台账)。跨工作区由 service 扇出。
24
+ * 名册为空的工作区脚本会报错 ⇒ settle 收进 degraded,本工作区分组自然不出现。
35
25
  */
36
- export declare function readWorklog(workspace: LedgerWorkspace, task: TaskRow): Promise<string | null>;
26
+ export declare function readOwners(workspace: LedgerWorkspace): Promise<WorkspaceOwners>;
@@ -1,21 +1,14 @@
1
- import { existsSync } from "node:fs";
2
- import { readFile } from "node:fs/promises";
3
- import path from "node:path";
4
1
  import { cached, cacheKey } from "./cache.js";
5
- import { parseHistoryRecords, parseTaskList } from "./records.js";
2
+ import { parseTaskList } from "./records.js";
6
3
  import { runLedgerScript } from "./runner.js";
7
- /** 台账目录名(各工作区根下的这一个目录;布局知识只在本文件出现一次)。 */
8
- const LEDGER_DIR = "ledger";
9
4
  /**
10
5
  * LedgerFilter → skill 脚本参数(只拼调用方给了的;undefined 一律不拼,
11
6
  * 让脚本走自己的默认——插件不替脚本决定默认值)。
12
- *
13
- * **按子命令裁参数**:`list` 不认 `--limit` / `--months`,`history` 不认 `--status`,
14
- * 拼错一个就让 argparse 退 2、整块列表 500。口径以 `skill/shell/cli.py` 为准。
7
+ * 参数名以 `skill/shell/cli.go` 的 `list` 白名单为准,拼错一个就让 argparse 退 2、整块列表 500。
15
8
  */
16
- export function filterArgs(filter, sub) {
9
+ export function filterArgs(filter) {
17
10
  const args = [];
18
- if (sub === "list" && filter.status !== undefined)
11
+ if (filter.status !== undefined)
19
12
  args.push("--status", filter.status);
20
13
  // 身份只收 uuid(脚本同样口径);按名字模糊筛走 --owner-name
21
14
  if (filter.owner !== undefined)
@@ -30,22 +23,24 @@ export function filterArgs(filter, sub) {
30
23
  args.push("--since", filter.since);
31
24
  if (filter.until !== undefined)
32
25
  args.push("--until", filter.until);
33
- if (sub === "history") {
34
- if (filter.limit !== undefined)
35
- args.push("--limit", String(filter.limit));
36
- if (filter.months !== undefined)
37
- args.push("--months", String(filter.months));
38
- }
39
26
  return args;
40
27
  }
41
28
  /**
42
- * 台账工作区探测:`<path>/ledger/pool.json` 在才算(分片布局的初始化标志;
43
- * 未初始化 = 后端不可见,2026-10-17 拍板)。全 host 对台账目录的 IO 只在本文件
44
- * ——别处再写 existsSync 即为分层倒退。
29
+ * 台账工作区探测:走 skill `root --json`(判据是「这个目录直接含 `ledger/`」,
30
+ * 与脚本定位工作区根同一口径;有没有 `pool.json` 不影响)。
31
+ *
32
+ * 曾经的 `<path>/ledger/pool.json` 直查已废——布局知识只留 skill 一份。
33
+ * 程序起不来时抛错(不静默把工作区藏掉)。
45
34
  */
46
- export function hasLedger(workspacePath) {
47
- // 分片布局(2026-10-23):公共池 pool.json 在才算初始化;各人分片 owners/*.json 由脚本维护。
48
- return existsSync(path.join(workspacePath, LEDGER_DIR, "pool.json"));
35
+ export async function hasLedger(workspacePath) {
36
+ const result = await runLedgerScript("root", [
37
+ "--dir",
38
+ workspacePath,
39
+ "--json",
40
+ ]);
41
+ if (!result.ok)
42
+ throw new Error(`台账根检测失败(${workspacePath}):${result.error}`);
43
+ return result.data.root === true;
49
44
  }
50
45
  /**
51
46
  * 在办任务(包 `list --json`;结果过 records 入口校验后打上 workspace)。
@@ -53,12 +48,12 @@ export function hasLedger(workspacePath) {
53
48
  * 不静默返空(空 = 撒谎)。**过 1.5s 读缓存**(同屏多端点会反复读同一份在办)。
54
49
  */
55
50
  export async function readTasks(workspace, filter = {}) {
56
- return await cached(cacheKey(workspace.id, "list", argsKey(filter, "list")), async () => {
51
+ return await cached(cacheKey(workspace.id, "list", argsKey(filter)), async () => {
57
52
  const result = await runLedgerScript("list", [
58
53
  "--repo",
59
54
  workspace.path,
60
55
  "--json",
61
- ...filterArgs(filter, "list"),
56
+ ...filterArgs(filter),
62
57
  ]);
63
58
  if (!result.ok)
64
59
  throw new Error(`${workspace.title}: ${result.error}`);
@@ -66,43 +61,29 @@ export async function readTasks(workspace, filter = {}) {
66
61
  });
67
62
  }
68
63
  /**
69
- * 已结案记录(包 `history --json`;`records` 必须存在且为数组,缺了即坏数据)。同样过读缓存。
70
- * 一并带回 `warnings`(被脚本整行跳过的坏行原因)——调用方必须展示,不许吞。
64
+ * 工作区花名册(skill `owners --json`,只读、不改台账)。跨工作区由 service 扇出。
65
+ * 名册为空的工作区脚本会报错 ⇒ settle 收进 degraded,本工作区分组自然不出现。
71
66
  */
72
- export async function readHistory(workspace, filter = {}) {
73
- return await cached(cacheKey(workspace.id, "history", argsKey(filter, "history")), async () => {
74
- const result = await runLedgerScript("history", [
75
- "--repo",
76
- workspace.path,
77
- "--json",
78
- ...filterArgs(filter, "history"),
79
- ]);
67
+ export async function readOwners(workspace) {
68
+ return await cached(cacheKey(workspace.id, "owners", ""), async () => {
69
+ const result = await runLedgerScript("owners", ["--repo", workspace.path, "--json"]);
80
70
  if (!result.ok)
81
71
  throw new Error(`${workspace.title}: ${result.error}`);
82
- return parseHistoryRecords(result.data, workspace);
72
+ const owners = Object.entries(result.data.owners ?? {}).map(([uuid, value]) => ({
73
+ uuid,
74
+ name: typeof value.name === "string" && value.name.trim() !== ""
75
+ ? value.name
76
+ : uuid,
77
+ mine: value.mine === true,
78
+ }));
79
+ return {
80
+ workspaceId: workspace.id,
81
+ workspaceTitle: workspace.title,
82
+ owners,
83
+ };
83
84
  });
84
85
  }
85
86
  /** 缓存键的 filter 段:脚本参数原样序列化(分隔符取不可见字符,值里带空格也不会串)。 */
86
- function argsKey(filter, sub) {
87
- return filterArgs(filter, sub).join("\u0001");
88
- }
89
- /** 单条在办(展开行 / 详情用)。取不到回 null,不抛。 */
90
- export async function getTask(workspace, taskId) {
91
- const rows = await readTasks(workspace);
92
- return rows.find((row) => row.id === taskId) ?? null;
93
- }
94
- /**
95
- * 过程文档正文(`task.worklog` 相对 `ledger/` 的路径)。
96
- * 读不到回 null(文件被清 / 路径坏都不炸——由调用方展示「过程文档缺失」)。
97
- */
98
- export async function readWorklog(workspace, task) {
99
- const relative = task.worklog;
100
- if (relative === undefined || relative === null || relative === "")
101
- return null;
102
- try {
103
- return await readFile(path.join(workspace.path, LEDGER_DIR, relative), "utf-8");
104
- }
105
- catch {
106
- return null;
107
- }
87
+ function argsKey(filter) {
88
+ return filterArgs(filter).join("\u0001");
108
89
  }
@@ -1,13 +1,3 @@
1
- import type { HistoryRecord, LedgerWorkspace, TaskRow } from "../types.js";
1
+ import type { LedgerWorkspace, TaskRow } from "../types.js";
2
2
  /** `list --json`(任务号为键的对象)→ TaskRow[](打上 id 与 workspace)。 */
3
3
  export declare function parseTaskList(raw: unknown, workspace: LedgerWorkspace): TaskRow[];
4
- /**
5
- * `history --json`(`{count, filters, records, warnings}`)→ 记录 + 脚本警告(都打上 workspace)。
6
- *
7
- * ⚠️ `warnings` **必须带出去**:脚本只认十一列历史,列数不对 / 时间不合法的行会被
8
- * **整行跳过**(不是少几个字段)。丢掉 warnings 就是「趋势图少算一截还装作没发生」。
9
- */
10
- export declare function parseHistoryRecords(raw: unknown, workspace: LedgerWorkspace): {
11
- records: HistoryRecord[];
12
- warnings: string[];
13
- };
@@ -2,10 +2,8 @@
2
2
  * 入口校验(纯函数):skill 脚本 JSON → 领域对象。
3
3
  *
4
4
  * 口径:**容器级严格、元素级宽松**。
5
- * - 容器:`list --json` 顶层必须是 plain object(任务号为键);`history --json` 的 `records`
6
- * 必须存在且为数组(现状 `records ?? []` 的容忍在脚本回归时会静默产出空历史 = 撒谎);
7
- * - 元素:是 object 就收,字段缺失一律放行(TaskRecord 全字段可选是脚本既有契约,
8
- * 旧 8 列 history 不因此被误杀)。
5
+ * - 容器:`list --json` 顶层必须是 plain object(任务号为键);
6
+ * - 元素:是 object 就收,字段缺失一律放行(TaskRecord 全字段可选是脚本既有契约)。
9
7
  * 失败抛 {@link LedgerDataError}——由 service 层捕获进 degraded,不静默。
10
8
  */
11
9
  import { LedgerDataError } from "../errors.js";
@@ -25,52 +23,28 @@ export function parseTaskList(raw, workspace) {
25
23
  if (id === "") {
26
24
  throw new LedgerDataError(workspace.id, `${workspace.title}: 台账存在空任务号,格式可能已变`);
27
25
  }
28
- rows.push({ ...value, id, workspace });
26
+ rows.push(toTaskRow(value, id, workspace));
29
27
  }
30
28
  return rows;
31
29
  }
32
30
  /**
33
- * `history --json`(`{count, filters, records, warnings}`)→ 记录 + 脚本警告(都打上 workspace)。
31
+ * 脚本的 snake_case 派生字段 → 领域 camelCase。
34
32
  *
35
- * ⚠️ `warnings` **必须带出去**:脚本只认十一列历史,列数不对 / 时间不合法的行会被
36
- * **整行跳过**(不是少几个字段)。丢掉 warnings 就是「趋势图少算一截还装作没发生」。
33
+ * `stale` / `overdue` / `backlog` / `mine` 与三个天数**都是 skill 算好的**(阈值判定、
34
+ * 身份比较都在脚本侧),插件只搬字段,不重算一遍。缺字段一律 null / false(不编 0)。
37
35
  */
38
- export function parseHistoryRecords(raw, workspace) {
39
- if (!isPlainObject(raw)) {
40
- throw new LedgerDataError(workspace.id, `${workspace.title}: 台账 history 输出不是对象,拒绝当成空历史`);
41
- }
42
- const records = raw.records;
43
- if (!Array.isArray(records)) {
44
- throw new LedgerDataError(workspace.id, `${workspace.title}: 台账 history 输出缺 records 数组,格式可能已变`);
45
- }
46
- const rows = [];
47
- for (const record of records) {
48
- if (!isPlainObject(record))
49
- continue; // 元素级宽松:坏行跳过,不整板炸
50
- rows.push(toHistoryRecord(record, workspace));
51
- }
52
- const rawWarnings = raw.warnings;
53
- const warnings = Array.isArray(rawWarnings)
54
- ? rawWarnings.filter((item) => typeof item === "string")
55
- : [];
56
- return { records: rows, warnings };
57
- }
58
- /**
59
- * 脚本 9 列的键 → 领域字段名。
60
- *
61
- * 脚本(`history --json`)吐的是**列名**:`finished` / `title` / `worklog`;
62
- * 领域层用的是 `time` / `workpackage` / `log`。两个名字都得能用:
63
- * 不映射的话结案时间会落到 `created`、标题落到摘要、趋势图的「完成数」恒 0(`dayOf(undefined)`)。
64
- * 两边都给:脚本将来改回列名以外的实现也不会炸。
65
- */
66
- function toHistoryRecord(source, workspace) {
67
- const text = (value) => typeof value === "string" && value !== "" ? value : undefined;
36
+ function toTaskRow(source, id, workspace) {
37
+ const days = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
68
38
  return {
69
39
  ...source,
70
- // 脚本里的列名是真源;领域字段名作为兼容保留
71
- time: text(source.finished) ?? text(source.time),
72
- workpackage: text(source.title) ?? text(source.workpackage),
73
- log: text(source.worklog) ?? text(source.log),
40
+ id,
74
41
  workspace,
42
+ ageDays: days(source.age_days),
43
+ idleDays: days(source.idle_days),
44
+ waitingDays: days(source.waiting_days),
45
+ stale: source.stale === true,
46
+ overdue: source.overdue === true,
47
+ backlog: source.backlog === true,
48
+ mine: source.mine === true,
75
49
  };
76
50
  }
@@ -1,10 +1,3 @@
1
- /**
2
- * 写命令的超时上限(ms):脚本每条 git 调用各自 30s 超时,写前(fetch / ff / push /
3
- * rebase / push)与写后(push / fetch / rebase / push)串起来最坏约 270s。
4
- * 低到 120s 会在网络挂死时先报「超时失败」——台账其实已写入,用户一重试就是二次写入。
5
- * 180s 覆盖常见最坏组合,又不至于让真卡死的请求占着界面太久。
6
- */
7
- export declare const WRITE_TIMEOUT_MS = 180000;
8
1
  export type LedgerScriptResult<T> = {
9
2
  ok: true;
10
3
  data: T;
@@ -12,28 +5,11 @@ export type LedgerScriptResult<T> = {
12
5
  ok: false;
13
6
  error: string;
14
7
  };
15
- /**
16
- * 写命令的返回:stdout 原文(claim / set 打的是人话,不是 JSON)+ 退出码 0 时的 stderr。
17
- * `notice` 有值说明脚本写成功了、但同步(commit / push)没成功 —— 不能当失败抛,也不能吞掉。
18
- */
19
- export type LedgerCommandResult = {
20
- ok: true;
21
- output: string;
22
- notice?: string;
23
- } | {
24
- ok: false;
25
- error: string;
26
- };
27
8
  export interface RunOptions {
28
9
  /** 测试用:覆盖脚本路径。 */
29
10
  scriptPath?: string;
30
11
  /** 覆盖超时。 */
31
12
  timeoutMs?: number;
32
- /**
33
- * 子进程 cwd。写命令(claim / set)必须落在工作区:
34
- * 脚本按 cwd 逐级往上找 `ledger/`(或靠 `--repo` 指定),与身份无关。
35
- */
36
- cwd?: string;
37
13
  }
38
14
  /**
39
15
  * 读命令:跑内嵌台账程序,收 stdout JSON。
@@ -43,11 +19,5 @@ export interface RunOptions {
43
19
  * @param args - 参数数组,由调用方自拼(含 --repo / --json / --months 等)
44
20
  * @param opts.scriptPath - 测试用:覆盖脚本路径
45
21
  * @param opts.timeoutMs - 覆盖超时
46
- * @param opts.cwd - 子进程工作目录(写命令按 cwd 定位台账用)
47
22
  */
48
23
  export declare function runLedgerScript<T = unknown>(sub: string, args: readonly string[], opts?: RunOptions): Promise<LedgerScriptResult<T>>;
49
- /**
50
- * 写命令(claim / set):同样是 spawn 入口脚本,但**不解析 JSON**——这些子命令打的是人话,
51
- * 成功与否看退出码,失败时 stderr 就是能直接上界面的人话。
52
- */
53
- export declare function runLedgerCommand(sub: string, args: readonly string[], opts?: RunOptions): Promise<LedgerCommandResult>;
@@ -11,13 +11,6 @@ import { fileURLToPath } from "node:url";
11
11
  const DEFAULT_SCRIPT_PATH = fileURLToPath(new URL("../../../skill/scripts/progress", import.meta.url));
12
12
  /** 单次脚本调用超时上限(ms)。实测单仓 5 万行全量 1.2s,10s 是宽松上限。 */
13
13
  const DEFAULT_TIMEOUT_MS = 10_000;
14
- /**
15
- * 写命令的超时上限(ms):脚本每条 git 调用各自 30s 超时,写前(fetch / ff / push /
16
- * rebase / push)与写后(push / fetch / rebase / push)串起来最坏约 270s。
17
- * 低到 120s 会在网络挂死时先报「超时失败」——台账其实已写入,用户一重试就是二次写入。
18
- * 180s 覆盖常见最坏组合,又不至于让真卡死的请求占着界面太久。
19
- */
20
- export const WRITE_TIMEOUT_MS = 180_000;
21
14
  /**
22
15
  * 起一次台账程序、收 stdout / stderr。出错一律归一成人话(起不了程序 / 超时 / 非零退出)。
23
16
  * 解析留给调用方:读命令要 JSON,写命令要原文。
@@ -27,7 +20,6 @@ function exec(sub, args, opts) {
27
20
  const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
28
21
  return new Promise((resolve) => {
29
22
  const child = spawn(scriptPath, [sub, ...args], {
30
- cwd: opts?.cwd,
31
23
  stdio: ["ignore", "pipe", "pipe"],
32
24
  });
33
25
  let stdout = "";
@@ -79,7 +71,6 @@ function exec(sub, args, opts) {
79
71
  * @param args - 参数数组,由调用方自拼(含 --repo / --json / --months 等)
80
72
  * @param opts.scriptPath - 测试用:覆盖脚本路径
81
73
  * @param opts.timeoutMs - 覆盖超时
82
- * @param opts.cwd - 子进程工作目录(写命令按 cwd 定位台账用)
83
74
  */
84
75
  export async function runLedgerScript(sub, args, opts) {
85
76
  const result = await exec(sub, args, opts);
@@ -95,17 +86,3 @@ export async function runLedgerScript(sub, args, opts) {
95
86
  };
96
87
  }
97
88
  }
98
- /**
99
- * 写命令(claim / set):同样是 spawn 入口脚本,但**不解析 JSON**——这些子命令打的是人话,
100
- * 成功与否看退出码,失败时 stderr 就是能直接上界面的人话。
101
- */
102
- export async function runLedgerCommand(sub, args, opts) {
103
- const result = await exec(sub, args, opts);
104
- if (!result.ok)
105
- return result;
106
- const output = result.stdout.trim();
107
- // 退出码 0 但 stderr 有字 = 写成功、同步失败(台账程序的既定行为),必须带出去
108
- return result.stderr
109
- ? { ok: true, output, notice: result.stderr }
110
- : { ok: true, output };
111
- }
@@ -0,0 +1,36 @@
1
+ import type { LedgerWorkspace } from "../types.js";
2
+ /** `stats --json` 的领域投影(只收界面用到的面)。 */
3
+ export interface WorkspaceStats {
4
+ readonly total: number;
5
+ readonly open: number;
6
+ readonly doing: number;
7
+ readonly stale: number;
8
+ readonly overdue: number;
9
+ readonly backlog: number;
10
+ readonly mine: number;
11
+ readonly myDoing: number;
12
+ readonly myStale: number;
13
+ readonly myOverdue: number;
14
+ readonly oldestOpenDays: number | null;
15
+ readonly staleDays: number;
16
+ readonly overdueDays: number;
17
+ readonly backlogDays: number;
18
+ }
19
+ /** 一个时间窗口内的结案数(`history --count-only`)。 */
20
+ export interface ClosedCount {
21
+ readonly count: number;
22
+ /** count 到了 skill 的封顶(10000)⇒ 真实值只多不少。 */
23
+ readonly capped: boolean;
24
+ }
25
+ /** 单工作区在办统计(skill `stats --json`;过 1.5s 读缓存,同屏多端点复用)。 */
26
+ export declare function readStats(workspace: LedgerWorkspace): Promise<WorkspaceStats>;
27
+ /** `stats --json` → {@link WorkspaceStats}(容器级严格:缺字段即坏,不当 0)。 */
28
+ export declare function parseStats(raw: unknown, where: string): WorkspaceStats;
29
+ /**
30
+ * 一个时间窗口内结案了多少条(skill `history --count-only`)。
31
+ * 区间写进缓存键:本周 / 上周两个窗口各算一次,别互相串味。
32
+ */
33
+ export declare function countClosed(workspace: LedgerWorkspace, window: {
34
+ readonly from: string;
35
+ readonly to: string;
36
+ }): Promise<ClosedCount>;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * 统计面取数:8 个卡片的数字**全部**来自 skill 方法,插件不自己数。
3
+ *
4
+ * - 在办各面(待认领 / 进行中 / 积压 / 逾期 / 停滞 / 我的)→ `stats --json`;
5
+ * 阈值、mine 判定、最老待认领天数都由 skill 算好回显。
6
+ * - 本周 / 上周完成 → `history --count-only`(结案时间落在区间内;区间界线由插件算,日期窗口归插件)。
7
+ *
8
+ * 形状不符一律抛 {@link LedgerDataError}(不拿 0 充数)。
9
+ */
10
+ import { LedgerDataError } from "../errors.js";
11
+ import { cached, cacheKey } from "./cache.js";
12
+ import { runLedgerScript } from "./runner.js";
13
+ function isPlainObject(value) {
14
+ return value !== null && typeof value === "object" && !Array.isArray(value);
15
+ }
16
+ function num(source, key, where) {
17
+ const value = source[key];
18
+ if (typeof value !== "number" || !Number.isFinite(value))
19
+ throw new LedgerDataError(where, `${where}: stats 输出缺少数字字段 ${key},格式可能已变`);
20
+ return value;
21
+ }
22
+ /** 单工作区在办统计(skill `stats --json`;过 1.5s 读缓存,同屏多端点复用)。 */
23
+ export async function readStats(workspace) {
24
+ return await cached(cacheKey(workspace.id, "stats", ""), async () => {
25
+ const result = await runLedgerScript("stats", [
26
+ "--repo",
27
+ workspace.path,
28
+ "--json",
29
+ ]);
30
+ if (!result.ok)
31
+ throw new Error(`${workspace.title}: ${result.error}`);
32
+ return parseStats(result.data, workspace.title);
33
+ });
34
+ }
35
+ /** `stats --json` → {@link WorkspaceStats}(容器级严格:缺字段即坏,不当 0)。 */
36
+ export function parseStats(raw, where) {
37
+ if (!isPlainObject(raw))
38
+ throw new LedgerDataError(where, `${where}: stats 输出不是对象,格式可能已变`);
39
+ const thresholds = raw.thresholds;
40
+ if (!isPlainObject(thresholds))
41
+ throw new LedgerDataError(where, `${where}: stats 输出缺 thresholds,阈值不许由插件硬编码`);
42
+ const oldest = raw.oldest_open_days;
43
+ if (oldest !== null && typeof oldest !== "number")
44
+ throw new LedgerDataError(where, `${where}: stats 的 oldest_open_days 既不是数字也不是 null`);
45
+ return {
46
+ total: num(raw, "total", where),
47
+ open: num(raw, "open", where),
48
+ doing: num(raw, "doing", where),
49
+ stale: num(raw, "stale", where),
50
+ overdue: num(raw, "overdue", where),
51
+ backlog: num(raw, "backlog", where),
52
+ mine: num(raw, "mine", where),
53
+ myDoing: num(raw, "my_doing", where),
54
+ myStale: num(raw, "my_stale", where),
55
+ myOverdue: num(raw, "my_overdue", where),
56
+ oldestOpenDays: oldest,
57
+ staleDays: num(thresholds, "stale_days", where),
58
+ overdueDays: num(thresholds, "overdue_days", where),
59
+ backlogDays: num(thresholds, "backlog_days", where),
60
+ };
61
+ }
62
+ /**
63
+ * 一个时间窗口内结案了多少条(skill `history --count-only`)。
64
+ * 区间写进缓存键:本周 / 上周两个窗口各算一次,别互相串味。
65
+ */
66
+ export async function countClosed(workspace, window) {
67
+ return await cached(cacheKey(workspace.id, "history", `count:${window.from}~${window.to}`), async () => {
68
+ const result = await runLedgerScript("history", [
69
+ "--repo",
70
+ workspace.path,
71
+ "--count-only",
72
+ "--since",
73
+ window.from,
74
+ "--until",
75
+ window.to,
76
+ ]);
77
+ if (!result.ok)
78
+ throw new Error(`${workspace.title}: ${result.error}`);
79
+ if (!isPlainObject(result.data))
80
+ throw new LedgerDataError(workspace.title, `${workspace.title}: history --count-only 输出不是对象`);
81
+ const count = result.data.count;
82
+ if (typeof count !== "number" || !Number.isFinite(count))
83
+ throw new LedgerDataError(workspace.title, `${workspace.title}: history --count-only 缺 count 数字`);
84
+ return { count, capped: result.data.capped === true };
85
+ });
86
+ }