dsh-task-ledger 0.0.7 → 0.0.8
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 +731 -471
- package/dist/client.js.map +1 -1
- package/dist/host/board.d.ts +46 -16
- package/dist/host/board.js +76 -26
- package/dist/host/routes.js +3 -3
- package/package.json +1 -1
package/dist/host/board.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { readHistory, readTasks } from './ledger-read.js';
|
|
3
|
-
import { runLedgerScript } from './script.js';
|
|
4
3
|
/** 历史窗口:只读最近 13 个月的文件(2026-10-17 拍板;脚本默认读全部,窗口由插件显式传)。 */
|
|
5
4
|
const RECENT_MONTHS = 13;
|
|
6
5
|
/** 已结案列表默认分页大小(「加载更多」= 调用方加大 limit 重取)。 */
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
const
|
|
6
|
+
const DEFAULT_HISTORY_LIMIT = 500;
|
|
7
|
+
/** 停滞判定阈值:doing / blocked 且 created 至今超过这么多天(2026-10-17 拍板,显示级)。 */
|
|
8
|
+
export const STALE_DAYS = 14;
|
|
9
|
+
/** 趋势图桶数:近 8 周(2026-10-17 定稿,与 dsh-context「近 7 天」同思路,按周更耐看)。 */
|
|
10
|
+
export const WEEKLY_WEEKS = 8;
|
|
10
11
|
/** 周一所在日期(YYYY-MM-DD)。skill 的 --since 只给日期按当天 00:00 解析。 */
|
|
11
12
|
function mondayDateString(now) {
|
|
12
13
|
const date = new Date(now);
|
|
@@ -15,6 +16,16 @@ function mondayDateString(now) {
|
|
|
15
16
|
const pad = (value) => String(value).padStart(2, '0');
|
|
16
17
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
17
18
|
}
|
|
19
|
+
/** 「YYYY-MM-DD HH:mm:ss」→ 距今天数(解析不了 / 未来都按 0;不编造)。 */
|
|
20
|
+
function daysSince(stamp, now) {
|
|
21
|
+
if (stamp === null || stamp === undefined || stamp === '')
|
|
22
|
+
return null;
|
|
23
|
+
const parsed = new Date(stamp.replace(' ', 'T'));
|
|
24
|
+
if (Number.isNaN(parsed.getTime()))
|
|
25
|
+
return null;
|
|
26
|
+
const days = Math.floor((now.getTime() - parsed.getTime()) / 86_400_000);
|
|
27
|
+
return days > 0 ? days : 0;
|
|
28
|
+
}
|
|
18
29
|
/**
|
|
19
30
|
* 当前 git 身份(cwd = 该工作区)——「我的待办」判定源。
|
|
20
31
|
* 与 agent 在会话里跑 skill 时的取值环境一致;取不到回 null(非 git 目录 / 没配身份),
|
|
@@ -37,41 +48,80 @@ export function getCurrentIdentity(workspace) {
|
|
|
37
48
|
* 与历史量彻底无关——progress.json 只装没结案的任务(close 会移走),永远有界。
|
|
38
49
|
*/
|
|
39
50
|
export async function getBoardSnapshot(workspaces) {
|
|
51
|
+
const now = new Date();
|
|
40
52
|
const perWorkspace = await Promise.all(workspaces.map(async (workspace) => ({
|
|
41
53
|
workspace,
|
|
42
54
|
tasks: await readTasks(workspace),
|
|
43
55
|
identity: await getCurrentIdentity(workspace),
|
|
44
56
|
})));
|
|
45
|
-
const
|
|
57
|
+
const withMeta = perWorkspace.flatMap((entry) => entry.tasks.map((task) => {
|
|
58
|
+
const working = task.status === 'doing' || task.status === 'blocked';
|
|
59
|
+
const ageDays = working ? daysSince(task.started ?? task.created, now) : null;
|
|
60
|
+
return { ...task, ageDays, stale: ageDays !== null && ageDays > STALE_DAYS };
|
|
61
|
+
}));
|
|
46
62
|
const myActive = perWorkspace.reduce((sum, entry) => sum + entry.tasks.filter((task) => task.owner === entry.identity && (task.status === 'doing' || task.status === 'blocked')).length, 0);
|
|
47
|
-
const
|
|
63
|
+
const myBlocked = perWorkspace.reduce((sum, entry) => sum + entry.tasks.filter((task) => task.owner === entry.identity && task.status === 'blocked').length, 0);
|
|
64
|
+
const openTasks = withMeta.filter((task) => task.status === 'open');
|
|
65
|
+
const blockedTasks = withMeta.filter((task) => task.status === 'blocked');
|
|
66
|
+
const oldestOpenDays = openTasks.length === 0
|
|
67
|
+
? null
|
|
68
|
+
: openTasks.reduce((max, task) => Math.max(max, daysSince(task.created, now) ?? 0), 0);
|
|
69
|
+
const longestBlockedDays = blockedTasks.length === 0
|
|
70
|
+
? null
|
|
71
|
+
: blockedTasks.reduce((max, task) => Math.max(max, task.ageDays ?? 0), 0);
|
|
48
72
|
const identities = {};
|
|
49
73
|
for (const entry of perWorkspace)
|
|
50
74
|
identities[entry.workspace.id] = entry.identity;
|
|
51
|
-
return {
|
|
75
|
+
return {
|
|
76
|
+
at: now.toISOString(),
|
|
77
|
+
workspaces,
|
|
78
|
+
tasks: withMeta,
|
|
79
|
+
stats: {
|
|
80
|
+
myActive,
|
|
81
|
+
myBlocked,
|
|
82
|
+
open: openTasks.length,
|
|
83
|
+
doing: withMeta.filter((task) => task.status === 'doing').length,
|
|
84
|
+
blocked: blockedTasks.length,
|
|
85
|
+
stale: withMeta.filter((task) => task.stale).length,
|
|
86
|
+
staleDays: STALE_DAYS,
|
|
87
|
+
oldestOpenDays,
|
|
88
|
+
longestBlockedDays,
|
|
89
|
+
},
|
|
90
|
+
identities,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** 取一条记录的结案日期部分(history 首列 `time`;旧行 / 缺列返回 null)。 */
|
|
94
|
+
function closedDateOf(record) {
|
|
95
|
+
const value = record.time;
|
|
96
|
+
if (value === undefined || value === null || value === '')
|
|
97
|
+
return null;
|
|
98
|
+
return String(value).slice(0, 10);
|
|
52
99
|
}
|
|
53
100
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
101
|
+
* 完成类统计 + 近 8 周趋势:读一次 13 个月窗口的 history,按**结案时间**落周桶。
|
|
102
|
+
* 任一工作区失败即抛(聚合数部分失败 = 错数,不兜)。
|
|
56
103
|
*/
|
|
57
|
-
export async function
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
104
|
+
export async function getClosedSummary(workspaces) {
|
|
105
|
+
const now = new Date();
|
|
106
|
+
const thisMonday = mondayDateString(now);
|
|
107
|
+
const weeks = [];
|
|
108
|
+
for (let offset = WEEKLY_WEEKS - 1; offset >= 0; offset -= 1) {
|
|
109
|
+
weeks.push(mondayDateString(new Date(now.getFullYear(), now.getMonth(), now.getDate() - offset * 7)));
|
|
110
|
+
}
|
|
111
|
+
const perWorkspace = await Promise.all(workspaces.map((workspace) => readHistory(workspace, { months: RECENT_MONTHS })));
|
|
112
|
+
const counts = new Map(weeks.map((week) => [week, 0]));
|
|
113
|
+
for (const record of perWorkspace.flat()) {
|
|
114
|
+
const date = closedDateOf(record);
|
|
115
|
+
if (date === null)
|
|
116
|
+
continue;
|
|
117
|
+
const monday = mondayDateString(new Date(`${date}T00:00:00`));
|
|
118
|
+
const bucket = counts.get(monday);
|
|
119
|
+
if (bucket !== undefined)
|
|
120
|
+
counts.set(monday, bucket + 1);
|
|
121
|
+
}
|
|
71
122
|
return {
|
|
72
|
-
weekDone:
|
|
73
|
-
|
|
74
|
-
yearCapped,
|
|
123
|
+
weekDone: counts.get(thisMonday) ?? 0,
|
|
124
|
+
weekly: weeks.map((week) => ({ week, count: counts.get(week) ?? 0 })),
|
|
75
125
|
};
|
|
76
126
|
}
|
|
77
127
|
/**
|
package/dist/host/routes.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getBoardSnapshot,
|
|
1
|
+
import { getBoardSnapshot, getClosedSummary, getFilterOptions, queryClosed } from './board.js';
|
|
2
2
|
import { getTask, readTasks, readWorklog } from './ledger-read.js';
|
|
3
3
|
import { resolveWorkspaces } from './workspace.js';
|
|
4
4
|
/** API 前缀(唯一真源就在这一处)。 */
|
|
@@ -57,14 +57,14 @@ export function registerRoutes(webServer, deps) {
|
|
|
57
57
|
path: `${API_PREFIX}/workspaces`,
|
|
58
58
|
handler: (_req, res) => { sendJson(res, 200, { workspaces: workspacesOf(deps) }); },
|
|
59
59
|
});
|
|
60
|
-
//
|
|
60
|
+
// 首页一次拿全:在办快照(含停滞 / 天数 / 身份)+ 完成类统计与近 8 周趋势。
|
|
61
61
|
webServer.register({
|
|
62
62
|
kind: 'exact',
|
|
63
63
|
path: `${API_PREFIX}/board`,
|
|
64
64
|
handler: async (_req, res) => {
|
|
65
65
|
try {
|
|
66
66
|
const workspaces = workspacesOf(deps);
|
|
67
|
-
const [snapshot, closed] = await Promise.all([getBoardSnapshot(workspaces),
|
|
67
|
+
const [snapshot, closed] = await Promise.all([getBoardSnapshot(workspaces), getClosedSummary(workspaces)]);
|
|
68
68
|
sendJson(res, 200, { ...snapshot, closed });
|
|
69
69
|
}
|
|
70
70
|
catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-task-ledger",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "Task ledger board for dsh (DeepSeek Harness): read-only board over task-ledger skill workspaces. The task-ledger skill is bundled — install the plugin, get the skill globally.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek",
|