dsh-task-ledger 0.0.78 → 0.0.80
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.js +29 -14
- package/dist/client.js.map +1 -1
- package/dist/host/board.d.ts +2 -2
- package/dist/host/board.js +2 -2
- package/dist/host/repositories/cache.js +18 -2
- package/dist/host/repositories/identity.repository.d.ts +3 -8
- package/dist/host/repositories/identity.repository.js +32 -23
- package/dist/host/repositories/ledger.repository.d.ts +7 -3
- package/dist/host/repositories/ledger.repository.js +23 -14
- package/dist/host/repositories/records.js +20 -1
- package/dist/host/repositories/runner.d.ts +2 -2
- package/dist/host/repositories/runner.js +1 -1
- package/dist/host/routes.js +40 -11
- package/dist/host/services/home.service.d.ts +2 -2
- package/dist/host/services/home.service.js +7 -4
- package/dist/host/services/ledger-write.service.d.ts +4 -4
- package/dist/host/services/ledger-write.service.js +7 -7
- package/dist/host/services/trend.service.d.ts +2 -2
- package/dist/host/services/trend.service.js +9 -10
- package/dist/host/services/uuid.d.ts +11 -0
- package/dist/host/services/uuid.js +15 -0
- package/dist/host/types.d.ts +21 -5
- package/package.json +1 -1
- package/skill/README.md +1 -1
- package/skill/SKILL.md +30 -17
- package/skill/package.json +1 -1
- package/skill/references/state-model.md +12 -10
- package/skill/references/troubleshooting.md +19 -7
- package/skill/scripts/ledgerkit/actions/atom.py +2 -2
- package/skill/scripts/ledgerkit/actions/bootstrap.py +9 -2
- package/skill/scripts/ledgerkit/actions/claim.py +7 -6
- package/skill/scripts/ledgerkit/actions/close_task.py +17 -12
- package/skill/scripts/ledgerkit/actions/guard.py +38 -16
- package/skill/scripts/ledgerkit/actions/handoff.py +24 -8
- package/skill/scripts/ledgerkit/actions/init_identity.py +90 -0
- package/skill/scripts/ledgerkit/actions/list_tasks.py +21 -3
- package/skill/scripts/ledgerkit/actions/log_progress.py +5 -2
- package/skill/scripts/ledgerkit/actions/new_task.py +11 -7
- package/skill/scripts/ledgerkit/actions/query.py +4 -2
- package/skill/scripts/ledgerkit/actions/query_history.py +55 -9
- package/skill/scripts/ledgerkit/actions/registry.py +1 -1
- package/skill/scripts/ledgerkit/actions/release.py +25 -4
- package/skill/scripts/ledgerkit/actions/set_task.py +18 -7
- package/skill/scripts/ledgerkit/actions/validate.py +0 -4
- package/skill/scripts/ledgerkit/core/history_md.py +47 -250
- package/skill/scripts/ledgerkit/core/identity.py +34 -7
- package/skill/scripts/ledgerkit/core/labels.py +0 -1
- package/skill/scripts/ledgerkit/core/paths.py +0 -1
- package/skill/scripts/ledgerkit/core/worklog_md.py +23 -3
- package/skill/scripts/ledgerkit/env.py +70 -71
- package/skill/scripts/ledgerkit/shell/cli.py +60 -3
- package/skill/scripts/ledgerkit/shell/gitops.py +14 -1
- package/skill/scripts/ledgerkit/shell/render.py +3 -2
- package/skill/scripts/ledgerkit/shell/runner.py +1 -1
- package/skill/scripts/ledgerkit/store/history_store.py +26 -81
- package/skill/scripts/ledgerkit/store/owners_index.py +16 -3
- package/skill/scripts/ledgerkit/store/task_store.py +2 -2
package/dist/host/board.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 首页**组合入口**(`/board` 兼容全量端点用):一次读全(tasks + history +
|
|
2
|
+
* 首页**组合入口**(`/board` 兼容全量端点用):一次读全(tasks + history + 本机身份),
|
|
3
3
|
* 喂快 / 慢两条纯计算——**读一次算两样,不重复 IO**。
|
|
4
4
|
*
|
|
5
5
|
* 拆端点后(host-data-plane §五 A)首屏走 `services/home.service.ts::getSnapshot()`(不碰 history),
|
|
@@ -9,7 +9,7 @@ import { type DegradedInfo } from "./errors.js";
|
|
|
9
9
|
import type { HomeAggregate, LedgerWorkspace } from "./types.js";
|
|
10
10
|
/**
|
|
11
11
|
* 首页一次拿全:在办快照(含停滞 / 天数 / 身份)+ 近 8 周趋势(完成 / 新增 × 所有 / 我的)
|
|
12
|
-
* + 四个自然周期计数。**每个工作区只读一次**(list + history +
|
|
12
|
+
* + 四个自然周期计数。**每个工作区只读一次**(list + history + 本机身份),
|
|
13
13
|
* 避免「快照读一遍、统计再读一遍」把 5 万行级工作区的 history 解析翻倍。
|
|
14
14
|
* 单工作区坏 ⇒ 进 degraded(其余照常);全坏 ⇒ 抛(路由转 500 + 人话)。
|
|
15
15
|
*/
|
package/dist/host/board.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 首页**组合入口**(`/board` 兼容全量端点用):一次读全(tasks + history +
|
|
2
|
+
* 首页**组合入口**(`/board` 兼容全量端点用):一次读全(tasks + history + 本机身份),
|
|
3
3
|
* 喂快 / 慢两条纯计算——**读一次算两样,不重复 IO**。
|
|
4
4
|
*
|
|
5
5
|
* 拆端点后(host-data-plane §五 A)首屏走 `services/home.service.ts::getSnapshot()`(不碰 history),
|
|
@@ -10,7 +10,7 @@ import { computeSnapshot } from "./services/home.service.js";
|
|
|
10
10
|
import { computeClosedSummary, computeTrendPeriods, readTrendScope, } from "./services/trend.service.js";
|
|
11
11
|
/**
|
|
12
12
|
* 首页一次拿全:在办快照(含停滞 / 天数 / 身份)+ 近 8 周趋势(完成 / 新增 × 所有 / 我的)
|
|
13
|
-
* + 四个自然周期计数。**每个工作区只读一次**(list + history +
|
|
13
|
+
* + 四个自然周期计数。**每个工作区只读一次**(list + history + 本机身份),
|
|
14
14
|
* 避免「快照读一遍、统计再读一遍」把 5 万行级工作区的 history 解析翻倍。
|
|
15
15
|
* 单工作区坏 ⇒ 进 degraded(其余照常);全坏 ⇒ 抛(路由转 500 + 人话)。
|
|
16
16
|
*/
|
|
@@ -16,6 +16,12 @@ const SEP = "\u0000";
|
|
|
16
16
|
const settled = new Map();
|
|
17
17
|
/** 在途单飞:key → 正在跑的 Promise(同 key 并发只跑一次)。 */
|
|
18
18
|
const inflight = new Map();
|
|
19
|
+
/**
|
|
20
|
+
* 失效代次:`invalidate()` 递增一次。
|
|
21
|
+
* 在飞的 Promise 落地时若代次变了就不写回 `settled`——否则「清缓存 → 旧读回来又填上」,
|
|
22
|
+
* 手动刷新 / 写完立刻刷新都会看到旧数据。
|
|
23
|
+
*/
|
|
24
|
+
let epoch = 0;
|
|
19
25
|
/** 缓存键:`op:workspace:filter` 三条信息都要,否则不同筛选 / 不同工作区会串味。 */
|
|
20
26
|
export function cacheKey(workspaceId, op, detail) {
|
|
21
27
|
return `${op}${SEP}${workspaceId}${SEP}${detail}`;
|
|
@@ -31,14 +37,17 @@ export function cached(key, load, now = Date.now()) {
|
|
|
31
37
|
const pending = inflight.get(key);
|
|
32
38
|
if (pending !== undefined)
|
|
33
39
|
return pending;
|
|
40
|
+
const issued = epoch;
|
|
34
41
|
const task = Promise.resolve()
|
|
35
42
|
.then(load)
|
|
36
43
|
.then((value) => {
|
|
37
|
-
|
|
44
|
+
if (issued === epoch)
|
|
45
|
+
settled.set(key, { at: Date.now(), value });
|
|
38
46
|
return value;
|
|
39
47
|
})
|
|
40
48
|
.finally(() => {
|
|
41
|
-
inflight.
|
|
49
|
+
if (inflight.get(key) === task)
|
|
50
|
+
inflight.delete(key);
|
|
42
51
|
});
|
|
43
52
|
inflight.set(key, task);
|
|
44
53
|
return task;
|
|
@@ -48,12 +57,19 @@ export function cached(key, load, now = Date.now()) {
|
|
|
48
57
|
* 写路径(任务 4,未拍板)落地后在其成功后调本函数;现在只有手动刷新这一处用。
|
|
49
58
|
*/
|
|
50
59
|
export function invalidate(workspaceId) {
|
|
60
|
+
// 代次一律递增:在飞的结果落地时不再写回(按工作区区分度太细,多跑一次脚本不心疼)
|
|
61
|
+
epoch += 1;
|
|
51
62
|
if (workspaceId === undefined) {
|
|
52
63
|
settled.clear();
|
|
64
|
+
inflight.clear();
|
|
53
65
|
return;
|
|
54
66
|
}
|
|
55
67
|
for (const key of settled.keys()) {
|
|
56
68
|
if (key.split(SEP)[1] === workspaceId)
|
|
57
69
|
settled.delete(key);
|
|
58
70
|
}
|
|
71
|
+
for (const key of inflight.keys()) {
|
|
72
|
+
if (key.split(SEP)[1] === workspaceId)
|
|
73
|
+
inflight.delete(key);
|
|
74
|
+
}
|
|
59
75
|
}
|
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
import type { LedgerWorkspace } from "../types.js";
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* 该工作区的「我的」统计按 0 计(不谎报)。
|
|
6
|
-
*
|
|
7
|
-
* 全 host 唯一 spawn git 的地方(python3 的唯一出口在 ./runner.ts)。
|
|
8
|
-
*/
|
|
9
|
-
export declare function getCurrentIdentity(workspace: LedgerWorkspace): Promise<string | null>;
|
|
2
|
+
/** 机器级身份文件路径(与 skill 侧 `env.identity_file()` 同一口径)。 */
|
|
3
|
+
export declare function identityPath(): string;
|
|
4
|
+
export declare function getCurrentIdentity(_workspace?: LedgerWorkspace): Promise<string | null>;
|
|
@@ -1,27 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
/**
|
|
3
|
-
*
|
|
4
|
-
* 与 agent 在会话里跑 skill 时的取值环境一致;取不到回 null(非 git 目录 / 没配身份),
|
|
5
|
-
* 该工作区的「我的」统计按 0 计(不谎报)。
|
|
5
|
+
* 本机身份 uuid —— 「我的待办 / 我的结案」判定的**唯一**来源。
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* 身份是**机器级**的:skill 的 `init` 把 `{uuid, name}` 写进
|
|
8
|
+
* `$TASK_LEDGER_HOME/.taskledger/identity.json`(默认 `~/.taskledger/identity.json`),
|
|
9
|
+
* 与工作区、git 配置都无关;任务与历史里的 `owner` / `creator` 存的正是这个 uuid。
|
|
10
|
+
* 参数保留是为了与其它 per-workspace 读法同形(同一个 uuid 对所有工作区都成立)。
|
|
11
|
+
*
|
|
12
|
+
* **只取 `uuid`**:`name` 是冗余显示字段,不参与任何判定(按名字比 owner 永远比不到)。
|
|
13
|
+
* 读不到回 null(没 init / 文件坏 / 没 home 目录),该工作区的「我的」统计按 0 计(不谎报)。
|
|
8
14
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
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
|
+
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 {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
27
36
|
}
|
|
@@ -2,11 +2,15 @@ import type { HistoryRecord, LedgerFilter, LedgerWorkspace, TaskRow } from "../t
|
|
|
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
8
|
*/
|
|
6
|
-
export declare function filterArgs(filter: LedgerFilter): string[];
|
|
9
|
+
export declare function filterArgs(filter: LedgerFilter, sub: "list" | "history"): string[];
|
|
7
10
|
/**
|
|
8
|
-
* 台账工作区探测:`<path>/ledger/
|
|
9
|
-
* 2026-10-17 拍板)。全 host 对台账目录的 IO
|
|
11
|
+
* 台账工作区探测:`<path>/ledger/pool.json` 在才算(分片布局的初始化标志;
|
|
12
|
+
* 未初始化 = 后端不可见,2026-10-17 拍板)。全 host 对台账目录的 IO 只在本文件
|
|
13
|
+
* ——别处再写 existsSync 即为分层倒退。
|
|
10
14
|
*/
|
|
11
15
|
export declare function hasLedger(workspacePath: string): boolean;
|
|
12
16
|
/**
|
|
@@ -9,13 +9,19 @@ const LEDGER_DIR = "ledger";
|
|
|
9
9
|
/**
|
|
10
10
|
* LedgerFilter → skill 脚本参数(只拼调用方给了的;undefined 一律不拼,
|
|
11
11
|
* 让脚本走自己的默认——插件不替脚本决定默认值)。
|
|
12
|
+
*
|
|
13
|
+
* **按子命令裁参数**:`list` 不认 `--limit` / `--months`,`history` 不认 `--status`,
|
|
14
|
+
* 拼错一个就让 argparse 退 2、整块列表 500。口径以 `skill/shell/cli.py` 为准。
|
|
12
15
|
*/
|
|
13
|
-
export function filterArgs(filter) {
|
|
16
|
+
export function filterArgs(filter, sub) {
|
|
14
17
|
const args = [];
|
|
15
|
-
if (filter.status !== undefined)
|
|
18
|
+
if (sub === "list" && filter.status !== undefined)
|
|
16
19
|
args.push("--status", filter.status);
|
|
20
|
+
// 身份只收 uuid(脚本同样口径);按名字模糊筛走 --owner-name
|
|
17
21
|
if (filter.owner !== undefined)
|
|
18
22
|
args.push("--owner", filter.owner);
|
|
23
|
+
if (filter.ownerName !== undefined)
|
|
24
|
+
args.push("--owner-name", filter.ownerName);
|
|
19
25
|
if (filter.category !== undefined)
|
|
20
26
|
args.push("--category", filter.category);
|
|
21
27
|
if (filter.tag !== undefined)
|
|
@@ -24,15 +30,18 @@ export function filterArgs(filter) {
|
|
|
24
30
|
args.push("--since", filter.since);
|
|
25
31
|
if (filter.until !== undefined)
|
|
26
32
|
args.push("--until", filter.until);
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
+
}
|
|
31
39
|
return args;
|
|
32
40
|
}
|
|
33
41
|
/**
|
|
34
|
-
* 台账工作区探测:`<path>/ledger/
|
|
35
|
-
* 2026-10-17 拍板)。全 host 对台账目录的 IO
|
|
42
|
+
* 台账工作区探测:`<path>/ledger/pool.json` 在才算(分片布局的初始化标志;
|
|
43
|
+
* 未初始化 = 后端不可见,2026-10-17 拍板)。全 host 对台账目录的 IO 只在本文件
|
|
44
|
+
* ——别处再写 existsSync 即为分层倒退。
|
|
36
45
|
*/
|
|
37
46
|
export function hasLedger(workspacePath) {
|
|
38
47
|
// 分片布局(2026-10-23):公共池 pool.json 在才算初始化;各人分片 owners/*.json 由脚本维护。
|
|
@@ -44,12 +53,12 @@ export function hasLedger(workspacePath) {
|
|
|
44
53
|
* 不静默返空(空 = 撒谎)。**过 1.5s 读缓存**(同屏多端点会反复读同一份在办)。
|
|
45
54
|
*/
|
|
46
55
|
export async function readTasks(workspace, filter = {}) {
|
|
47
|
-
return await cached(cacheKey(workspace.id, "list", argsKey(filter)), async () => {
|
|
56
|
+
return await cached(cacheKey(workspace.id, "list", argsKey(filter, "list")), async () => {
|
|
48
57
|
const result = await runLedgerScript("list", [
|
|
49
58
|
"--repo",
|
|
50
59
|
workspace.path,
|
|
51
60
|
"--json",
|
|
52
|
-
...filterArgs(filter),
|
|
61
|
+
...filterArgs(filter, "list"),
|
|
53
62
|
]);
|
|
54
63
|
if (!result.ok)
|
|
55
64
|
throw new Error(`${workspace.title}: ${result.error}`);
|
|
@@ -58,12 +67,12 @@ export async function readTasks(workspace, filter = {}) {
|
|
|
58
67
|
}
|
|
59
68
|
/** 已结案记录(包 `history --json`;`records` 必须存在且为数组,缺了即坏数据)。同样过读缓存。 */
|
|
60
69
|
export async function readHistory(workspace, filter = {}) {
|
|
61
|
-
return await cached(cacheKey(workspace.id, "history", argsKey(filter)), async () => {
|
|
70
|
+
return await cached(cacheKey(workspace.id, "history", argsKey(filter, "history")), async () => {
|
|
62
71
|
const result = await runLedgerScript("history", [
|
|
63
72
|
"--repo",
|
|
64
73
|
workspace.path,
|
|
65
74
|
"--json",
|
|
66
|
-
...filterArgs(filter),
|
|
75
|
+
...filterArgs(filter, "history"),
|
|
67
76
|
]);
|
|
68
77
|
if (!result.ok)
|
|
69
78
|
throw new Error(`${workspace.title}: ${result.error}`);
|
|
@@ -71,8 +80,8 @@ export async function readHistory(workspace, filter = {}) {
|
|
|
71
80
|
});
|
|
72
81
|
}
|
|
73
82
|
/** 缓存键的 filter 段:脚本参数原样序列化(分隔符取不可见字符,值里带空格也不会串)。 */
|
|
74
|
-
function argsKey(filter) {
|
|
75
|
-
return filterArgs(filter).join("\u0001");
|
|
83
|
+
function argsKey(filter, sub) {
|
|
84
|
+
return filterArgs(filter, sub).join("\u0001");
|
|
76
85
|
}
|
|
77
86
|
/** 单条在办(展开行 / 详情用)。取不到回 null,不抛。 */
|
|
78
87
|
export async function getTask(workspace, taskId) {
|
|
@@ -42,7 +42,26 @@ export function parseHistoryRecords(raw, workspace) {
|
|
|
42
42
|
for (const record of records) {
|
|
43
43
|
if (!isPlainObject(record))
|
|
44
44
|
continue; // 元素级宽松:坏行跳过,不整板炸
|
|
45
|
-
rows.push(
|
|
45
|
+
rows.push(toHistoryRecord(record, workspace));
|
|
46
46
|
}
|
|
47
47
|
return rows;
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* 脚本 9 列的键 → 领域字段名。
|
|
51
|
+
*
|
|
52
|
+
* 脚本(`history --json`)吐的是**列名**:`finished` / `title` / `worklog`;
|
|
53
|
+
* 领域层用的是 `time` / `workpackage` / `log`。两个名字都得能用:
|
|
54
|
+
* 不映射的话结案时间会落到 `created`、标题落到摘要、趋势图的「完成数」恒 0(`dayOf(undefined)`)。
|
|
55
|
+
* 两边都给:脚本将来改回列名以外的实现也不会炸。
|
|
56
|
+
*/
|
|
57
|
+
function toHistoryRecord(source, workspace) {
|
|
58
|
+
const text = (value) => typeof value === "string" && value !== "" ? value : undefined;
|
|
59
|
+
return {
|
|
60
|
+
...source,
|
|
61
|
+
// 脚本里的列名是真源;领域字段名作为兼容保留
|
|
62
|
+
time: text(source.finished) ?? text(source.time),
|
|
63
|
+
workpackage: text(source.title) ?? text(source.workpackage),
|
|
64
|
+
log: text(source.worklog) ?? text(source.log),
|
|
65
|
+
workspace,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -31,7 +31,7 @@ export interface RunOptions {
|
|
|
31
31
|
timeoutMs?: number;
|
|
32
32
|
/**
|
|
33
33
|
* 子进程 cwd。写命令(claim / set)必须落在工作区:
|
|
34
|
-
*
|
|
34
|
+
* 脚本按 cwd 逐级往上找 `ledger/`(或靠 `--repo` 指定),与身份无关。
|
|
35
35
|
*/
|
|
36
36
|
cwd?: string;
|
|
37
37
|
}
|
|
@@ -43,7 +43,7 @@ export interface RunOptions {
|
|
|
43
43
|
* @param args - 参数数组,由调用方自拼(含 --repo / --json / --months 等)
|
|
44
44
|
* @param opts.scriptPath - 测试用:覆盖脚本路径
|
|
45
45
|
* @param opts.timeoutMs - 覆盖超时
|
|
46
|
-
* @param opts.cwd -
|
|
46
|
+
* @param opts.cwd - 子进程工作目录(写命令按 cwd 定位台账用)
|
|
47
47
|
*/
|
|
48
48
|
export declare function runLedgerScript<T = unknown>(sub: string, args: readonly string[], opts?: RunOptions): Promise<LedgerScriptResult<T>>;
|
|
49
49
|
/**
|
|
@@ -76,7 +76,7 @@ function exec(sub, args, opts) {
|
|
|
76
76
|
* @param args - 参数数组,由调用方自拼(含 --repo / --json / --months 等)
|
|
77
77
|
* @param opts.scriptPath - 测试用:覆盖脚本路径
|
|
78
78
|
* @param opts.timeoutMs - 覆盖超时
|
|
79
|
-
* @param opts.cwd -
|
|
79
|
+
* @param opts.cwd - 子进程工作目录(写命令按 cwd 定位台账用)
|
|
80
80
|
*/
|
|
81
81
|
export async function runLedgerScript(sub, args, opts) {
|
|
82
82
|
const result = await exec(sub, args, opts);
|
package/dist/host/routes.js
CHANGED
|
@@ -12,7 +12,18 @@ function sendJson(res, status, body) {
|
|
|
12
12
|
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
13
13
|
res.end(JSON.stringify(body));
|
|
14
14
|
}
|
|
15
|
-
/**
|
|
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
|
+
*/
|
|
16
27
|
function filterFromQuery(url) {
|
|
17
28
|
const filter = {};
|
|
18
29
|
const str = (key) => {
|
|
@@ -30,26 +41,36 @@ function filterFromQuery(url) {
|
|
|
30
41
|
if (status !== null)
|
|
31
42
|
filter.status = status;
|
|
32
43
|
const owner = str("owner");
|
|
33
|
-
if (owner !== null)
|
|
34
|
-
|
|
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;
|
|
35
53
|
const category = str("category");
|
|
36
54
|
if (category !== null)
|
|
37
55
|
filter.category = category;
|
|
38
56
|
const tag = str("tag");
|
|
39
57
|
if (tag !== null)
|
|
40
58
|
filter.tag = tag;
|
|
41
|
-
const since = str("since");
|
|
42
|
-
if (since !== null)
|
|
43
|
-
filter.since = since;
|
|
44
|
-
const until = str("until");
|
|
45
|
-
if (until !== null)
|
|
46
|
-
filter.until = until;
|
|
47
59
|
const limit = int("limit");
|
|
48
60
|
if (limit !== undefined)
|
|
49
61
|
filter.limit = limit;
|
|
50
62
|
const months = int("months");
|
|
51
|
-
if (months !== undefined)
|
|
63
|
+
if (months !== undefined) {
|
|
52
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
|
+
}
|
|
53
74
|
return filter;
|
|
54
75
|
}
|
|
55
76
|
/** 每请求重新解析工作区(registry 可能刚变;探测走 hasLedger(),微秒级,不缓存)。 */
|
|
@@ -93,7 +114,7 @@ export function registerRoutes(webServer, deps) {
|
|
|
93
114
|
}
|
|
94
115
|
});
|
|
95
116
|
// ── 拆端点(2026-10-18 步骤 4a:纯加法,旧 /board 全量原样保留)──
|
|
96
|
-
// 快路径:首屏。只读 progress.json +
|
|
117
|
+
// 快路径:首屏。只读 progress.json + 本机身份,**不碰 history** ⇒ 耗时与历史总量解耦
|
|
97
118
|
// (20 万条量级实测 527ms vs 合并端点 4.4s)。
|
|
98
119
|
registerRead("/board/snapshot", async (_req, res) => {
|
|
99
120
|
try {
|
|
@@ -129,6 +150,10 @@ export function registerRoutes(webServer, deps) {
|
|
|
129
150
|
sendJson(res, 200, { tasks: perWorkspace.flat() });
|
|
130
151
|
}
|
|
131
152
|
catch (error) {
|
|
153
|
+
if (error instanceof UsageError) {
|
|
154
|
+
sendJson(res, 400, { error: error.message });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
132
157
|
sendJson(res, 500, {
|
|
133
158
|
error: error instanceof Error ? error.message : String(error),
|
|
134
159
|
});
|
|
@@ -167,6 +192,10 @@ export function registerRoutes(webServer, deps) {
|
|
|
167
192
|
});
|
|
168
193
|
}
|
|
169
194
|
catch (error) {
|
|
195
|
+
if (error instanceof UsageError) {
|
|
196
|
+
sendJson(res, 400, { error: error.message });
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
170
199
|
sendJson(res, 500, {
|
|
171
200
|
error: error instanceof Error ? error.message : String(error),
|
|
172
201
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 首页**快路径**用例:在办快照 + 统计卡。
|
|
3
|
-
* 只读 progress.json(list)+
|
|
3
|
+
* 只读 progress.json(list)+ 本机身份,**不碰 history**——首屏耗时与历史总量解耦
|
|
4
4
|
* (20 万条量级实测:527ms vs 合并端点 4.4s,见 docs/design/host-data-plane.md §四)。
|
|
5
5
|
* 慢路径(趋势)在 ./trend.service.ts;组合入口(/board 兼容全量)在 ../board.ts。
|
|
6
6
|
*/
|
|
@@ -12,7 +12,7 @@ export declare const STALE_DAYS = 3;
|
|
|
12
12
|
export declare const OVERDUE_DAYS = 14;
|
|
13
13
|
/** 积压阈值:待认领且 created 至今满这么多天还没人接(2026-10-19 拍板)。 */
|
|
14
14
|
export declare const BACKLOG_DAYS = 30;
|
|
15
|
-
/** 快路径读法:单工作区 readTasks +
|
|
15
|
+
/** 快路径读法:单工作区 readTasks + 本机身份(并行;供 settle 逐工作区调)。 */
|
|
16
16
|
export declare function readTaskScope(workspace: LedgerWorkspace): Promise<TaskScope>;
|
|
17
17
|
/** 在办行加工 + 六个统计面 + identities(纯函数;`now` 显式传入)。 */
|
|
18
18
|
export declare function computeSnapshot(scopes: readonly TaskScope[], now: Date): BoardSnapshot;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 首页**快路径**用例:在办快照 + 统计卡。
|
|
3
|
-
* 只读 progress.json(list)+
|
|
3
|
+
* 只读 progress.json(list)+ 本机身份,**不碰 history**——首屏耗时与历史总量解耦
|
|
4
4
|
* (20 万条量级实测:527ms vs 合并端点 4.4s,见 docs/design/host-data-plane.md §四)。
|
|
5
5
|
* 慢路径(趋势)在 ./trend.service.ts;组合入口(/board 兼容全量)在 ../board.ts。
|
|
6
6
|
*/
|
|
7
7
|
import { settleWorkspaces } from "../errors.js";
|
|
8
|
+
import { sameUuid } from "./uuid.js";
|
|
8
9
|
import { getCurrentIdentity } from "../repositories/identity.repository.js";
|
|
9
10
|
import { readTasks } from "../repositories/ledger.repository.js";
|
|
10
11
|
import { daysSince } from "./dates.js";
|
|
@@ -14,7 +15,7 @@ export const STALE_DAYS = 3;
|
|
|
14
15
|
export const OVERDUE_DAYS = 14;
|
|
15
16
|
/** 积压阈值:待认领且 created 至今满这么多天还没人接(2026-10-19 拍板)。 */
|
|
16
17
|
export const BACKLOG_DAYS = 30;
|
|
17
|
-
/** 快路径读法:单工作区 readTasks +
|
|
18
|
+
/** 快路径读法:单工作区 readTasks + 本机身份(并行;供 settle 逐工作区调)。 */
|
|
18
19
|
export async function readTaskScope(workspace) {
|
|
19
20
|
const [tasks, identity] = await Promise.all([
|
|
20
21
|
readTasks(workspace),
|
|
@@ -48,12 +49,14 @@ export function computeSnapshot(scopes, now) {
|
|
|
48
49
|
}));
|
|
49
50
|
const openTasks = withMeta.filter((task) => task.status === "open");
|
|
50
51
|
const myActive = scopes.reduce((sum, scope) => sum +
|
|
51
|
-
scope.tasks.filter((task) =>
|
|
52
|
+
scope.tasks.filter((task) =>
|
|
53
|
+
// identity 为 null(本机没 init)时不许把 owner 为空的 doing 任务算成「我的」
|
|
54
|
+
sameUuid(task.owner, scope.identity) &&
|
|
52
55
|
(task.status === "doing" || task.status === "blocked")).length, 0);
|
|
53
56
|
const identities = {};
|
|
54
57
|
for (const scope of scopes)
|
|
55
58
|
identities[scope.workspace.id] = scope.identity;
|
|
56
|
-
const mine = (task) => task.owner
|
|
59
|
+
const mine = (task) => sameUuid(task.owner, identities[task.workspace.id]);
|
|
57
60
|
const backlogTasks = withMeta.filter((task) => task.backlog);
|
|
58
61
|
const overdueTasks = withMeta.filter((task) => task.overdue);
|
|
59
62
|
const staleTasks = withMeta.filter((task) => task.stale);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { LedgerWorkspace } from "../types.js";
|
|
2
|
-
/** 认领:只有 open 且无人认领才能接(脚本内判定),owner =
|
|
2
|
+
/** 认领:只有 open 且无人认领才能接(脚本内判定),owner = 本机 identity 文件里的 uuid。 */
|
|
3
3
|
export declare function claimTask(workspace: LedgerWorkspace, taskId: string): Promise<string>;
|
|
4
4
|
/**
|
|
5
|
-
* 释放:写回公共池(owner /
|
|
5
|
+
* 释放:写回公共池(owner / owner_name 清空、status 回 open),只许当前 owner 本人(脚本内判定)。
|
|
6
6
|
*
|
|
7
|
-
* 走 `set --owner ''
|
|
8
|
-
*
|
|
7
|
+
* 走 `set --release`。**不能用 `--owner ''`**:脚本不接受「空 owner = 释放」这种隐式语义,
|
|
8
|
+
* 空串会被当成非法 uuid 直接退 1(2026-10-25 修,此前这条路必失败)。
|
|
9
9
|
*/
|
|
10
10
|
export declare function releaseTask(workspace: LedgerWorkspace, taskId: string): Promise<string>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 台账写方法(2026-10-18 拍板 B 混合制):认领 / 释放没有判断量,host 直跑 skill 脚本,**不开会话**。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 身份:脚本只读本机 identity 文件里的 uuid 作为 owner(机器级、与工作区和 git 配置无关),
|
|
5
|
+
* 宿主进程不传、也不推断身份。
|
|
6
6
|
*
|
|
7
7
|
* 校验全在脚本里(认领要求 open 且无 owner;释放要求是当前 owner),本层不重复实现。
|
|
8
8
|
*/
|
|
@@ -17,16 +17,16 @@ async function run(workspace, sub, args) {
|
|
|
17
17
|
throw new Error(`${workspace.title}:${result.error}`);
|
|
18
18
|
return result.notice ?? "";
|
|
19
19
|
}
|
|
20
|
-
/** 认领:只有 open 且无人认领才能接(脚本内判定),owner =
|
|
20
|
+
/** 认领:只有 open 且无人认领才能接(脚本内判定),owner = 本机 identity 文件里的 uuid。 */
|
|
21
21
|
export function claimTask(workspace, taskId) {
|
|
22
22
|
return run(workspace, "claim", ["--id", taskId]);
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
-
* 释放:写回公共池(owner /
|
|
25
|
+
* 释放:写回公共池(owner / owner_name 清空、status 回 open),只许当前 owner 本人(脚本内判定)。
|
|
26
26
|
*
|
|
27
|
-
* 走 `set --owner ''
|
|
28
|
-
*
|
|
27
|
+
* 走 `set --release`。**不能用 `--owner ''`**:脚本不接受「空 owner = 释放」这种隐式语义,
|
|
28
|
+
* 空串会被当成非法 uuid 直接退 1(2026-10-25 修,此前这条路必失败)。
|
|
29
29
|
*/
|
|
30
30
|
export function releaseTask(workspace, taskId) {
|
|
31
|
-
return run(workspace, "set", ["--id", taskId, "--
|
|
31
|
+
return run(workspace, "set", ["--id", taskId, "--release"]);
|
|
32
32
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 首页**慢路径**用例:近 8 周趋势 + 四个自然周期。
|
|
3
|
-
* 读 history(months=RECENT_MONTHS)+ 在办 +
|
|
3
|
+
* 读 history(months=RECENT_MONTHS)+ 在办 + 本机身份——首屏不等它(异步/按需拉)。
|
|
4
4
|
*
|
|
5
5
|
* ⚠️ 窗口必须是 13 个月而不是 2:8 周桶在月初会横跨 3 个自然月(如 10 月第 1 周回溯到 8 月),
|
|
6
6
|
* `--months` 是**文件级**过滤,缩到 2 会静默漏掉 8 月结案记录、趋势少算(2026-10-18 架构评审结论)。
|
|
@@ -11,7 +11,7 @@ import type { ClosedSummary, LedgerFilter, LedgerWorkspace, TrendPeriods, TrendS
|
|
|
11
11
|
export declare const RECENT_MONTHS = 13;
|
|
12
12
|
/** 趋势图桶数:近 8 周(2026-10-17 定稿)。 */
|
|
13
13
|
export declare const WEEKLY_WEEKS = 8;
|
|
14
|
-
/** 慢路径读法:单工作区 readTasks + readHistory(months) +
|
|
14
|
+
/** 慢路径读法:单工作区 readTasks + readHistory(months) + 本机身份(并行;供 settle 逐工作区调)。 */
|
|
15
15
|
export declare function readTrendScope(workspace: LedgerWorkspace): Promise<TrendScope>;
|
|
16
16
|
/**
|
|
17
17
|
* 近 N 周分组桶(完成按结案时间、新增按创建时间)× 所有 / 我的(纯函数)。
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 首页**慢路径**用例:近 8 周趋势 + 四个自然周期。
|
|
3
|
-
* 读 history(months=RECENT_MONTHS)+ 在办 +
|
|
3
|
+
* 读 history(months=RECENT_MONTHS)+ 在办 + 本机身份——首屏不等它(异步/按需拉)。
|
|
4
4
|
*
|
|
5
5
|
* ⚠️ 窗口必须是 13 个月而不是 2:8 周桶在月初会横跨 3 个自然月(如 10 月第 1 周回溯到 8 月),
|
|
6
6
|
* `--months` 是**文件级**过滤,缩到 2 会静默漏掉 8 月结案记录、趋势少算(2026-10-18 架构评审结论)。
|
|
7
7
|
*/
|
|
8
8
|
import { settleWorkspaces } from "../errors.js";
|
|
9
|
+
import { sameUuid } from "./uuid.js";
|
|
9
10
|
import { getCurrentIdentity } from "../repositories/identity.repository.js";
|
|
10
11
|
import { readHistory, readTasks } from "../repositories/ledger.repository.js";
|
|
11
12
|
import { buildWeeks, dayOf, inWindow, mondayOf, monthWindow, weekWindow, ymd, } from "./dates.js";
|
|
@@ -13,7 +14,7 @@ import { buildWeeks, dayOf, inWindow, mondayOf, monthWindow, weekWindow, ymd, }
|
|
|
13
14
|
export const RECENT_MONTHS = 13;
|
|
14
15
|
/** 趋势图桶数:近 8 周(2026-10-17 定稿)。 */
|
|
15
16
|
export const WEEKLY_WEEKS = 8;
|
|
16
|
-
/** 慢路径读法:单工作区 readTasks + readHistory(months) +
|
|
17
|
+
/** 慢路径读法:单工作区 readTasks + readHistory(months) + 本机身份(并行;供 settle 逐工作区调)。 */
|
|
17
18
|
export async function readTrendScope(workspace) {
|
|
18
19
|
const [tasks, history, identity] = await Promise.all([
|
|
19
20
|
readTasks(workspace),
|
|
@@ -59,11 +60,11 @@ export function computeClosedSummary(scopes, now) {
|
|
|
59
60
|
if (created === null)
|
|
60
61
|
continue;
|
|
61
62
|
const week = ymd(mondayOf(created));
|
|
62
|
-
const createdMine =
|
|
63
|
+
const createdMine = sameUuid(task.creator, identity);
|
|
63
64
|
bump(bucketsAll, week, "created");
|
|
64
65
|
if (createdMine)
|
|
65
66
|
bump(bucketsMine, week, "created");
|
|
66
|
-
const ownedMine =
|
|
67
|
+
const ownedMine = sameUuid(task.owner, identity);
|
|
67
68
|
bumpRemaining(bucketsAll, created, null);
|
|
68
69
|
if (ownedMine)
|
|
69
70
|
bumpRemaining(bucketsMine, created, null);
|
|
@@ -73,7 +74,7 @@ export function computeClosedSummary(scopes, now) {
|
|
|
73
74
|
if (closed !== null) {
|
|
74
75
|
const week = ymd(mondayOf(closed));
|
|
75
76
|
bump(bucketsAll, week, "done");
|
|
76
|
-
if (
|
|
77
|
+
if (sameUuid(record.owner, identity))
|
|
77
78
|
bump(bucketsMine, week, "done");
|
|
78
79
|
}
|
|
79
80
|
const created = dayOf(record.created);
|
|
@@ -81,14 +82,12 @@ export function computeClosedSummary(scopes, now) {
|
|
|
81
82
|
const week = ymd(mondayOf(created));
|
|
82
83
|
bump(bucketsAll, week, "created");
|
|
83
84
|
// 新增(我的)= 我创建的(creator);旧 8 列历史无 creator 列 ⇒ 兜底 owner
|
|
84
|
-
const createdMine = identity
|
|
85
|
-
((record.creator ?? null) === identity
|
|
86
|
-
((record.creator ?? null) === null &&
|
|
87
|
-
(record.owner ?? null) === identity));
|
|
85
|
+
const createdMine = sameUuid(record.creator, identity) ||
|
|
86
|
+
((record.creator ?? null) === null && sameUuid(record.owner, identity));
|
|
88
87
|
if (createdMine)
|
|
89
88
|
bump(bucketsMine, week, "created");
|
|
90
89
|
}
|
|
91
|
-
const ownedMine =
|
|
90
|
+
const ownedMine = sameUuid(record.owner, identity);
|
|
92
91
|
bumpRemaining(bucketsAll, created, closed);
|
|
93
92
|
if (ownedMine)
|
|
94
93
|
bumpRemaining(bucketsMine, created, closed);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 身份比较的唯一口径:**两边都 trim + 小写再比**。
|
|
3
|
+
*
|
|
4
|
+
* 台账里的 owner / creator 存的是 uuid,本机身份也取 uuid;但两边来源不同
|
|
5
|
+
* (identity.json 可能是手写的、台账里可能是旧版本写的),大小写不一定一致。
|
|
6
|
+
* 脚本侧 `core/identity.normalize` 就是这么比的,这里必须同口径,
|
|
7
|
+
* 否则脚本说「是你的」,界面说「不是你的」。
|
|
8
|
+
*
|
|
9
|
+
* 空值一律不算相等(`"" === ""` 会让「没 init」被误判成「是我的」)。
|
|
10
|
+
*/
|
|
11
|
+
export declare function sameUuid(a: string | null | undefined, b: string | null | undefined): boolean;
|