dsh-task-ledger 0.0.73 → 0.0.75
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 +128 -128
- package/dist/client.js.map +1 -1
- package/dist/host/board.d.ts +4 -4
- package/dist/host/board.js +6 -6
- package/dist/host/errors.d.ts +3 -3
- package/dist/host/errors.js +13 -7
- package/dist/host/repositories/cache.d.ts +1 -1
- package/dist/host/repositories/cache.js +4 -2
- package/dist/host/repositories/identity.repository.d.ts +1 -1
- package/dist/host/repositories/identity.repository.js +14 -7
- package/dist/host/repositories/ledger.repository.d.ts +1 -1
- package/dist/host/repositories/ledger.repository.js +34 -23
- package/dist/host/repositories/records.d.ts +1 -1
- package/dist/host/repositories/records.js +3 -3
- package/dist/host/repositories/runner.d.ts +5 -4
- package/dist/host/repositories/runner.js +41 -19
- package/dist/host/routes.d.ts +4 -4
- package/dist/host/routes.js +145 -96
- package/dist/host/services/dates.js +4 -4
- package/dist/host/services/history.service.d.ts +5 -2
- package/dist/host/services/history.service.js +24 -13
- package/dist/host/services/home.service.d.ts +8 -2
- package/dist/host/services/home.service.js +29 -14
- package/dist/host/services/ledger-write.service.d.ts +8 -3
- package/dist/host/services/ledger-write.service.js +14 -9
- package/dist/host/services/session.service.d.ts +6 -6
- package/dist/host/services/session.service.js +47 -37
- package/dist/host/services/trend.service.d.ts +9 -2
- package/dist/host/services/trend.service.js +42 -21
- package/dist/host/skill/registration.d.ts +1 -1
- package/dist/host/skill/registration.js +16 -11
- package/dist/host/workspace.d.ts +1 -1
- package/dist/host/workspace.js +6 -2
- package/dist/index.js +12 -12
- package/package.json +57 -58
- package/skill/README.md +29 -245
- package/skill/SKILL.md +173 -88
- package/skill/package.json +22 -22
- package/skill/references/log-note.md +3 -1
- package/skill/references/state-model.md +29 -27
- package/skill/references/troubleshooting.md +98 -25
- package/skill/scripts/ledgerkit/__init__.py +0 -0
- package/skill/scripts/ledgerkit/actions/__init__.py +0 -0
- package/skill/scripts/ledgerkit/actions/atom.py +154 -0
- package/skill/scripts/ledgerkit/actions/bootstrap.py +27 -0
- package/skill/scripts/ledgerkit/actions/claim.py +82 -0
- package/skill/scripts/ledgerkit/actions/close_task.py +180 -0
- package/skill/scripts/ledgerkit/actions/guard.py +140 -0
- package/skill/scripts/ledgerkit/actions/handoff.py +74 -0
- package/skill/scripts/ledgerkit/actions/list_tasks.py +53 -0
- package/skill/scripts/ledgerkit/actions/log_progress.py +60 -0
- package/skill/scripts/ledgerkit/actions/new_task.py +120 -0
- package/skill/scripts/ledgerkit/actions/query.py +33 -0
- package/skill/scripts/ledgerkit/actions/query_history.py +173 -0
- package/skill/scripts/ledgerkit/actions/registry.py +82 -0
- package/skill/scripts/ledgerkit/actions/release.py +51 -0
- package/skill/scripts/ledgerkit/actions/result.py +88 -0
- package/skill/scripts/ledgerkit/actions/set_task.py +43 -0
- package/skill/scripts/ledgerkit/actions/show_task.py +27 -0
- package/skill/scripts/ledgerkit/actions/validate.py +224 -0
- package/skill/scripts/ledgerkit/core/__init__.py +0 -0
- package/skill/scripts/ledgerkit/core/history_md.py +350 -0
- package/skill/scripts/ledgerkit/core/identity.py +43 -0
- package/skill/scripts/ledgerkit/core/ids.py +58 -0
- package/skill/scripts/ledgerkit/core/labels.py +79 -0
- package/skill/scripts/ledgerkit/core/paths.py +93 -0
- package/skill/scripts/ledgerkit/core/time.py +102 -0
- package/skill/scripts/ledgerkit/core/worklog_md.py +232 -0
- package/skill/scripts/ledgerkit/env.py +93 -0
- package/skill/scripts/ledgerkit/errors.py +15 -0
- package/skill/scripts/ledgerkit/notices.py +28 -0
- package/skill/scripts/ledgerkit/shell/__init__.py +0 -0
- package/skill/scripts/ledgerkit/shell/cli.py +259 -0
- package/skill/scripts/ledgerkit/shell/gitops.py +655 -0
- package/skill/scripts/ledgerkit/shell/merge.py +266 -0
- package/skill/scripts/ledgerkit/shell/presync.py +23 -0
- package/skill/scripts/ledgerkit/shell/render.py +165 -0
- package/skill/scripts/ledgerkit/shell/runner.py +225 -0
- package/skill/scripts/ledgerkit/store/__init__.py +0 -0
- package/skill/scripts/ledgerkit/store/atomic.py +130 -0
- package/skill/scripts/ledgerkit/store/dirty.py +108 -0
- package/skill/scripts/ledgerkit/store/history_store.py +189 -0
- package/skill/scripts/ledgerkit/store/json_store.py +166 -0
- package/skill/scripts/ledgerkit/store/locks.py +64 -0
- package/skill/scripts/ledgerkit/store/owners_index.py +135 -0
- package/skill/scripts/ledgerkit/store/task_store.py +31 -0
- package/skill/scripts/ledgerkit/store/worklog_store.py +168 -0
- package/skill/scripts/progress.py +8 -3021
- package/skill/references/taxonomy.md +0 -26
- package/skill/references/worklog.md +0 -39
package/dist/host/routes.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { resolveWorkspaces } from
|
|
1
|
+
import { getFilterOptions, getHomeAggregate, queryClosed } from "./board.js";
|
|
2
|
+
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";
|
|
8
|
+
import { resolveWorkspaces } from "./workspace.js";
|
|
9
9
|
/** API 前缀(唯一真源就在这一处)。 */
|
|
10
|
-
const API_PREFIX =
|
|
10
|
+
const API_PREFIX = "/api/task-ledger";
|
|
11
11
|
function sendJson(res, status, body) {
|
|
12
|
-
res.writeHead(status, {
|
|
12
|
+
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
13
13
|
res.end(JSON.stringify(body));
|
|
14
14
|
}
|
|
15
15
|
/** URL query → LedgerFilter(只认白名单键;limit/months 非正数忽略)。 */
|
|
@@ -17,7 +17,7 @@ function filterFromQuery(url) {
|
|
|
17
17
|
const filter = {};
|
|
18
18
|
const str = (key) => {
|
|
19
19
|
const value = url.searchParams.get(key);
|
|
20
|
-
return value !== null && value !==
|
|
20
|
+
return value !== null && value !== "" ? value : null;
|
|
21
21
|
};
|
|
22
22
|
const int = (key) => {
|
|
23
23
|
const raw = str(key);
|
|
@@ -26,28 +26,28 @@ function filterFromQuery(url) {
|
|
|
26
26
|
const value = Number.parseInt(raw, 10);
|
|
27
27
|
return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
28
28
|
};
|
|
29
|
-
const status = str(
|
|
29
|
+
const status = str("status");
|
|
30
30
|
if (status !== null)
|
|
31
31
|
filter.status = status;
|
|
32
|
-
const owner = str(
|
|
32
|
+
const owner = str("owner");
|
|
33
33
|
if (owner !== null)
|
|
34
34
|
filter.owner = owner;
|
|
35
|
-
const category = str(
|
|
35
|
+
const category = str("category");
|
|
36
36
|
if (category !== null)
|
|
37
37
|
filter.category = category;
|
|
38
|
-
const tag = str(
|
|
38
|
+
const tag = str("tag");
|
|
39
39
|
if (tag !== null)
|
|
40
40
|
filter.tag = tag;
|
|
41
|
-
const since = str(
|
|
41
|
+
const since = str("since");
|
|
42
42
|
if (since !== null)
|
|
43
43
|
filter.since = since;
|
|
44
|
-
const until = str(
|
|
44
|
+
const until = str("until");
|
|
45
45
|
if (until !== null)
|
|
46
46
|
filter.until = until;
|
|
47
|
-
const limit = int(
|
|
47
|
+
const limit = int("limit");
|
|
48
48
|
if (limit !== undefined)
|
|
49
49
|
filter.limit = limit;
|
|
50
|
-
const months = int(
|
|
50
|
+
const months = int("months");
|
|
51
51
|
if (months !== undefined)
|
|
52
52
|
filter.months = months;
|
|
53
53
|
return filter;
|
|
@@ -63,96 +63,124 @@ export function registerRoutes(webServer, deps) {
|
|
|
63
63
|
*/
|
|
64
64
|
const registerRead = (path, handler) => {
|
|
65
65
|
webServer.register({
|
|
66
|
-
kind:
|
|
66
|
+
kind: "exact",
|
|
67
67
|
path: `${API_PREFIX}${path}`,
|
|
68
68
|
handler: async (req, res) => {
|
|
69
|
-
if (new URL(req.url ??
|
|
69
|
+
if (new URL(req.url ?? "/", "http://localhost").searchParams.get("fresh") === "1")
|
|
70
70
|
invalidate();
|
|
71
71
|
await handler(req, res);
|
|
72
72
|
},
|
|
73
73
|
});
|
|
74
74
|
};
|
|
75
|
-
registerRead(
|
|
75
|
+
registerRead("/workspaces", (_req, res) => {
|
|
76
|
+
sendJson(res, 200, { workspaces: workspacesOf(deps) });
|
|
77
|
+
});
|
|
76
78
|
// 首页一次拿全:在办快照 + 近 8 周趋势 + 四个自然周期计数(host 每个工作区只读一次)。
|
|
77
|
-
registerRead(
|
|
79
|
+
registerRead("/board", async (_req, res) => {
|
|
78
80
|
try {
|
|
79
81
|
const home = await getHomeAggregate(workspacesOf(deps));
|
|
80
|
-
sendJson(res, 200, {
|
|
82
|
+
sendJson(res, 200, {
|
|
83
|
+
...home.snapshot,
|
|
84
|
+
closed: home.closed,
|
|
85
|
+
trend: home.trend,
|
|
86
|
+
degraded: home.degraded,
|
|
87
|
+
});
|
|
81
88
|
}
|
|
82
89
|
catch (error) {
|
|
83
|
-
sendJson(res, 500, {
|
|
90
|
+
sendJson(res, 500, {
|
|
91
|
+
error: error instanceof Error ? error.message : String(error),
|
|
92
|
+
});
|
|
84
93
|
}
|
|
85
94
|
});
|
|
86
95
|
// ── 拆端点(2026-10-18 步骤 4a:纯加法,旧 /board 全量原样保留)──
|
|
87
96
|
// 快路径:首屏。只读 progress.json + git 身份,**不碰 history** ⇒ 耗时与历史总量解耦
|
|
88
97
|
// (20 万条量级实测 527ms vs 合并端点 4.4s)。
|
|
89
|
-
registerRead(
|
|
98
|
+
registerRead("/board/snapshot", async (_req, res) => {
|
|
90
99
|
try {
|
|
91
100
|
const result = await getSnapshot(workspacesOf(deps));
|
|
92
101
|
sendJson(res, 200, { ...result.snapshot, degraded: result.degraded });
|
|
93
102
|
}
|
|
94
103
|
catch (error) {
|
|
95
|
-
sendJson(res, 500, {
|
|
104
|
+
sendJson(res, 500, {
|
|
105
|
+
error: error instanceof Error ? error.message : String(error),
|
|
106
|
+
});
|
|
96
107
|
}
|
|
97
108
|
});
|
|
98
109
|
// 慢路径:趋势(近 8 周 + 四个自然周期)。读 history ⇒ 异步/按需拉,首屏不等它。
|
|
99
|
-
registerRead(
|
|
110
|
+
registerRead("/board/trend", async (_req, res) => {
|
|
100
111
|
try {
|
|
101
112
|
const result = await getTrend(workspacesOf(deps));
|
|
102
|
-
sendJson(res, 200, {
|
|
113
|
+
sendJson(res, 200, {
|
|
114
|
+
closed: result.closed,
|
|
115
|
+
trend: result.trend,
|
|
116
|
+
degraded: result.degraded,
|
|
117
|
+
});
|
|
103
118
|
}
|
|
104
119
|
catch (error) {
|
|
105
|
-
sendJson(res, 500, {
|
|
120
|
+
sendJson(res, 500, {
|
|
121
|
+
error: error instanceof Error ? error.message : String(error),
|
|
122
|
+
});
|
|
106
123
|
}
|
|
107
124
|
});
|
|
108
|
-
registerRead(
|
|
125
|
+
registerRead("/tasks", async (req, res) => {
|
|
109
126
|
try {
|
|
110
|
-
const filter = filterFromQuery(new URL(req.url ??
|
|
127
|
+
const filter = filterFromQuery(new URL(req.url ?? "/", "http://localhost"));
|
|
111
128
|
const perWorkspace = await Promise.all(workspacesOf(deps).map((workspace) => readTasks(workspace, filter)));
|
|
112
129
|
sendJson(res, 200, { tasks: perWorkspace.flat() });
|
|
113
130
|
}
|
|
114
131
|
catch (error) {
|
|
115
|
-
sendJson(res, 500, {
|
|
132
|
+
sendJson(res, 500, {
|
|
133
|
+
error: error instanceof Error ? error.message : String(error),
|
|
134
|
+
});
|
|
116
135
|
}
|
|
117
136
|
});
|
|
118
137
|
// 任务详情 + 过程文档正文(query 传 ws/id,不做路径段解析——任务号含中文和连字符)。
|
|
119
|
-
registerRead(
|
|
138
|
+
registerRead("/task", async (req, res) => {
|
|
120
139
|
try {
|
|
121
|
-
const url = new URL(req.url ??
|
|
122
|
-
const workspace = workspacesOf(deps).find((item) => item.id === url.searchParams.get(
|
|
123
|
-
const taskId = url.searchParams.get(
|
|
140
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
141
|
+
const workspace = workspacesOf(deps).find((item) => item.id === url.searchParams.get("ws"));
|
|
142
|
+
const taskId = url.searchParams.get("id");
|
|
124
143
|
if (workspace === undefined || taskId === null) {
|
|
125
|
-
sendJson(res, 400, { error:
|
|
144
|
+
sendJson(res, 400, { error: "缺少 ws / id 参数" });
|
|
126
145
|
return;
|
|
127
146
|
}
|
|
128
147
|
const task = await getTask(workspace, taskId);
|
|
129
148
|
if (task === null) {
|
|
130
|
-
sendJson(res, 404, { error:
|
|
149
|
+
sendJson(res, 404, { error: "任务不存在" });
|
|
131
150
|
return;
|
|
132
151
|
}
|
|
133
152
|
sendJson(res, 200, { task, worklog: await readWorklog(workspace, task) });
|
|
134
153
|
}
|
|
135
154
|
catch (error) {
|
|
136
|
-
sendJson(res, 500, {
|
|
155
|
+
sendJson(res, 500, {
|
|
156
|
+
error: error instanceof Error ? error.message : String(error),
|
|
157
|
+
});
|
|
137
158
|
}
|
|
138
159
|
});
|
|
139
|
-
registerRead(
|
|
160
|
+
registerRead("/history", async (req, res) => {
|
|
140
161
|
try {
|
|
141
|
-
const filter = filterFromQuery(new URL(req.url ??
|
|
162
|
+
const filter = filterFromQuery(new URL(req.url ?? "/", "http://localhost"));
|
|
142
163
|
const result = await queryClosed(workspacesOf(deps), filter);
|
|
143
|
-
sendJson(res, 200, {
|
|
164
|
+
sendJson(res, 200, {
|
|
165
|
+
records: result.records,
|
|
166
|
+
degraded: result.degraded,
|
|
167
|
+
});
|
|
144
168
|
}
|
|
145
169
|
catch (error) {
|
|
146
|
-
sendJson(res, 500, {
|
|
170
|
+
sendJson(res, 500, {
|
|
171
|
+
error: error instanceof Error ? error.message : String(error),
|
|
172
|
+
});
|
|
147
173
|
}
|
|
148
174
|
});
|
|
149
|
-
registerRead(
|
|
175
|
+
registerRead("/filters", async (_req, res) => {
|
|
150
176
|
try {
|
|
151
177
|
const result = await getFilterOptions(workspacesOf(deps));
|
|
152
178
|
sendJson(res, 200, { ...result.options, degraded: result.degraded });
|
|
153
179
|
}
|
|
154
180
|
catch (error) {
|
|
155
|
-
sendJson(res, 500, {
|
|
181
|
+
sendJson(res, 500, {
|
|
182
|
+
error: error instanceof Error ? error.message : String(error),
|
|
183
|
+
});
|
|
156
184
|
}
|
|
157
185
|
});
|
|
158
186
|
}
|
|
@@ -166,15 +194,15 @@ export function registerRoutes(webServer, deps) {
|
|
|
166
194
|
export function registerWriteRoutes(webServer, deps) {
|
|
167
195
|
const registerWrite = (path, action) => {
|
|
168
196
|
webServer.register({
|
|
169
|
-
kind:
|
|
197
|
+
kind: "exact",
|
|
170
198
|
path: `${API_PREFIX}${path}`,
|
|
171
199
|
handler: async (req, res) => {
|
|
172
|
-
if (req.method !==
|
|
173
|
-
sendJson(res, 405, { error:
|
|
200
|
+
if (req.method !== "POST") {
|
|
201
|
+
sendJson(res, 405, { error: "只支持 POST" });
|
|
174
202
|
return;
|
|
175
203
|
}
|
|
176
204
|
if (!isTrustedRequest(req)) {
|
|
177
|
-
sendJson(res, 403, { error:
|
|
205
|
+
sendJson(res, 403, { error: "只接受同源请求" });
|
|
178
206
|
return;
|
|
179
207
|
}
|
|
180
208
|
let body;
|
|
@@ -182,21 +210,25 @@ export function registerWriteRoutes(webServer, deps) {
|
|
|
182
210
|
body = await readJsonBody(req);
|
|
183
211
|
}
|
|
184
212
|
catch (error) {
|
|
185
|
-
const tooLarge = error instanceof Error && error.message ===
|
|
186
|
-
sendJson(res, tooLarge ? 413 : 400, {
|
|
213
|
+
const tooLarge = error instanceof Error && error.message === "body-too-large";
|
|
214
|
+
sendJson(res, tooLarge ? 413 : 400, {
|
|
215
|
+
error: tooLarge ? "请求体过大" : "请求体不是合法 JSON",
|
|
216
|
+
});
|
|
187
217
|
return;
|
|
188
218
|
}
|
|
189
|
-
const fields = typeof body ===
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
219
|
+
const fields = typeof body === "object" && body !== null
|
|
220
|
+
? body
|
|
221
|
+
: {};
|
|
222
|
+
const str = (key) => typeof fields[key] === "string" ? fields[key] : "";
|
|
223
|
+
const workspaceId = str("ws");
|
|
224
|
+
const taskId = str("id");
|
|
225
|
+
if (workspaceId === "" || taskId === "") {
|
|
226
|
+
sendJson(res, 400, { error: "缺少 ws / id 参数" });
|
|
195
227
|
return;
|
|
196
228
|
}
|
|
197
229
|
const workspace = workspacesOf(deps).find((item) => item.id === workspaceId);
|
|
198
230
|
if (workspace === undefined) {
|
|
199
|
-
sendJson(res, 404, { error:
|
|
231
|
+
sendJson(res, 404, { error: "工作区不存在或不含台账" });
|
|
200
232
|
return;
|
|
201
233
|
}
|
|
202
234
|
try {
|
|
@@ -206,13 +238,15 @@ export function registerWriteRoutes(webServer, deps) {
|
|
|
206
238
|
sendJson(res, 200, notice ? { ok: true, notice } : { ok: true });
|
|
207
239
|
}
|
|
208
240
|
catch (error) {
|
|
209
|
-
sendJson(res, 500, {
|
|
241
|
+
sendJson(res, 500, {
|
|
242
|
+
error: error instanceof Error ? error.message : String(error),
|
|
243
|
+
});
|
|
210
244
|
}
|
|
211
245
|
},
|
|
212
246
|
});
|
|
213
247
|
};
|
|
214
|
-
registerWrite(
|
|
215
|
-
registerWrite(
|
|
248
|
+
registerWrite("/claim", claimTask);
|
|
249
|
+
registerWrite("/release", releaseTask);
|
|
216
250
|
}
|
|
217
251
|
// ───────────────────────── 会话交接(按钮侧 · 写类端点) ─────────────────────────
|
|
218
252
|
/** 写端点请求体上限:只收一小段用户留言,别让人往这塞文件。 */
|
|
@@ -225,11 +259,11 @@ async function readJsonBody(req) {
|
|
|
225
259
|
const buffer = chunk;
|
|
226
260
|
size += buffer.length;
|
|
227
261
|
if (size > BODY_LIMIT)
|
|
228
|
-
throw new Error(
|
|
262
|
+
throw new Error("body-too-large");
|
|
229
263
|
chunks.push(buffer);
|
|
230
264
|
}
|
|
231
|
-
const text = Buffer.concat(chunks).toString(
|
|
232
|
-
if (text.trim() ===
|
|
265
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
266
|
+
if (text.trim() === "")
|
|
233
267
|
return {};
|
|
234
268
|
return JSON.parse(text);
|
|
235
269
|
}
|
|
@@ -239,11 +273,13 @@ async function readJsonBody(req) {
|
|
|
239
273
|
* 读端点保持现状(只读无副作用);**会起会话的写端点必须过这道**。
|
|
240
274
|
*/
|
|
241
275
|
function isTrustedRequest(req) {
|
|
242
|
-
const origin = typeof req.headers.origin ===
|
|
243
|
-
const host = typeof req.headers.host ===
|
|
276
|
+
const origin = typeof req.headers.origin === "string" ? req.headers.origin : undefined;
|
|
277
|
+
const host = typeof req.headers.host === "string" ? req.headers.host : undefined;
|
|
244
278
|
if (origin === undefined) {
|
|
245
279
|
const address = req.socket.remoteAddress;
|
|
246
|
-
return address ===
|
|
280
|
+
return (address === "127.0.0.1" ||
|
|
281
|
+
address === "::1" ||
|
|
282
|
+
address === "::ffff:127.0.0.1");
|
|
247
283
|
}
|
|
248
284
|
if (host === undefined)
|
|
249
285
|
return false;
|
|
@@ -264,15 +300,15 @@ function isTrustedRequest(req) {
|
|
|
264
300
|
*/
|
|
265
301
|
export function registerSessionRoutes(webServer, deps) {
|
|
266
302
|
webServer.register({
|
|
267
|
-
kind:
|
|
303
|
+
kind: "exact",
|
|
268
304
|
path: `${API_PREFIX}/close`,
|
|
269
305
|
handler: async (req, res) => {
|
|
270
|
-
if (req.method !==
|
|
271
|
-
sendJson(res, 405, { error:
|
|
306
|
+
if (req.method !== "POST") {
|
|
307
|
+
sendJson(res, 405, { error: "「结束任务」只支持 POST" });
|
|
272
308
|
return;
|
|
273
309
|
}
|
|
274
310
|
if (!isTrustedRequest(req)) {
|
|
275
|
-
sendJson(res, 403, { error:
|
|
311
|
+
sendJson(res, 403, { error: "只接受同源请求" });
|
|
276
312
|
return;
|
|
277
313
|
}
|
|
278
314
|
let body;
|
|
@@ -280,52 +316,65 @@ export function registerSessionRoutes(webServer, deps) {
|
|
|
280
316
|
body = await readJsonBody(req);
|
|
281
317
|
}
|
|
282
318
|
catch (error) {
|
|
283
|
-
const tooLarge = error instanceof Error && error.message ===
|
|
284
|
-
sendJson(res, tooLarge ? 413 : 400, {
|
|
319
|
+
const tooLarge = error instanceof Error && error.message === "body-too-large";
|
|
320
|
+
sendJson(res, tooLarge ? 413 : 400, {
|
|
321
|
+
error: tooLarge ? "请求体过大" : "请求体不是合法 JSON",
|
|
322
|
+
});
|
|
285
323
|
return;
|
|
286
324
|
}
|
|
287
|
-
const fields = typeof body ===
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
325
|
+
const fields = typeof body === "object" && body !== null
|
|
326
|
+
? body
|
|
327
|
+
: {};
|
|
328
|
+
const str = (key) => typeof fields[key] === "string" ? fields[key] : "";
|
|
329
|
+
const workspaceId = str("ws");
|
|
330
|
+
const taskId = str("id");
|
|
331
|
+
if (workspaceId === "" || taskId === "") {
|
|
332
|
+
sendJson(res, 400, { error: "缺少 ws / id 参数" });
|
|
293
333
|
return;
|
|
294
334
|
}
|
|
295
|
-
const provider = str(
|
|
296
|
-
const model = str(
|
|
297
|
-
if ((provider ===
|
|
298
|
-
sendJson(res, 400, { error:
|
|
335
|
+
const provider = str("provider");
|
|
336
|
+
const model = str("model");
|
|
337
|
+
if ((provider === "") !== (model === "")) {
|
|
338
|
+
sendJson(res, 400, { error: "provider 与 model 必须成对给出" });
|
|
299
339
|
return;
|
|
300
340
|
}
|
|
301
341
|
const workspace = workspacesOf(deps).find((item) => item.id === workspaceId);
|
|
302
342
|
if (workspace === undefined) {
|
|
303
|
-
sendJson(res, 404, { error:
|
|
343
|
+
sendJson(res, 404, { error: "工作区不存在或不含台账" });
|
|
304
344
|
return;
|
|
305
345
|
}
|
|
306
346
|
try {
|
|
307
347
|
const result = await startCloseSession({
|
|
308
348
|
agents: deps.agents,
|
|
309
|
-
presets: deps.getService(
|
|
310
|
-
defaultModel: deps.getService(
|
|
349
|
+
presets: deps.getService("agentPresets"),
|
|
350
|
+
defaultModel: deps.getService("agentDefaultModel"),
|
|
311
351
|
}, {
|
|
312
352
|
workspace,
|
|
313
353
|
taskId,
|
|
314
|
-
message: str(
|
|
315
|
-
model: provider ===
|
|
354
|
+
message: str("message"),
|
|
355
|
+
model: provider === "" ? undefined : { provider, model },
|
|
316
356
|
});
|
|
317
|
-
const warning = await attachSession(deps, workspaceId, result.sessionId) ??
|
|
318
|
-
|
|
357
|
+
const warning = (await attachSession(deps, workspaceId, result.sessionId)) ??
|
|
358
|
+
result.warning;
|
|
359
|
+
sendJson(res, 200, warning === undefined
|
|
360
|
+
? { sessionId: result.sessionId }
|
|
361
|
+
: { sessionId: result.sessionId, warning });
|
|
319
362
|
}
|
|
320
363
|
catch (error) {
|
|
321
364
|
if (error instanceof HandoffError) {
|
|
322
|
-
const status = error.code ===
|
|
365
|
+
const status = error.code === "task-missing"
|
|
323
366
|
? 404
|
|
324
|
-
: error.code ===
|
|
367
|
+
: error.code === "no-agents" ||
|
|
368
|
+
error.code === "no-preset" ||
|
|
369
|
+
error.code === "no-model"
|
|
370
|
+
? 503
|
|
371
|
+
: 500;
|
|
325
372
|
sendJson(res, status, { error: error.message, code: error.code });
|
|
326
373
|
return;
|
|
327
374
|
}
|
|
328
|
-
sendJson(res, 500, {
|
|
375
|
+
sendJson(res, 500, {
|
|
376
|
+
error: error instanceof Error ? error.message : String(error),
|
|
377
|
+
});
|
|
329
378
|
}
|
|
330
379
|
},
|
|
331
380
|
});
|
|
@@ -336,8 +385,8 @@ export function registerSessionRoutes(webServer, deps) {
|
|
|
336
385
|
*/
|
|
337
386
|
async function attachSession(deps, workspaceId, sessionId) {
|
|
338
387
|
const entity = deps.registry.list().find((item) => item.id === workspaceId);
|
|
339
|
-
if (entity === undefined || typeof entity.attachSession !==
|
|
340
|
-
return
|
|
388
|
+
if (entity === undefined || typeof entity.attachSession !== "function") {
|
|
389
|
+
return "会话已建,但宿主该工作区没有 attachSession 面:会话不会出现在工作区列表里";
|
|
341
390
|
}
|
|
342
391
|
try {
|
|
343
392
|
await entity.attachSession(sessionId);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* 所有函数显式收 `now`——不自己读时钟,便于单测与跨时区复现。
|
|
4
4
|
*/
|
|
5
5
|
function pad(value) {
|
|
6
|
-
return String(value).padStart(2,
|
|
6
|
+
return String(value).padStart(2, "0");
|
|
7
7
|
}
|
|
8
8
|
/** Date → `YYYY-MM-DD`。 */
|
|
9
9
|
export function ymd(date) {
|
|
@@ -11,7 +11,7 @@ export function ymd(date) {
|
|
|
11
11
|
}
|
|
12
12
|
/** 时刻串(`YYYY-MM-DD HH:mm:ss` / `YYYY-MM-DD`)→ 日期部分;解析不了回 null。 */
|
|
13
13
|
export function dayOf(stamp) {
|
|
14
|
-
if (stamp === null || stamp === undefined || stamp ===
|
|
14
|
+
if (stamp === null || stamp === undefined || stamp === "")
|
|
15
15
|
return null;
|
|
16
16
|
const day = String(stamp).slice(0, 10);
|
|
17
17
|
return /^\d{4}-\d{2}-\d{2}$/.test(day) ? day : null;
|
|
@@ -49,9 +49,9 @@ export function inWindow(day, window) {
|
|
|
49
49
|
}
|
|
50
50
|
/** 「YYYY-MM-DD HH:mm:ss」→ 距今天数(解析不了 / 未来都按 0;不编造)。 */
|
|
51
51
|
export function daysSince(stamp, now) {
|
|
52
|
-
if (stamp === null || stamp === undefined || stamp ===
|
|
52
|
+
if (stamp === null || stamp === undefined || stamp === "")
|
|
53
53
|
return null;
|
|
54
|
-
const parsed = new Date(String(stamp).replace(
|
|
54
|
+
const parsed = new Date(String(stamp).replace(" ", "T"));
|
|
55
55
|
if (Number.isNaN(parsed.getTime()))
|
|
56
56
|
return null;
|
|
57
57
|
const days = Math.floor((now.getTime() - parsed.getTime()) / 86_400_000);
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 已结案列表 / 筛选项候选用例(history tab 与筛选控件用;非首屏,按需调用)。
|
|
3
|
+
*/
|
|
4
|
+
import { type DegradedInfo } from "../errors.js";
|
|
5
|
+
import type { FilterOptions, HistoryRecord, LedgerFilter, LedgerWorkspace, TrendScope } from "../types.js";
|
|
3
6
|
/** 已结案列表默认分页大小(「加载更多」= 调用方加大 limit 重取)。 */
|
|
4
7
|
export declare const DEFAULT_HISTORY_LIMIT = 500;
|
|
5
8
|
/**
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 已结案列表 / 筛选项候选用例(history tab 与筛选控件用;非首屏,按需调用)。
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { RECENT_MONTHS, readTrendScope } from
|
|
4
|
+
import { settleWorkspaces } from "../errors.js";
|
|
5
|
+
import { readHistory } from "../repositories/ledger.repository.js";
|
|
6
|
+
import { RECENT_MONTHS, readTrendScope } from "./trend.service.js";
|
|
7
7
|
/** 已结案列表默认分页大小(「加载更多」= 调用方加大 limit 重取)。 */
|
|
8
8
|
export const DEFAULT_HISTORY_LIMIT = 500;
|
|
9
9
|
/**
|
|
@@ -14,8 +14,9 @@ export const DEFAULT_HISTORY_LIMIT = 500;
|
|
|
14
14
|
*/
|
|
15
15
|
export async function queryClosed(workspaces, filter = {}) {
|
|
16
16
|
const windowed = { months: RECENT_MONTHS, ...filter };
|
|
17
|
-
const { data, degraded } = await settleWorkspaces(workspaces,
|
|
18
|
-
.
|
|
17
|
+
const { data, degraded } = await settleWorkspaces(workspaces, "history", (workspace) => readHistory(workspace, windowed), (rows) => rows
|
|
18
|
+
.flat()
|
|
19
|
+
.sort((a, b) => String(b.created ?? b.time ?? "").localeCompare(String(a.created ?? a.time ?? "")))
|
|
19
20
|
.slice(0, filter.limit ?? DEFAULT_HISTORY_LIMIT));
|
|
20
21
|
return { records: data, degraded };
|
|
21
22
|
}
|
|
@@ -25,19 +26,25 @@ export function collectFilterOptions(scopes) {
|
|
|
25
26
|
const categories = new Set();
|
|
26
27
|
const tags = new Set();
|
|
27
28
|
const addTask = (task) => {
|
|
28
|
-
if (task.owner !== undefined && task.owner !== null && task.owner !==
|
|
29
|
+
if (task.owner !== undefined && task.owner !== null && task.owner !== "")
|
|
29
30
|
owners.add(task.owner);
|
|
30
|
-
if (task.creator !== undefined &&
|
|
31
|
+
if (task.creator !== undefined &&
|
|
32
|
+
task.creator !== null &&
|
|
33
|
+
task.creator !== "")
|
|
31
34
|
owners.add(task.creator);
|
|
32
|
-
if (task.category !== undefined &&
|
|
35
|
+
if (task.category !== undefined &&
|
|
36
|
+
task.category !== null &&
|
|
37
|
+
task.category !== "")
|
|
33
38
|
categories.add(task.category);
|
|
34
39
|
for (const tag of task.tags ?? [])
|
|
35
40
|
tags.add(tag);
|
|
36
41
|
};
|
|
37
42
|
const addRecord = (record) => {
|
|
38
|
-
if (record.owner !== undefined && record.owner !==
|
|
43
|
+
if (record.owner !== undefined && record.owner !== "")
|
|
39
44
|
owners.add(record.owner);
|
|
40
|
-
if (record.category !== undefined &&
|
|
45
|
+
if (record.category !== undefined &&
|
|
46
|
+
record.category !== null &&
|
|
47
|
+
record.category !== "")
|
|
41
48
|
categories.add(record.category);
|
|
42
49
|
for (const tag of record.tags ?? [])
|
|
43
50
|
tags.add(tag);
|
|
@@ -48,14 +55,18 @@ export function collectFilterOptions(scopes) {
|
|
|
48
55
|
for (const record of scope.history)
|
|
49
56
|
addRecord(record);
|
|
50
57
|
}
|
|
51
|
-
const sorted = (values) => [...values].sort((a, b) => a.localeCompare(b,
|
|
52
|
-
return {
|
|
58
|
+
const sorted = (values) => [...values].sort((a, b) => a.localeCompare(b, "zh-Hans-CN"));
|
|
59
|
+
return {
|
|
60
|
+
owners: sorted(owners),
|
|
61
|
+
categories: sorted(categories),
|
|
62
|
+
tags: sorted(tags),
|
|
63
|
+
};
|
|
53
64
|
}
|
|
54
65
|
/**
|
|
55
66
|
* 筛选项候选:在办(全量)+ 近 13 个月历史的去重值(owner / category / tag)。
|
|
56
67
|
* 按需调用(/filters 路由),非首屏——历史侧在极端量级下是百毫秒级。单工作区坏 ⇒ 进 degraded。
|
|
57
68
|
*/
|
|
58
69
|
export async function getFilterOptions(workspaces) {
|
|
59
|
-
const { data, degraded } = await settleWorkspaces(workspaces,
|
|
70
|
+
const { data, degraded } = await settleWorkspaces(workspaces, "history", readTrendScope, collectFilterOptions);
|
|
60
71
|
return { options: data, degraded };
|
|
61
72
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 首页**快路径**用例:在办快照 + 统计卡。
|
|
3
|
+
* 只读 progress.json(list)+ git 身份,**不碰 history**——首屏耗时与历史总量解耦
|
|
4
|
+
* (20 万条量级实测:527ms vs 合并端点 4.4s,见 docs/design/host-data-plane.md §四)。
|
|
5
|
+
* 慢路径(趋势)在 ./trend.service.ts;组合入口(/board 兼容全量)在 ../board.ts。
|
|
6
|
+
*/
|
|
7
|
+
import { type DegradedInfo } from "../errors.js";
|
|
8
|
+
import type { BoardSnapshot, LedgerWorkspace, TaskScope } from "../types.js";
|
|
3
9
|
/** 停滞阈值:进行中且 updated 至今满这么多天没更新(2026-10-19 拍板,显示级)。 */
|
|
4
10
|
export declare const STALE_DAYS = 3;
|
|
5
11
|
/** 逾期阈值:进行中且 started 至今满这么多天还没做完——中途更新不影响(2026-10-19 拍板 7 → 14 天)。 */
|