@mingmaple/dsh-handover 0.9.22 → 1.0.0
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/README.md +117 -113
- package/docs/migrate.md +52 -41
- package/docs/releases/0.9.12.md +2 -3
- package/docs/releases/0.9.14.md +1 -5
- package/docs/releases/0.9.22.md +1 -1
- package/docs/releases/1.0.0.md +33 -0
- package/docs/troubleshoot.md +221 -61
- package/package.json +5 -2
- package/scripts/behavior.mjs +398 -0
- package/scripts/blank-dsh.mjs +12 -2
- package/scripts/blank-smoke.mjs +95 -44
- package/scripts/check-docs.mjs +7 -0
- package/scripts/portability.mjs +71 -0
- package/scripts/style-gate.mjs +571 -0
- package/src/client.js +1028 -994
- package/src/index.mjs +273 -109
- package/src/lib/absrefs.mjs +1 -1
- package/src/lib/api.mjs +9 -3
- package/src/lib/archive.mjs +1 -1
- package/src/lib/brief.mjs +8 -6
- package/src/lib/config.mjs +21 -6
- package/src/lib/docmap.mjs +5 -8
- package/src/lib/drift.mjs +1 -1
- package/src/lib/events.mjs +1 -1
- package/src/lib/facts.mjs +19 -12
- package/src/lib/gates.mjs +12 -12
- package/src/lib/global-task.mjs +69 -0
- package/src/lib/guide.mjs +80 -75
- package/src/lib/handover-io.mjs +69 -6
- package/src/lib/identity.mjs +330 -0
- package/src/lib/legacy.mjs +89 -0
- package/src/lib/lint.mjs +14 -11
- package/src/lib/manual.mjs +117 -80
- package/src/lib/mover.mjs +18 -14
- package/src/lib/office.mjs +42 -2
- package/src/lib/pack-ops.mjs +10 -10
- package/src/lib/pathkey.mjs +24 -0
- package/src/lib/phase.mjs +8 -13
- package/src/lib/profile.mjs +1 -1
- package/src/lib/reminder.mjs +110 -0
- package/src/lib/role.mjs +1 -1
- package/src/lib/scaffold.mjs +30 -10
- package/src/lib/schedule.mjs +5 -5
- package/src/lib/session-create.mjs +2 -2
- package/src/lib/session-ops.mjs +76 -47
- package/src/lib/sessions.mjs +40 -2
- package/src/lib/sevenzip.mjs +10 -1
- package/src/lib/status.mjs +337 -336
- package/src/lib/succession.mjs +14 -14
- package/src/lib/system.mjs +364 -0
- package/src/templates/Changelog.skeleton.md +3 -3
- package/src/templates/Charter.skeleton.md +13 -9
- package/src/templates/Handover.global.skeleton.md +20 -13
- package/src/templates/Handover.template.md +89 -0
- package/src/templates/Plan.skeleton.md +34 -0
- package/src/templates/Prompt_Templates.md +91 -10
- package/src/templates/Runtime_Manual.md +254 -139
- package/src/templates/System.skeleton.md +31 -0
- package/src/templates/Systems.global.skeleton.md +13 -0
- package/src/templates/rules/Delegation_Rules.md +45 -0
- package/src/templates/rules/Handover_Rules.md +46 -24
- package/src/templates/rules/Migration_Rules.md +7 -7
- package/src/templates/rules/Misc_Task_Standard.md +17 -10
- package/src/templates/rules/Writing_Rules.md +33 -16
- package/src/templates/standards/README.md +19 -7
- package/src/templates/standards//345/206/231/344/275/234/350/247/204/350/214/203/README.md +120 -0
- package/src/templates/standards//345/206/231/344/275/234/350/247/204/350/214/203//344/276/235/346/215/256_/350/256/272/346/226/207.md +389 -0
- package/src/tools/ho_backup.mjs +8 -8
- package/src/tools/ho_changelog.mjs +4 -4
- package/src/tools/ho_charter.mjs +7 -6
- package/src/tools/ho_checkpoint.mjs +34 -31
- package/src/tools/ho_docmap.mjs +135 -49
- package/src/tools/ho_gate.mjs +9 -4
- package/src/tools/ho_handover.mjs +61 -35
- package/src/tools/ho_office.mjs +45 -4
- package/src/tools/ho_profile.mjs +3 -3
- package/src/tools/ho_sessions.mjs +43 -22
- package/src/tools/ho_status.mjs +14 -9
- package/src/tools/ho_succeed.mjs +3 -3
- package/src/tools/ho_workspace.mjs +83 -51
- package/src/tools/peer.mjs +219 -8
package/src/lib/status.mjs
CHANGED
|
@@ -1,337 +1,338 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 设置页用的状态快照(只读):插件自身状态 + 闸门 + 交接体检 + 会话水位 + 最近一次守卫判定。
|
|
3
|
-
* 全部读本地文件,不唤醒任何会话、不花任何会话的 token。
|
|
4
|
-
* 任何一段出错都只让该段带 error 字段,不让整页 500。
|
|
5
|
-
*/
|
|
6
|
-
import fs from 'node:fs';
|
|
7
|
-
import os from 'node:os';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
out.
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const mapRow = (r) => {
|
|
269
|
-
const meta = sessionMeta(r.sid);
|
|
270
|
-
const info = roleOf(r.sid, meta.cwd, meta.firstPrompt, r.project);
|
|
271
|
-
const d = dutyOf(r.sid);
|
|
272
|
-
const w = wsOf(r.sid
|
|
273
|
-
// 目录已不存在的老会话(旧树里删掉的项目库等)不再罗列。
|
|
274
|
-
const cwdOk = !meta.cwd || !!realOf(meta.cwd);
|
|
275
|
-
return {
|
|
276
|
-
sid: r.sid,
|
|
277
|
-
title: r.project === '-' ? '' : r.project,
|
|
278
|
-
project: r.project,
|
|
279
|
-
role: info.role,
|
|
280
|
-
roleLabel: ROLE_LABEL[info.role] || info.role,
|
|
281
|
-
source: info.source,
|
|
282
|
-
projectName: info.project,
|
|
283
|
-
task: info.task,
|
|
284
|
-
cwdOk,
|
|
285
|
-
//
|
|
286
|
-
wsKey: w ? w.key : '',
|
|
287
|
-
wsTitle: w ? w.title : '',
|
|
288
|
-
wsPath: w ? w.path : String(meta.cwd || ''),
|
|
289
|
-
// 类别与名称以在任登记为准:登记过的会话才能说清自己是全局、项目还是杂项。
|
|
290
|
-
dutyKind: d ? d.kindLabel : '',
|
|
291
|
-
dutyKey: d ? d.key : '',
|
|
292
|
-
dutyName: d ? d.name : '',
|
|
293
|
-
pct: r.pct,
|
|
294
|
-
state: r.state,
|
|
295
|
-
active: r.mtime >= wide.cutoffMs,
|
|
296
|
-
onduty: !!d,
|
|
297
|
-
mtime: r.mtime,
|
|
298
|
-
seen: stampTz(r.mtime),
|
|
299
|
-
};
|
|
300
|
-
};
|
|
301
|
-
const all = wide.rows
|
|
302
|
-
.filter((r) => r.kind === '平级' && !archived(r.sid) && stillThere(r.sid))
|
|
303
|
-
.map(mapRow)
|
|
304
|
-
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 设置页用的状态快照(只读):插件自身状态 + 闸门 + 交接体检 + 会话水位 + 最近一次守卫判定。
|
|
3
|
+
* 全部读本地文件,不唤醒任何会话、不花任何会话的 token。
|
|
4
|
+
* 任何一段出错都只让该段带 error 字段,不让整页 500。
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { pathKey } from './pathkey.mjs';
|
|
10
|
+
import { syncCheck } from './gates.mjs';
|
|
11
|
+
import { lintGlobal, lintProject } from './lint.mjs';
|
|
12
|
+
import { scanSessions } from './sessions.mjs';
|
|
13
|
+
import { routeWorkspace, hasCenter } from './workspace.mjs';
|
|
14
|
+
import { stampTz } from './brief.mjs';
|
|
15
|
+
import { buildGuide, SETUP_STEPS } from './guide.mjs';
|
|
16
|
+
import { readRoles, resolveRole, ROLE_LABEL, sessionMeta } from './role.mjs';
|
|
17
|
+
import { listConsent } from './consent.mjs';
|
|
18
|
+
import { listProfiles } from './profile.mjs';
|
|
19
|
+
import { loadOffice, readRegistryProjects, KIND_LABEL } from './office.mjs';
|
|
20
|
+
import { checkRegistries } from './system.mjs';
|
|
21
|
+
import { legacyInfo } from './legacy.mjs';
|
|
22
|
+
|
|
23
|
+
function guard(fn, fallback) {
|
|
24
|
+
try { return fn(); } catch (e) { return { ...fallback, error: String(e && e.message ? e.message : e) }; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* DSH 工作区索引:读 `~/.dsh/storages/workspace.json`(只读)。
|
|
29
|
+
* 返回 { list, bySession, archived }:
|
|
30
|
+
* - list:按登记顺序的工作区(key、title、path);
|
|
31
|
+
* - bySession:sessionId → 工作区;
|
|
32
|
+
* - archived:已归档会话 id,设置页不再罗列。
|
|
33
|
+
* 读不到时全部为空,调用方据此退化为「按会话目录分组、只按归档表过滤」。
|
|
34
|
+
*/
|
|
35
|
+
function loadWorkspaceIndex() {
|
|
36
|
+
const out = { list: [], bySession: new Map(), archived: new Set() };
|
|
37
|
+
let v = null;
|
|
38
|
+
try {
|
|
39
|
+
const file = path.join(os.homedir(), '.dsh', 'storages', 'workspace.json');
|
|
40
|
+
v = JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
|
|
41
|
+
} catch { return out; }
|
|
42
|
+
if (!v || typeof v !== 'object') return out;
|
|
43
|
+
const tables = v.tables && v.tables.workspaces ? v.tables.workspaces : {};
|
|
44
|
+
const order = v.global && Array.isArray(v.global.workspaceIds) && v.global.workspaceIds.length
|
|
45
|
+
? v.global.workspaceIds : Object.keys(tables);
|
|
46
|
+
for (const key of order) {
|
|
47
|
+
const w = tables[key];
|
|
48
|
+
if (!w) continue;
|
|
49
|
+
const item = { key: String(key), title: String(w.title || ''), path: String(w.path || '') };
|
|
50
|
+
out.list.push(item);
|
|
51
|
+
for (const sid of (Array.isArray(w.sessionIds) ? w.sessionIds : [])) out.bySession.set(String(sid), item);
|
|
52
|
+
}
|
|
53
|
+
for (const sid of (v.global && Array.isArray(v.global.archivedSessionIds) ? v.global.archivedSessionIds : [])) {
|
|
54
|
+
out.archived.add(String(sid));
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 仍然存在的会话目录:`~/.dsh/sessions` 下以 session- 开头的目录名(含已归档里恢复不了的会话就自然不在)。 */
|
|
60
|
+
function liveSessionDirs() {
|
|
61
|
+
const set = new Set();
|
|
62
|
+
try {
|
|
63
|
+
const walk = (dir, depth) => {
|
|
64
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
65
|
+
if (!e.isDirectory()) continue;
|
|
66
|
+
if (e.name.indexOf('session-') === 0) set.add(e.name);
|
|
67
|
+
else if (depth < 4) walk(path.join(dir, e.name), depth + 1);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
walk(path.join(os.homedir(), '.dsh', 'sessions'), 0);
|
|
71
|
+
} catch { /* 读不到就返回空集 */ }
|
|
72
|
+
return set;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 在任登记:Office.json 中每条登记(全局、各项目、各杂项任务)的现任。
|
|
76
|
+
* 返回 sessionId → { key, kind, kindLabel, name };读不到登记表时为空表,调用方据此退化为「不过滤」。
|
|
77
|
+
* 普通会话不登记,因此不会被当成在任会话。
|
|
78
|
+
*/
|
|
79
|
+
function loadDuty(ws) {
|
|
80
|
+
const map = new Map();
|
|
81
|
+
let office = null;
|
|
82
|
+
try { office = loadOffice(ws); } catch { office = null; }
|
|
83
|
+
const lineages = (office && office.lineages) || {};
|
|
84
|
+
for (const key of Object.keys(lineages)) {
|
|
85
|
+
const lin = lineages[key] || {};
|
|
86
|
+
const cur = lin.current;
|
|
87
|
+
if (!cur || !cur.sessionId) continue;
|
|
88
|
+
map.set(String(cur.sessionId), {
|
|
89
|
+
key,
|
|
90
|
+
kind: String(lin.kind || ''),
|
|
91
|
+
kindLabel: KIND_LABEL[lin.kind] || '',
|
|
92
|
+
project: String(lin.project || ''),
|
|
93
|
+
task: String(lin.task || ''),
|
|
94
|
+
name: String(cur.name || key),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return { map, office };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 项目交接体检:只查 Registry.md 已登记的项目,未登记的旧目录不在此列。 */
|
|
101
|
+
function lintProjects(ws) {
|
|
102
|
+
const out = [];
|
|
103
|
+
let names = [];
|
|
104
|
+
try { names = readRegistryProjects(ws); } catch { return out; }
|
|
105
|
+
for (const name of names) {
|
|
106
|
+
const r = lintProject(path.join(ws, '_Project_Meta', name, 'Handover.md'));
|
|
107
|
+
out.push({ name, verdict: r.verdict, reasons: Array.isArray(r.reasons) ? r.reasons.slice(0, 5) : [] });
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 杂项目录:规范位置 `_Misc/tasks/<任务>/`,兼容旧布局 `_Misc/<任务>/`;两处都没有就返回空串。 */
|
|
113
|
+
function miscDir(ws, task) {
|
|
114
|
+
const name = String(task || '').trim();
|
|
115
|
+
if (!name) return '';
|
|
116
|
+
const cands = [path.join(ws, '_Misc', 'tasks', name), path.join(ws, '_Misc', name)];
|
|
117
|
+
for (const dir of cands) {
|
|
118
|
+
try { if (fs.statSync(dir).isDirectory()) return dir; } catch { /* 继续看下一个 */ }
|
|
119
|
+
}
|
|
120
|
+
return '';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 杂项交接体检:按 Office.json 里已登记的杂项条目找任务目录。
|
|
125
|
+
* 登记名与目录名不一致(例如登记「示例任务」、目录名不同)时报「未找到」,
|
|
126
|
+
* 不猜近似目录:猜错会把别人的交接算到这条任务头上。
|
|
127
|
+
*/
|
|
128
|
+
function lintMisc(ws, office) {
|
|
129
|
+
const out = [];
|
|
130
|
+
const lineages = (office && office.lineages) || {};
|
|
131
|
+
for (const key of Object.keys(lineages)) {
|
|
132
|
+
const lin = lineages[key] || {};
|
|
133
|
+
if (String(lin.kind || '') !== 'misc') continue;
|
|
134
|
+
const name = String(lin.task || key).trim();
|
|
135
|
+
if (!name) continue;
|
|
136
|
+
const dir = miscDir(ws, name);
|
|
137
|
+
const r = dir
|
|
138
|
+
? lintProject(path.join(dir, 'Handover.md'))
|
|
139
|
+
: { verdict: '未找到', reasons: ['按登记名找不到任务目录:_Misc/tasks/' + name + ' 或 _Misc/' + name] };
|
|
140
|
+
out.push({ name, verdict: r.verdict, reasons: Array.isArray(r.reasons) ? r.reasons.slice(0, 5) : [] });
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function buildStatus(env, extra) {
|
|
146
|
+
const info = extra || {};
|
|
147
|
+
const ws = routeWorkspace(env);
|
|
148
|
+
const found = hasCenter(ws);
|
|
149
|
+
const warn = Math.round((env.cfg.remindRatio || 0.35) * 100);
|
|
150
|
+
const duty = loadDuty(ws);
|
|
151
|
+
const onDuty = (sid) => (duty.map.size ? duty.map.has(String(sid || '')) : true);
|
|
152
|
+
const dutyOf = (sid) => duty.map.get(String(sid || '')) || null;
|
|
153
|
+
const out = {
|
|
154
|
+
generatedAt: stampTz(Date.now()),
|
|
155
|
+
plugin: {
|
|
156
|
+
name: info.name || '@mingmaple/dsh-handover',
|
|
157
|
+
version: info.version || '?',
|
|
158
|
+
tools: typeof info.tools === 'number' ? info.tools : 0,
|
|
159
|
+
inject: guard(() => env.injectList(), []),
|
|
160
|
+
initErrors: guard(() => env.initErrors(), []),
|
|
161
|
+
scheduler: info.scheduler || '未知',
|
|
162
|
+
},
|
|
163
|
+
workspace: { root: ws, found },
|
|
164
|
+
ratios: guard(() => ({
|
|
165
|
+
remind: Math.round((env.cfg.remindRatio !== undefined ? env.cfg.remindRatio : 0.35) * 100),
|
|
166
|
+
remind2: Math.round((env.cfg.remind2Ratio !== undefined ? env.cfg.remind2Ratio : 0.5) * 100),
|
|
167
|
+
handover: Math.round((env.cfg.handoverRatio !== undefined ? env.cfg.handoverRatio : 0.35) * 100),
|
|
168
|
+
hard: Math.round((env.cfg.hardRatio !== undefined ? env.cfg.hardRatio : 0.85) * 100),
|
|
169
|
+
}), { remind: 35, remind2: 50, handover: 35, hard: 85 }),
|
|
170
|
+
guardLast: guard(() => env.guardLast(), null),
|
|
171
|
+
extraPrompt: guard(() => (typeof env.extraPrompt === 'function' ? env.extraPrompt() : ''), ''),
|
|
172
|
+
extraPromptEnabled: guard(() => (typeof env.extraPromptEnabled === 'function' ? env.extraPromptEnabled() : false), false),
|
|
173
|
+
extraPromptOrder: 950,
|
|
174
|
+
roles: guard(() => {
|
|
175
|
+
const store = readRoles(env.cfg);
|
|
176
|
+
const map = {};
|
|
177
|
+
for (const sid of Object.keys(store.sessions)) {
|
|
178
|
+
const e = store.sessions[sid] || {};
|
|
179
|
+
map[sid] = { role: String(e.role || ''), project: String(e.project || ''), task: String(e.task || ''), setAt: String(e.setAt || '') };
|
|
180
|
+
}
|
|
181
|
+
return map;
|
|
182
|
+
}, {}),
|
|
183
|
+
consent: guard(() => listConsent().map((r) => ({ ...r, onduty: onDuty(r.sessionId) })), []),
|
|
184
|
+
profiles: guard(() => listProfiles(ws), []),
|
|
185
|
+
guide: guard(() => buildGuide(env.cfg), { intro: [], situations: [], groups: [], rules: [] }),
|
|
186
|
+
gate: null,
|
|
187
|
+
handover: null,
|
|
188
|
+
sessions: null,
|
|
189
|
+
notes: [],
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
if (!found) {
|
|
193
|
+
out.notes.push('未找到 _Control_Center:本实例尚未接入工作区。可在插件 config 中填写 workspaceRoot。');
|
|
194
|
+
out.notes.push('插件本身已加载:工具与设置页不依赖工作区,仅闸门、交接与会话三部分需要工作区。');
|
|
195
|
+
out.setup = { steps: SETUP_STEPS };
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
out.gate = guard(() => {
|
|
200
|
+
const g = syncCheck(ws);
|
|
201
|
+
return {
|
|
202
|
+
ok: !!g.ok,
|
|
203
|
+
error: g.error || '',
|
|
204
|
+
verdict: g.verdict || '',
|
|
205
|
+
total: g.total || 0,
|
|
206
|
+
bodyLines: g.bodyLines || 0,
|
|
207
|
+
libraries: (g.libraries || []).map((r) => ({
|
|
208
|
+
name: r.name, ok: !!r.ok, titleOk: !!r.titleOk, waterOk: !!r.waterOk,
|
|
209
|
+
missing: r.missing ? r.missing.length : 0, lines: r.total || 0,
|
|
210
|
+
})),
|
|
211
|
+
};
|
|
212
|
+
}, { ok: false, libraries: [] });
|
|
213
|
+
|
|
214
|
+
out.handover = guard(() => {
|
|
215
|
+
const g = lintGlobal(path.join(ws, '_Control_Center', 'records', 'Global_Handover.md'));
|
|
216
|
+
const projects = lintProjects(ws);
|
|
217
|
+
const misc = lintMisc(ws, duty.office);
|
|
218
|
+
const bad = projects.concat(misc).filter((p) => p.verdict === '不合格');
|
|
219
|
+
return {
|
|
220
|
+
global: { verdict: g.verdict, reasons: Array.isArray(g.reasons) ? g.reasons.slice(0, 5) : [] },
|
|
221
|
+
projects,
|
|
222
|
+
misc,
|
|
223
|
+
fail: bad.length,
|
|
224
|
+
};
|
|
225
|
+
}, { global: { verdict: '读不到', reasons: [] }, projects: [], misc: [], fail: 0 });
|
|
226
|
+
|
|
227
|
+
out.systems = guard(() => {
|
|
228
|
+
const r = checkRegistries(ws);
|
|
229
|
+
return { rows: r.rows.map((x) => ({ label: x.label, rel: x.rel, exists: x.exists, entries: x.entries, bad: x.bad.length })), missing: r.missing };
|
|
230
|
+
}, { rows: [], missing: 0 });
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const legacy = legacyInfo(ws);
|
|
234
|
+
if (legacy.exists) {
|
|
235
|
+
out.notes.push('检测到旧文档定位(_Control_Center/records/docmap/):执行 ho_workspace(action="migrate") 并入体系注册。');
|
|
236
|
+
}
|
|
237
|
+
} catch { /* 旧目录检查失败不影响整页 */ }
|
|
238
|
+
|
|
239
|
+
out.sessions = guard(() => {
|
|
240
|
+
const wide = scanSessions({
|
|
241
|
+
dir: env.cacheDir(), meId: null, meCwd: null, warn,
|
|
242
|
+
silentHours: env.cfg.sessionScanHours, projectsOnly: false,
|
|
243
|
+
});
|
|
244
|
+
const store = readRoles(env.cfg);
|
|
245
|
+
const roleOf = (sid, cwd, firstPrompt, title) => {
|
|
246
|
+
const meta = sessionMeta(sid);
|
|
247
|
+
return resolveRole({ cfg: env.cfg, roles: store, sessionId: sid, cwd, firstPrompt, subagent: meta.subagent, title });
|
|
248
|
+
};
|
|
249
|
+
// 工作区与归档:设置页按工作区分组罗列,已归档与目录已不存在的会话不再出现。
|
|
250
|
+
const wsIndex = loadWorkspaceIndex();
|
|
251
|
+
const live = liveSessionDirs();
|
|
252
|
+
// 会话 id 在缓存里有带与不带 session- 前缀两种写法,统一成带前缀再比。
|
|
253
|
+
const sidKey = (sid) => (/^session-/.test(String(sid)) ? String(sid) : 'session-' + String(sid));
|
|
254
|
+
const archived = (sid) => wsIndex.archived.has(sidKey(sid));
|
|
255
|
+
const stillThere = (sid) => live.size === 0 || live.has(sidKey(sid));
|
|
256
|
+
// 分组只认 DSH 工作区登记表的 sessionIds,不按 cwd 推断归属。
|
|
257
|
+
// 未登记的会话归入「未分组」,与 DSH 侧栏一致;cwd 推断会把未分组会话误并入同名工作区。
|
|
258
|
+
const realCache = new Map();
|
|
259
|
+
const realOf = (p) => {
|
|
260
|
+
const key = pathKey(String(p || ''));
|
|
261
|
+
if (realCache.has(key)) return realCache.get(key);
|
|
262
|
+
let v = '';
|
|
263
|
+
try { v = fs.realpathSync(String(p || '')); } catch { v = ''; }
|
|
264
|
+
realCache.set(key, v);
|
|
265
|
+
return v;
|
|
266
|
+
};
|
|
267
|
+
const wsOf = (sid) => wsIndex.bySession.get(sidKey(sid)) || null;
|
|
268
|
+
const mapRow = (r) => {
|
|
269
|
+
const meta = sessionMeta(r.sid);
|
|
270
|
+
const info = roleOf(r.sid, meta.cwd, meta.firstPrompt, r.project);
|
|
271
|
+
const d = dutyOf(r.sid);
|
|
272
|
+
const w = wsOf(r.sid);
|
|
273
|
+
// 目录已不存在的老会话(旧树里删掉的项目库等)不再罗列。
|
|
274
|
+
const cwdOk = !meta.cwd || !!realOf(meta.cwd);
|
|
275
|
+
return {
|
|
276
|
+
sid: r.sid,
|
|
277
|
+
title: r.project === '-' ? '' : r.project,
|
|
278
|
+
project: r.project,
|
|
279
|
+
role: info.role,
|
|
280
|
+
roleLabel: ROLE_LABEL[info.role] || info.role,
|
|
281
|
+
source: info.source,
|
|
282
|
+
projectName: info.project,
|
|
283
|
+
task: info.task,
|
|
284
|
+
cwdOk,
|
|
285
|
+
// 工作区分组:只认 DSH 工作区登记;未登记时留空,客户端归入「未分组」。
|
|
286
|
+
wsKey: w ? w.key : '',
|
|
287
|
+
wsTitle: w ? w.title : '',
|
|
288
|
+
wsPath: w ? w.path : String(meta.cwd || ''),
|
|
289
|
+
// 类别与名称以在任登记为准:登记过的会话才能说清自己是全局、项目还是杂项。
|
|
290
|
+
dutyKind: d ? d.kindLabel : '',
|
|
291
|
+
dutyKey: d ? d.key : '',
|
|
292
|
+
dutyName: d ? d.name : '',
|
|
293
|
+
pct: r.pct,
|
|
294
|
+
state: r.state,
|
|
295
|
+
active: r.mtime >= wide.cutoffMs,
|
|
296
|
+
onduty: !!d,
|
|
297
|
+
mtime: r.mtime,
|
|
298
|
+
seen: stampTz(r.mtime),
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
const all = wide.rows
|
|
302
|
+
.filter((r) => r.kind === '平级' && !archived(r.sid) && stillThere(r.sid))
|
|
303
|
+
.map(mapRow);
|
|
304
|
+
// 不按 cwd 是否存在过滤:DSH 的工作区与会话列表只认会话本身与归档表。
|
|
305
|
+
// cwd 已删除的旧会话仍可能被打开或恢复,误删会把 DSH 中存在的会话藏掉。
|
|
306
|
+
// 在任集合:全局、各项目、各杂项任务的现任各一行;普通会话不在登记表里,因此不出现。
|
|
307
|
+
const rows = all.filter((r) => r.onduty).slice(0, 60);
|
|
308
|
+
// 通知:每条登记的在任会话,若本轮仍活跃;已交出的会话即使更活跃也不通知。
|
|
309
|
+
const notify = [];
|
|
310
|
+
for (const sid of duty.map.keys()) {
|
|
311
|
+
const hit = rows.find((r) => r.sid === sid);
|
|
312
|
+
if (hit && hit.mtime >= wide.cutoffMs) notify.push({ sid: hit.sid, key: hit.dutyKey, name: hit.dutyName });
|
|
313
|
+
}
|
|
314
|
+
const hot = rows.filter((r) => r.pct !== null && r.pct >= warn).length;
|
|
315
|
+
return {
|
|
316
|
+
rows,
|
|
317
|
+
all,
|
|
318
|
+
workspaces: wsIndex.list.map((w) => ({ key: w.key, title: w.title || w.path, path: w.path })),
|
|
319
|
+
notify,
|
|
320
|
+
onduty: duty.map.size,
|
|
321
|
+
hot,
|
|
322
|
+
unknown: wide.unknown.length,
|
|
323
|
+
cutoff: stampTz(wide.cutoffMs),
|
|
324
|
+
silentHours: wide.silentHours,
|
|
325
|
+
verdict: hot
|
|
326
|
+
? hot + ' 个在任会话已过 ' + warn + '% 线,应先收齐交接,再更换会话'
|
|
327
|
+
: '没有在任会话超过 ' + warn + '% 线。',
|
|
328
|
+
};
|
|
329
|
+
}, { rows: [], notify: [], hot: 0, unknown: 0 });
|
|
330
|
+
|
|
331
|
+
// 设置页用:已接入工作区但没有全局会话时,给出补建入口。
|
|
332
|
+
out.globalSessions = guard(() => {
|
|
333
|
+
const all = (out.sessions && Array.isArray(out.sessions.all)) ? out.sessions.all : [];
|
|
334
|
+
return all.filter((r) => r && r.role === 'global').length;
|
|
335
|
+
}, 0);
|
|
336
|
+
|
|
337
|
+
return out;
|
|
337
338
|
}
|