@mingmaple/dsh-handover 0.9.14 → 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 +123 -92
- 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 +34 -0
- package/docs/releases/1.0.0.md +33 -0
- package/docs/troubleshoot.md +230 -46
- package/package.json +74 -71
- package/scripts/behavior.mjs +398 -0
- package/scripts/blank-dsh.mjs +41 -25
- package/scripts/blank-smoke.mjs +278 -109
- package/scripts/check-docs.mjs +7 -0
- package/scripts/e2e-migration.mjs +2 -2
- package/scripts/office.mjs +3 -3
- package/scripts/portability.mjs +71 -0
- package/scripts/style-gate.mjs +571 -0
- package/src/client.js +1028 -955
- package/src/index.mjs +313 -142
- package/src/lib/absrefs.mjs +1 -1
- package/src/lib/api.mjs +18 -10
- package/src/lib/archive.mjs +1 -1
- package/src/lib/brief.mjs +8 -6
- package/src/lib/config.mjs +23 -8
- package/src/lib/consent.mjs +54 -1
- package/src/lib/docmap.mjs +140 -0
- 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 +30 -6
- package/src/lib/global-task.mjs +69 -0
- package/src/lib/guide.mjs +81 -85
- 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 +17 -13
- package/src/lib/manual.mjs +135 -59
- package/src/lib/mover.mjs +23 -19
- package/src/lib/names.mjs +14 -19
- package/src/lib/office.mjs +63 -15
- package/src/lib/pack-ops.mjs +11 -11
- package/src/lib/pathkey.mjs +24 -0
- package/src/lib/peer.mjs +4 -4
- 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 +9 -6
- package/src/lib/scaffold.mjs +68 -26
- package/src/lib/schedule.mjs +5 -6
- package/src/lib/session-create.mjs +3 -3
- package/src/lib/session-ops.mjs +80 -51
- package/src/lib/sessions.mjs +41 -3
- package/src/lib/sevenzip.mjs +10 -1
- package/src/lib/status.mjs +337 -176
- package/src/lib/succession.mjs +16 -16
- package/src/lib/system.mjs +364 -0
- package/src/lib/zstd-log.mjs +1 -1
- package/src/selftest.mjs +6 -6
- 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 +101 -20
- package/src/templates/Runtime_Manual.md +263 -122
- 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 -23
- 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 +37 -0
- 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 +135 -0
- package/src/tools/ho_changelog.mjs +4 -4
- package/src/tools/ho_charter.mjs +7 -6
- package/src/tools/ho_checkpoint.mjs +36 -32
- package/src/tools/ho_docmap.mjs +239 -0
- package/src/tools/ho_gate.mjs +11 -5
- package/src/tools/ho_handover.mjs +61 -35
- package/src/tools/ho_office.mjs +46 -5
- package/src/tools/ho_profile.mjs +3 -3
- package/src/tools/ho_sessions.mjs +46 -26
- package/src/tools/ho_status.mjs +251 -0
- package/src/tools/ho_succeed.mjs +3 -3
- package/src/tools/ho_workspace.mjs +146 -42
- package/src/tools/peer.mjs +416 -0
- package/src/tools/ho_brief.mjs +0 -57
- package/src/tools/ho_diag.mjs +0 -87
- package/src/tools/ho_drift.mjs +0 -41
- package/src/tools/ho_init.mjs +0 -82
- package/src/tools/ho_pack.mjs +0 -38
- package/src/tools/ho_phase.mjs +0 -37
- package/src/tools/ho_snapshot.mjs +0 -104
- package/src/tools/ho_tokens.mjs +0 -57
- package/src/tools/peer_list.mjs +0 -37
- package/src/tools/peer_new.mjs +0 -75
- package/src/tools/peer_peek.mjs +0 -41
- package/src/tools/peer_rename.mjs +0 -31
- package/src/tools/peer_send.mjs +0 -52
- package/src/tools/peer_status.mjs +0 -46
package/src/lib/status.mjs
CHANGED
|
@@ -1,177 +1,338 @@
|
|
|
1
|
-
|
|
2
|
-
* 设置页用的状态快照(只读):插件自身状态 + 闸门 + 交接体检 + 会话水位 + 最近一次守卫判定。
|
|
3
|
-
* 全部读本地文件,不唤醒任何会话、不花任何会话的 token。
|
|
4
|
-
* 任何一段出错都只让该段带 error 字段,不让整页 500。
|
|
5
|
-
*/
|
|
6
|
-
import fs from 'node:fs';
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
out
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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;
|
|
177
338
|
}
|
package/src/lib/succession.mjs
CHANGED
|
@@ -100,7 +100,7 @@ function appendSuccessionBlock(file, lines) {
|
|
|
100
100
|
export async function runSuccession(env, exec, opts) {
|
|
101
101
|
const o = opts || {};
|
|
102
102
|
const note = typeof o.note === 'string' ? o.note.trim() : '';
|
|
103
|
-
const reject = (reason) => '
|
|
103
|
+
const reject = (reason) => '换任未执行,未写入任何内容。' + NL + reason;
|
|
104
104
|
|
|
105
105
|
// 1. 自身角色与类别:只认已登记的角色。
|
|
106
106
|
const info = env.roleOf(exec) || {};
|
|
@@ -114,14 +114,14 @@ export async function runSuccession(env, exec, opts) {
|
|
|
114
114
|
|
|
115
115
|
// 2. 用户授权:AI 不能自行批准。
|
|
116
116
|
const selfId = env.selfId(exec);
|
|
117
|
-
if (!selfId) return reject('
|
|
117
|
+
if (!selfId) return reject('无法获取当前会话 id,无法校验授权。');
|
|
118
118
|
const cs = checkConsent(selfId, 'succeed');
|
|
119
119
|
if (!cs.ok) {
|
|
120
120
|
return reject([
|
|
121
121
|
cs.reason + '。',
|
|
122
122
|
'授权方式(任选其一):',
|
|
123
|
-
' ① 在聊天框发送 /allow-succeed
|
|
124
|
-
' ② 在设置页的 dsh-handover
|
|
123
|
+
' ① 在聊天框发送 /allow-succeed(仅授权本会话,最为快捷);',
|
|
124
|
+
' ② 在设置页的 dsh-handover 中,点击「授权」内的「授权换任」。',
|
|
125
125
|
'授权有效期 5 分钟,用后失效。',
|
|
126
126
|
].join(NL));
|
|
127
127
|
}
|
|
@@ -129,7 +129,7 @@ export async function runSuccession(env, exec, opts) {
|
|
|
129
129
|
// 3. 交接结构检查(只判结构,不判质量)。
|
|
130
130
|
const hp = env.handoverFor(exec) || {};
|
|
131
131
|
const handoverFile = hp.path ? String(hp.path) : '';
|
|
132
|
-
if (!handoverFile) return reject('
|
|
132
|
+
if (!handoverFile) return reject('没有可用的交接路径:' + (hp.reason || '本会话角色未确定'));
|
|
133
133
|
const problems = auditHandover(handoverFile);
|
|
134
134
|
if (problems.length) {
|
|
135
135
|
return reject(['交接结构未通过:' + handoverFile]
|
|
@@ -145,10 +145,10 @@ export async function runSuccession(env, exec, opts) {
|
|
|
145
145
|
const lin = store.lineages[key];
|
|
146
146
|
// 只有本类别的在任者本人可换任:既防旧会话复活,也防同项目的旁支会话抢先。
|
|
147
147
|
if (!lin || !lin.current || !lin.current.sessionId) {
|
|
148
|
-
return reject('本类别(' + key + '
|
|
148
|
+
return reject('本类别(' + key + ')在在任登记表中尚无在任记录:先由全局建立会话并登记,再换任。');
|
|
149
149
|
}
|
|
150
150
|
if (String(lin.current.sessionId) !== String(selfId)) {
|
|
151
|
-
return reject('
|
|
151
|
+
return reject('本会话不是在任者:本类别在任为 ' + String(lin.current.name || '(未命名)')
|
|
152
152
|
+ '(' + String(lin.current.sessionId) + '),不得换任。');
|
|
153
153
|
}
|
|
154
154
|
{
|
|
@@ -156,17 +156,17 @@ export async function runSuccession(env, exec, opts) {
|
|
|
156
156
|
const closed = history.find((h) => h && String(h.sessionId || '') === String(selfId) && h.to);
|
|
157
157
|
if (closed) {
|
|
158
158
|
const curName = lin.current && lin.current.name ? lin.current.name : '(空缺)';
|
|
159
|
-
return reject('本类别已为本会话记过一次换任(' + String(closed.to || '') + '
|
|
159
|
+
return reject('本类别已为本会话记过一次换任(' + String(closed.to || '') + ',在任 ' + curName + '),旧会话不得再次换任。');
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
// 4.1
|
|
163
|
+
// 4.1 水位未到线:本会话就是在任者且水位低于提醒线时,换任通常多余。
|
|
164
164
|
// 放在授权与交接校验之后、任何写入之前;取不到水位就跳过。
|
|
165
165
|
if (o.force !== true) {
|
|
166
166
|
let pct = null;
|
|
167
167
|
try { pct = typeof env.selfPct === 'function' ? env.selfPct(selfId) : null; } catch { pct = null; }
|
|
168
|
-
if (pct !== null && pct !== undefined && Number(pct) < watermark(env.cfg).
|
|
169
|
-
return reject('本会话水位 ' + Number(pct).toFixed(1) + '
|
|
168
|
+
if (pct !== null && pct !== undefined && Number(pct) < watermark(env.cfg).remindValue) {
|
|
169
|
+
return reject('本会话水位 ' + Number(pct).toFixed(1) + '%,未到提醒线,通常无需换任;确需换任请传 force=true。');
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
@@ -188,14 +188,14 @@ export async function runSuccession(env, exec, opts) {
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
// 7. 拼启动提示词:末尾追加交接路径与「先读交接、读毕报到」。
|
|
191
|
-
// 「全局 id
|
|
191
|
+
// 「全局 id」填**现任全局**:项目与杂项换任时,已交出的旧会话不是全局,不能顶替这个位置。
|
|
192
192
|
let prompt = '';
|
|
193
193
|
try {
|
|
194
194
|
const globalId = kind === 'global' ? selfId : (currentGlobalId(ws) || selfId);
|
|
195
195
|
const base = promptFor(env, { ws, kind, project, task, title: name, sessionId: '', globalSessionId: globalId, extra: '' });
|
|
196
196
|
prompt = base + NL + NL + [
|
|
197
197
|
'接任交接:' + handoverFile,
|
|
198
|
-
'
|
|
198
|
+
'先读这份交接,再开始作业。读毕向当前用户报告:当前停在哪一步;交接列有哪些待办。随后等待当前用户指派任务。',
|
|
199
199
|
].join(NL);
|
|
200
200
|
} catch (e) {
|
|
201
201
|
return reject('启动提示词生成失败:' + msgOf(e));
|
|
@@ -215,7 +215,7 @@ export async function runSuccession(env, exec, opts) {
|
|
|
215
215
|
} catch (e) {
|
|
216
216
|
return [
|
|
217
217
|
'会话已建 ' + sessionId + '(' + name + '),但登记在任失败:' + msgOf(e),
|
|
218
|
-
'
|
|
218
|
+
'启动提示词尚未投递。先修好登记表,再用 peer(action="send")通知该会话。',
|
|
219
219
|
].join(NL);
|
|
220
220
|
}
|
|
221
221
|
|
|
@@ -253,8 +253,8 @@ export async function runSuccession(env, exec, opts) {
|
|
|
253
253
|
out.push('换任完成:' + name + '(' + sessionId + ')');
|
|
254
254
|
out.push('在任登记:' + key + (reg && reg.registry && reg.registry.changed && reg.registry.changed.length
|
|
255
255
|
? ',Registry.md 已同步在役列' : ''));
|
|
256
|
-
out.push('启动提示词:' + (delivered ? '已投递' : '未送达:' + deliveryError + '。会话已建,可用
|
|
256
|
+
out.push('启动提示词:' + (delivered ? '已投递' : '未送达:' + deliveryError + '。会话已建,可用 peer(action="send")补投。'));
|
|
257
257
|
out.push('交接登记:' + appendNote);
|
|
258
|
-
out.push('
|
|
258
|
+
out.push('本会话到此停止,等待当前用户在新会话中指派任务。');
|
|
259
259
|
return out.join(NL);
|
|
260
260
|
}
|