@mingmaple/dsh-handover 0.9.12 → 0.9.13
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 +15 -10
- package/docs/releases/0.9.12.md +28 -0
- package/docs/releases/0.9.13.md +30 -0
- package/docs/troubleshoot.md +11 -3
- package/package.json +1 -1
- package/scripts/blank-dsh.mjs +6 -5
- package/scripts/blank-smoke.mjs +72 -23
- package/scripts/check-docs.mjs +23 -2
- package/scripts/office.mjs +81 -0
- package/src/client.js +25 -14
- package/src/index.mjs +47 -23
- package/src/lib/api.mjs +5 -4
- package/src/lib/consent.mjs +59 -20
- package/src/lib/facts.mjs +5 -4
- package/src/lib/guide.mjs +4 -1
- package/src/lib/manual.mjs +23 -12
- package/src/lib/names.mjs +2 -2
- package/src/lib/office.mjs +272 -0
- package/src/lib/phase.mjs +1 -1
- package/src/lib/role.mjs +4 -4
- package/src/lib/session-ops.mjs +147 -89
- package/src/lib/succession.mjs +244 -0
- package/src/lib/workspace.mjs +1 -1
- package/src/selftest.mjs +15 -0
- package/src/templates/Charter.skeleton.md +1 -1
- package/src/templates/Prompt_Templates.md +9 -17
- package/src/templates/Runtime_Manual.md +12 -9
- package/src/templates/rules/Handover_Rules.md +2 -2
- package/src/templates/rules/Misc_Task_Standard.md +3 -3
- package/src/tools/ho_office.mjs +59 -0
- package/src/tools/ho_succeed.mjs +24 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 在任登记表(office):回答「这条类别现在谁在任」。
|
|
3
|
+
*
|
|
4
|
+
* 记录保存在 <工作区根>/_Control_Center/records/Office.json:每一类会话(全局/项目名/杂项任务名)
|
|
5
|
+
* 记一个在任会话与历史编号。Registry.md 的「在役会话」列由本文件定点改写——只换那一格,
|
|
6
|
+
* 不动人写的状态文字与整表结构。编号由这里分配(历史最大号 + 1),不采信会话自述。
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { NL, readUtf8 } from './text.mjs';
|
|
11
|
+
|
|
12
|
+
export const OFFICE_VERSION = 1;
|
|
13
|
+
|
|
14
|
+
/** 带时区偏移的时刻文本(与交接、登记表同一写法)。 */
|
|
15
|
+
export function stampNow() {
|
|
16
|
+
const d = new Date(Date.now() + 8 * 3600 * 1000);
|
|
17
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
18
|
+
return d.getUTCFullYear() + '-' + p(d.getUTCMonth() + 1) + '-' + p(d.getUTCDate())
|
|
19
|
+
+ ' ' + p(d.getUTCHours()) + ':' + p(d.getUTCMinutes()) + ' +0800';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const KIND_LABEL = { global: '全局', project: '项目', misc: '杂项' };
|
|
23
|
+
|
|
24
|
+
export function officePath(ws) {
|
|
25
|
+
return path.join(String(ws || ''), '_Control_Center', 'records', 'Office.json');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function loadOffice(ws) {
|
|
29
|
+
let v = null;
|
|
30
|
+
try { v = JSON.parse(readUtf8(officePath(ws)).replace(/^\uFEFF/, '')); } catch { v = null; }
|
|
31
|
+
if (!v || typeof v !== 'object' || Array.isArray(v)) v = {};
|
|
32
|
+
if (!v.lineages || typeof v.lineages !== 'object' || Array.isArray(v.lineages)) v.lineages = {};
|
|
33
|
+
v.version = OFFICE_VERSION;
|
|
34
|
+
return v;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 原子写:临时文件 + rename,半截文件不会被下一次读当成事实。 */
|
|
38
|
+
export function saveOffice(ws, store) {
|
|
39
|
+
const p = officePath(ws);
|
|
40
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
41
|
+
store.version = OFFICE_VERSION;
|
|
42
|
+
store.updatedAt = stampNow();
|
|
43
|
+
const tmp = p + '.tmp';
|
|
44
|
+
fs.writeFileSync(tmp, JSON.stringify(store, null, 2) + NL, 'utf8');
|
|
45
|
+
fs.renameSync(tmp, p);
|
|
46
|
+
return p;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 类别名:全局固定「全局」,项目用项目名,杂项用任务名。 */
|
|
50
|
+
export function lineageKeyOf(kind, project, task) {
|
|
51
|
+
if (kind === 'global') return '全局';
|
|
52
|
+
if (kind === 'project') return String(project || '').trim();
|
|
53
|
+
if (kind === 'misc') return String(task || '').trim();
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const GEN_RE = /^(.*\D)(\d+)$/;
|
|
58
|
+
|
|
59
|
+
/** 名字去掉尾部代际:全局11 → 全局;没有数字返回空串。 */
|
|
60
|
+
export function baseOf(name) {
|
|
61
|
+
const m = GEN_RE.exec(String(name || '').trim());
|
|
62
|
+
return m ? m[1] : '';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 名字里的代际号:全局11 → 11;没有数字返回 null。 */
|
|
66
|
+
export function generationOf(name) {
|
|
67
|
+
const m = GEN_RE.exec(String(name || '').trim());
|
|
68
|
+
return m ? Number(m[2]) : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 下一编号:该类别历史上出现过的最大号 + 1;没有历史就从 1 起算。 */
|
|
72
|
+
export function nextName(store, key) {
|
|
73
|
+
const lin = store && store.lineages ? store.lineages[key] : null;
|
|
74
|
+
const names = [];
|
|
75
|
+
if (lin && lin.current && lin.current.name) names.push(lin.current.name);
|
|
76
|
+
for (const h of (lin && Array.isArray(lin.history) ? lin.history : [])) {
|
|
77
|
+
if (h && h.name) names.push(h.name);
|
|
78
|
+
}
|
|
79
|
+
let max = 0;
|
|
80
|
+
for (const n of names) {
|
|
81
|
+
const g = generationOf(n);
|
|
82
|
+
if (g !== null && g > max) max = g;
|
|
83
|
+
}
|
|
84
|
+
return String(key) + String(max + 1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 名字 → 类别。用户给的多半是不确定的名字(全局3、全局、Mingent、赢了两次的旧叫法),
|
|
89
|
+
* 按「类别名精确 → 去尾部编号 → 现任名 → 唯一子串」四步收敛;收敛不了就报候选,不瞎猜。
|
|
90
|
+
*/
|
|
91
|
+
export function resolveOffice(store, query) {
|
|
92
|
+
const q = String(query || '').trim();
|
|
93
|
+
if (!q) return { ok: false, reason: '空查询' };
|
|
94
|
+
const keys = Object.keys((store && store.lineages) || {});
|
|
95
|
+
const lower = q.toLowerCase();
|
|
96
|
+
let key = '';
|
|
97
|
+
let via = '';
|
|
98
|
+
const exact = keys.filter((k) => k.toLowerCase() === lower);
|
|
99
|
+
if (exact.length === 1) { key = exact[0]; via = 'exact'; }
|
|
100
|
+
if (!key) {
|
|
101
|
+
const base = baseOf(q);
|
|
102
|
+
if (base) {
|
|
103
|
+
const hit = keys.filter((k) => k.toLowerCase() === base.toLowerCase());
|
|
104
|
+
if (hit.length === 1) { key = hit[0]; via = 'generation'; }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!key) {
|
|
108
|
+
const hit = keys.filter((k) => {
|
|
109
|
+
const cur = store.lineages[k] && store.lineages[k].current;
|
|
110
|
+
return cur && cur.name && String(cur.name).toLowerCase() === lower;
|
|
111
|
+
});
|
|
112
|
+
if (hit.length === 1) { key = hit[0]; via = 'current'; }
|
|
113
|
+
}
|
|
114
|
+
if (!key) {
|
|
115
|
+
const hit = keys.filter((k) => {
|
|
116
|
+
const a = k.toLowerCase();
|
|
117
|
+
return a.indexOf(lower) !== -1 || lower.indexOf(a) !== -1;
|
|
118
|
+
});
|
|
119
|
+
if (hit.length === 1) { key = hit[0]; via = 'fuzzy'; }
|
|
120
|
+
else if (hit.length > 1) return { ok: false, reason: '与多个类别相近:' + hit.join('、') + '(请写全名)', candidates: hit };
|
|
121
|
+
}
|
|
122
|
+
if (!key) return { ok: false, reason: '没有名为「' + q + '」的全局、项目或杂项任务' };
|
|
123
|
+
const lin = store.lineages[key];
|
|
124
|
+
const cur = lin && lin.current;
|
|
125
|
+
if (!cur || !cur.name) return { ok: true, key, lineage: lin, via, vacant: true };
|
|
126
|
+
return { ok: true, key, lineage: lin, via, vacant: false };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function rowsOf(store) {
|
|
130
|
+
const lines = (store && store.lineages) || {};
|
|
131
|
+
return Object.keys(lines).map((key) => {
|
|
132
|
+
const lin = lines[key] || {};
|
|
133
|
+
return {
|
|
134
|
+
key,
|
|
135
|
+
kind: String(lin.kind || ''),
|
|
136
|
+
project: String(lin.project || ''),
|
|
137
|
+
task: String(lin.task || ''),
|
|
138
|
+
workspace: String(lin.workspace || ''),
|
|
139
|
+
current: lin.current || null,
|
|
140
|
+
};
|
|
141
|
+
}).sort((a, b) => {
|
|
142
|
+
const order = { global: 0, project: 1, misc: 2 };
|
|
143
|
+
const d = (order[a.kind] === undefined ? 9 : order[a.kind]) - (order[b.kind] === undefined ? 9 : order[b.kind]);
|
|
144
|
+
return d !== 0 ? d : a.key.localeCompare(b.key, 'zh');
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 一行人类可读:名字、会话 id、类别、(提到旧编号时的来处)。 */
|
|
149
|
+
export function formatRow(row, askedAs) {
|
|
150
|
+
const cur = row && row.current;
|
|
151
|
+
if (!cur || !cur.name) return (row && row.key ? row.key : '?') + ' — 当前空缺';
|
|
152
|
+
const label = KIND_LABEL[row.kind] || row.kind || '-';
|
|
153
|
+
// 只有「拿旧编号问现任」时才加来处标注;直接问类别名或现任名就不必重复。
|
|
154
|
+
const tail = askedAs && askedAs !== cur.name && askedAs !== row.key ? '(' + askedAs + ' → ' + row.key + ')' : '';
|
|
155
|
+
return cur.name + ' ' + cur.sessionId + ' ' + label + (cur.since ? ' 自 ' + cur.since : '') + tail;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 登记一位在任者:原在任者进历史,写盘后同步 Registry.md。 */
|
|
159
|
+
export function registerHolder(ws, opts) {
|
|
160
|
+
const o = opts || {};
|
|
161
|
+
const key = lineageKeyOf(o.kind, o.project, o.task);
|
|
162
|
+
if (!key) throw new Error('类别名缺失:全局用 kind=global,项目给 project,杂项给 task');
|
|
163
|
+
const store = loadOffice(ws);
|
|
164
|
+
const lin = store.lineages[key] || (store.lineages[key] = {
|
|
165
|
+
kind: o.kind, project: String(o.project || ''), task: String(o.task || ''),
|
|
166
|
+
workspace: String(o.workspace || ''), current: null, history: [],
|
|
167
|
+
});
|
|
168
|
+
if (!Array.isArray(lin.history)) lin.history = [];
|
|
169
|
+
const previous = lin.current && lin.current.name ? { ...lin.current } : null;
|
|
170
|
+
if (previous) {
|
|
171
|
+
lin.history.push({
|
|
172
|
+
name: previous.name, sessionId: previous.sessionId,
|
|
173
|
+
from: previous.since || '', to: stampNow(), note: previous.note || '',
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
lin.kind = o.kind;
|
|
177
|
+
if (o.project) lin.project = String(o.project);
|
|
178
|
+
if (o.task) lin.task = String(o.task);
|
|
179
|
+
if (o.workspace) lin.workspace = String(o.workspace);
|
|
180
|
+
lin.current = { name: String(o.name || ''), sessionId: String(o.sessionId || ''), since: stampNow(), note: String(o.note || '') };
|
|
181
|
+
if (!lin.current.name || !lin.current.sessionId) throw new Error('在任登记需要 name 与 sessionId');
|
|
182
|
+
saveOffice(ws, store);
|
|
183
|
+
let registry = { changed: [], error: '' };
|
|
184
|
+
try { registry = syncRegistry(ws, store); } catch (e) { registry = { changed: [], error: String(e && e.message ? e.message : e) }; }
|
|
185
|
+
return { key, previous, current: lin.current, registry };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** 类别当前空缺(会话退役、项目暂停)。 */
|
|
189
|
+
export function retireHolder(ws, key, note) {
|
|
190
|
+
const store = loadOffice(ws);
|
|
191
|
+
const lin = store.lineages[String(key || '')];
|
|
192
|
+
if (!lin) throw new Error('没有这条类别:' + key);
|
|
193
|
+
const previous = lin.current && lin.current.name ? { ...lin.current } : null;
|
|
194
|
+
if (previous) {
|
|
195
|
+
lin.history.push({ name: previous.name, sessionId: previous.sessionId, from: previous.since || '', to: stampNow(), note: note || previous.note || '' });
|
|
196
|
+
}
|
|
197
|
+
lin.current = null;
|
|
198
|
+
saveOffice(ws, store);
|
|
199
|
+
let registry = { changed: [], error: '' };
|
|
200
|
+
try { registry = syncRegistry(ws, store); } catch (e) { registry = { changed: [], error: String(e && e.message ? e.message : e) }; }
|
|
201
|
+
return { key, previous, registry };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------- Registry.md 定点同步
|
|
205
|
+
|
|
206
|
+
function splitCells(line) {
|
|
207
|
+
// "| a | b |" → ['', ' a ', ' b ', ''];保持两端空格原样,便于回写。
|
|
208
|
+
return line.split('|');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function tailOf(cell) {
|
|
212
|
+
const m = /([^()]*)\s*$/.exec(cell) || /\([^()]*\)\s*$/.exec(cell);
|
|
213
|
+
return m ? ' ' + m[0].trim() : '';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function currentCellFor(row, withName) {
|
|
217
|
+
const cur = row.current;
|
|
218
|
+
if (!cur || !cur.name) return null;
|
|
219
|
+
return withName ? cur.name + ' `' + cur.sessionId + '`' : '`' + cur.sessionId + '`';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 把 Registry.md 里的「在役会话」格改成登记表里的现任。
|
|
224
|
+
* 只认两张表(项目清单、未登记但存在的任务),只换那一格;其它文字一律不动。
|
|
225
|
+
*/
|
|
226
|
+
export function syncRegistry(ws, store) {
|
|
227
|
+
const file = path.join(String(ws || ''), '_Project_Meta', 'Registry.md');
|
|
228
|
+
let text = '';
|
|
229
|
+
try { text = readUtf8(file); } catch { return { changed: [], error: '读不到 Registry.md' }; }
|
|
230
|
+
const bom = text.charCodeAt(0) === 0xFEFF ? '\uFEFF' : '';
|
|
231
|
+
if (bom) text = text.slice(1);
|
|
232
|
+
const lines = text.split(NL);
|
|
233
|
+
const changed = [];
|
|
234
|
+
let mode = '';
|
|
235
|
+
for (let i = 0; i < lines.length; i++) {
|
|
236
|
+
const line = lines[i];
|
|
237
|
+
if (line.startsWith('## ')) {
|
|
238
|
+
if (line.indexOf('项目清单') !== -1) mode = 'project';
|
|
239
|
+
else if (line.indexOf('未登记但存在的任务') !== -1) mode = 'misc';
|
|
240
|
+
else mode = '';
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (!mode || !line.startsWith('|')) continue;
|
|
244
|
+
const cells = splitCells(line);
|
|
245
|
+
// 表头与分隔行跳过
|
|
246
|
+
if (cells.length < 4) continue;
|
|
247
|
+
const name = String(cells[1] || '').trim();
|
|
248
|
+
if (!name || name === '项目名' || name === '任务短名' || /^-+$/.test(name)) continue;
|
|
249
|
+
const row = rowsOf(store).find((r) => r.key === name);
|
|
250
|
+
if (!row) continue;
|
|
251
|
+
if (mode === 'project') {
|
|
252
|
+
if (cells.length < 9) continue; // 8 列 + 首尾空
|
|
253
|
+
const old = String(cells[6] || '');
|
|
254
|
+
const next = currentCellFor(row, true);
|
|
255
|
+
if (!next) continue;
|
|
256
|
+
const want = ' ' + next + tailOf(old) + ' ';
|
|
257
|
+
if (old === want) continue;
|
|
258
|
+
cells[6] = want;
|
|
259
|
+
} else {
|
|
260
|
+
if (cells.length < 5) continue; // 4 列 + 首尾空
|
|
261
|
+
const next = currentCellFor(row, false);
|
|
262
|
+
if (!next) continue;
|
|
263
|
+
const want = ' ' + next + tailOf(String(cells[3] || '')) + ' ';
|
|
264
|
+
if (String(cells[3] || '') === want) continue;
|
|
265
|
+
cells[3] = want;
|
|
266
|
+
}
|
|
267
|
+
lines[i] = cells.join('|');
|
|
268
|
+
changed.push(name);
|
|
269
|
+
}
|
|
270
|
+
if (changed.length) fs.writeFileSync(file, bom + lines.join(NL), 'utf8');
|
|
271
|
+
return { changed, error: '' };
|
|
272
|
+
}
|
package/src/lib/phase.mjs
CHANGED
|
@@ -24,7 +24,7 @@ export function decide(input) {
|
|
|
24
24
|
return { action: 'unknown', reason: '水位读不到:人工判断,不要盲目压缩' };
|
|
25
25
|
}
|
|
26
26
|
if (input.ratio >= hard) {
|
|
27
|
-
return { action: 'force_compact', reason: '已到硬顶(' + Math.round(hard * 100) + '
|
|
27
|
+
return { action: 'force_compact', reason: '已到硬顶(' + Math.round(hard * 100) + '%):强制压缩,哪怕任务中途' };
|
|
28
28
|
}
|
|
29
29
|
if (input.ratio >= handover) {
|
|
30
30
|
return { action: 'checkpoint', reason: '过交接线(' + Math.round(handover * 100) + '%):写交接,随后压缩,再读交接续跑' };
|
package/src/lib/role.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* 会话角色:判定、逐角色交接路径、工具集与文案。角色表是本机状态(不进仓库、不进迁移包)。
|
|
3
3
|
* 判定顺序:角色表 → 全局标题 → 全局工作区 → 子会话(默认普通)→ cwd(Projects/<名> 或 _Misc/tasks/<任务>)→ 首条提示 → 普通。
|
|
4
4
|
*/
|
|
@@ -19,8 +19,8 @@ export const ALL_TOOL_NAMES = TOOL_NAMES.concat(PEER_NAMES);
|
|
|
19
19
|
/** 角色 → 可见工具(其余一律 deny)。改这张表就是改成本结构。 */
|
|
20
20
|
const ROLE_TOOLS = {
|
|
21
21
|
global: ALL_TOOL_NAMES.slice(),
|
|
22
|
-
project: [nameOf('phase'), nameOf('handover'), nameOf('checkpoint'), nameOf('charter'), nameOf('gate'), nameOf('snapshot'), nameOf('profile'), peerNameOf('send'), peerNameOf('status')],
|
|
23
|
-
misc: [nameOf('phase'), nameOf('handover'), nameOf('checkpoint'), nameOf('charter'), nameOf('snapshot'), nameOf('profile'), peerNameOf('send'), peerNameOf('status'), peerNameOf('new')],
|
|
22
|
+
project: [nameOf('phase'), nameOf('handover'), nameOf('checkpoint'), nameOf('charter'), nameOf('gate'), nameOf('snapshot'), nameOf('profile'), nameOf('office'), nameOf('succeed'), peerNameOf('send'), peerNameOf('status')],
|
|
23
|
+
misc: [nameOf('phase'), nameOf('handover'), nameOf('checkpoint'), nameOf('charter'), nameOf('snapshot'), nameOf('profile'), nameOf('office'), nameOf('succeed'), peerNameOf('send'), peerNameOf('status'), peerNameOf('new')],
|
|
24
24
|
plain: [],
|
|
25
25
|
};
|
|
26
26
|
|
|
@@ -179,7 +179,7 @@ export function resolveRole(opts) {
|
|
|
179
179
|
return { role, project: String(hit.project || ''), task: String(hit.task || ''), source: 'store', sessionId: sid };
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
|
-
// 旧角色表的 unknown
|
|
182
|
+
// 旧角色表的 unknown 是备用状态,按新口径并入普通。
|
|
183
183
|
if (role === 'unknown') {
|
|
184
184
|
return { role: 'plain', project: String(hit.project || ''), task: String(hit.task || ''), source: 'store', sessionId: sid };
|
|
185
185
|
}
|
package/src/lib/session-ops.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* 会话管理的实现(原 ho_session_new / ho_prompt / ho_session_archive / ho_session_unarchive 的代码搬到这里)。
|
|
3
3
|
* 对外只经 ho_sessions(action=…) 暴露,少 4 份 schema 的固定开销。
|
|
4
4
|
*/
|
|
@@ -7,12 +7,12 @@ import path from 'node:path';
|
|
|
7
7
|
import { NL, readUtf8, isDir, isFile } from './text.mjs';
|
|
8
8
|
import { handoverPathFor, assertProjectName, assertTaskName } from './role.mjs';
|
|
9
9
|
import { readHandover } from './handover-io.mjs';
|
|
10
|
-
import { scanSessions } from './sessions.mjs';
|
|
11
10
|
import { workspaceRows } from './registry.mjs';
|
|
12
11
|
import { checkConsent } from './consent.mjs';
|
|
13
12
|
import { watermark } from './facts.mjs';
|
|
14
13
|
import { sendPrompt } from './prompt.mjs';
|
|
15
14
|
import { newSessionIn } from './session-create.mjs';
|
|
15
|
+
import { lineageKeyOf, loadOffice, registerHolder } from './office.mjs';
|
|
16
16
|
|
|
17
17
|
const FENCE = String.fromCharCode(96, 96, 96);
|
|
18
18
|
|
|
@@ -35,6 +35,65 @@ function findWorkspaceId(key) {
|
|
|
35
35
|
return null;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* 新建会话落哪个工作区:用户指定的 workspace/cwd 优先,项目按标题或路径自动匹配,全局落到当前工作区。
|
|
40
|
+
* 返回 workspaceId;找不到时抛出可直接展示的中文错误,并列出已注册的工作区。
|
|
41
|
+
* @param {object} env 插件环境
|
|
42
|
+
* @param {object} exec 工具执行上下文(用来取当前工作区根)
|
|
43
|
+
* @param {string} kind 角色:global、project、misc
|
|
44
|
+
* @param {string} project 项目名
|
|
45
|
+
* @param {string} task 任务名
|
|
46
|
+
* @param {object} [opts] { workspace, cwd, wantGlobal }:前两项是用户明确指定的落点
|
|
47
|
+
* @returns {string} workspaceId
|
|
48
|
+
*/
|
|
49
|
+
export function resolveWorkspaceFor(env, exec, kind, project, task, opts) {
|
|
50
|
+
const o = opts || {};
|
|
51
|
+
const ws = env.resolveWs(exec);
|
|
52
|
+
const rows = listWorkspaces(env);
|
|
53
|
+
const choices = rows.length
|
|
54
|
+
? rows.map((r) => ' ' + r.id + ' ' + (r.title || '-') + ' ' + (r.path || '-')).join(NL)
|
|
55
|
+
: ' (还没有注册任何工作区)';
|
|
56
|
+
const pickWorkspace = (key) => {
|
|
57
|
+
const wid = findWorkspaceId(key);
|
|
58
|
+
if (!wid) {
|
|
59
|
+
throw new Error('找不到工作区:' + key + NL + '已注册的工作区:' + NL + choices + NL + '传 id、标题或路径都行;要先建工作区,用 ho_workspace(action="create", path="<目录>")。');
|
|
60
|
+
}
|
|
61
|
+
return wid;
|
|
62
|
+
};
|
|
63
|
+
const wantGlobalWs = o.wantGlobal === true || kind === 'global';
|
|
64
|
+
if (o.workspace) return pickWorkspace(o.workspace);
|
|
65
|
+
if (o.cwd) {
|
|
66
|
+
if (!isDir(o.cwd)) throw new Error('cwd 不存在:' + o.cwd);
|
|
67
|
+
// cwd 若正好是已注册工作区的根,必须按 workspaceId 建;否则会话会落进「未分组」。
|
|
68
|
+
let wid = null;
|
|
69
|
+
try { wid = findWorkspaceId(o.cwd); } catch { wid = null; }
|
|
70
|
+
if (!wid) {
|
|
71
|
+
throw new Error('cwd 不在任何已注册工作区里:' + o.cwd + NL + '已注册的工作区:' + NL + choices
|
|
72
|
+
+ NL + '要么把 cwd 换成某个工作区的根,要么先注册它:ho_workspace(action="create", path="' + o.cwd + '")。');
|
|
73
|
+
}
|
|
74
|
+
return wid;
|
|
75
|
+
}
|
|
76
|
+
if (project) {
|
|
77
|
+
// 项目会话:按项目名找同名工作区,或路径落在 Projects/<项目>、_Project_Meta/<项目> 下的工作区。
|
|
78
|
+
const want = String(project).toLowerCase();
|
|
79
|
+
const hit = rows.find((r) => {
|
|
80
|
+
if (String(r.title || '') === String(project)) return true;
|
|
81
|
+
const p = normPath(r.path);
|
|
82
|
+
return p.indexOf('/projects/' + want) !== -1 || p.indexOf('/_project_meta/' + want) !== -1;
|
|
83
|
+
});
|
|
84
|
+
if (!hit) {
|
|
85
|
+
throw new Error('项目「' + project + '」还没有对应的工作区。' + NL + '已注册的工作区:' + NL + choices
|
|
86
|
+
+ NL + '做法一:ho_workspace(action="create", path="<该项目目录>", title="' + project + '") 后再建会话;'
|
|
87
|
+
+ NL + '做法二:把 workspace 传成上面某个工作区。');
|
|
88
|
+
}
|
|
89
|
+
return String(hit.id);
|
|
90
|
+
}
|
|
91
|
+
if (wantGlobalWs) return pickWorkspace(ws);
|
|
92
|
+
throw new Error('新建会话必须指明工作区:传 workspace(id、标题或路径),或 cwd(某个已注册工作区的根)。' + NL
|
|
93
|
+
+ '已注册的工作区:' + NL + choices + NL
|
|
94
|
+
+ '全局会话落在标题含「全局」的工作区;项目会话同时传 project 与该项目的工作区;杂项会话传该项目外的独立工作区。');
|
|
95
|
+
}
|
|
96
|
+
|
|
38
97
|
/** 已注册工作区清单(插件服务优先,退化读 workspace.json):新建会话时用来列出可选项。 */
|
|
39
98
|
function listWorkspaces(env) {
|
|
40
99
|
try {
|
|
@@ -80,6 +139,24 @@ function fill(tpl, map) {
|
|
|
80
139
|
|
|
81
140
|
// ---------------------------------------------------------------- action: new
|
|
82
141
|
|
|
142
|
+
/**
|
|
143
|
+
* 复用判据:同一类别已有在任会话且其水位低于提醒线时,返回中文拒绝理由;否则返回空串。
|
|
144
|
+
* 只读在任登记表与水位缓存,不写任何文件;取不到水位就放行。
|
|
145
|
+
*/
|
|
146
|
+
function reuseBlocker(env, ws, kind, key) {
|
|
147
|
+
if (env.cfg.reuseCheck === false) return '';
|
|
148
|
+
if (!ws || !kind || !key) return '';
|
|
149
|
+
const lin = loadOffice(ws).lineages[key];
|
|
150
|
+
const cur = lin && lin.current;
|
|
151
|
+
if (!cur || !cur.sessionId) return '';
|
|
152
|
+
let pct = null;
|
|
153
|
+
try { pct = typeof env.selfPct === 'function' ? env.selfPct(cur.sessionId) : null; } catch { pct = null; }
|
|
154
|
+
if (pct === null || pct === undefined || !(Number(pct) < watermark(env.cfg).warnValue)) return '';
|
|
155
|
+
const label = kind === 'global' ? '全局类别' : kind === 'project' ? '项目「' + key + '」' : '杂项任务「' + key + '」';
|
|
156
|
+
return '拒绝新建:' + label + '已有在任会话 ' + (cur.name || '(未命名)') + '(' + cur.sessionId + ')水位 '
|
|
157
|
+
+ Number(pct).toFixed(1) + '%,低于 ' + watermark(env.cfg).warn + ';建议用 peer_send 复用,确需另建请传 force=true。';
|
|
158
|
+
}
|
|
159
|
+
|
|
83
160
|
export async function sessionNew(env, args, exec) {
|
|
84
161
|
const ws = env.resolveWs(exec);
|
|
85
162
|
const controller = env.controller();
|
|
@@ -91,91 +168,51 @@ export async function sessionNew(env, args, exec) {
|
|
|
91
168
|
if (args.task) assertTaskName(String(args.task));
|
|
92
169
|
const out = [];
|
|
93
170
|
const title = String(args.title || '');
|
|
94
|
-
|
|
95
|
-
const scan = scanSessions({
|
|
96
|
-
dir: env.cacheDir(),
|
|
97
|
-
meId: env.selfId(exec),
|
|
98
|
-
warn: watermark(env.cfg).warnValue,
|
|
99
|
-
silentHours: env.cfg.sessionScanHours,
|
|
100
|
-
});
|
|
101
|
-
const live = scan.rows.filter((r) => r.project === args.project && r.kind === '平级' && r.pct !== null);
|
|
102
|
-
if (live.length) {
|
|
103
|
-
const best = live.slice().sort((a, b) => b.mtime - a.mtime)[0];
|
|
104
|
-
if (best.pct < watermark(env.cfg).warnValue) {
|
|
105
|
-
throw new Error('拒绝新建:项目「' + args.project + '」在任会话 ' + best.sid + ' 水位 ' + best.pct.toFixed(1) + '%(<' + watermark(env.cfg).warn + '),按 Charter §7.2.1 应复用现有会话;确实要建请加 force=true');
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
// 角色先算好:新建会话按它登记并当场装配。
|
|
171
|
+
// 角色先算好:新建会话按它做在任判据、登记并当场装配。
|
|
110
172
|
let role = args.role || (args.project ? 'project' : args.kind || '');
|
|
111
173
|
if (role === 'session') role = '';
|
|
112
174
|
const project = args.project || '';
|
|
113
175
|
const task = args.task || '';
|
|
114
176
|
const finalRole = title.indexOf('全局') !== -1 ? 'global'
|
|
115
177
|
: (role === 'project' || role === 'misc' ? role : (project ? 'project' : ''));
|
|
178
|
+
const officeKind = finalRole === 'project' || finalRole === 'misc' || finalRole === 'global' ? finalRole : '';
|
|
116
179
|
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
: ' (还没有注册任何工作区)';
|
|
122
|
-
const pickWorkspace = (key) => {
|
|
123
|
-
const wid = findWorkspaceId(key);
|
|
124
|
-
if (!wid) {
|
|
125
|
-
throw new Error('找不到工作区:' + key + NL + '已注册的工作区:' + NL + choices + NL + '传 id、标题或路径都行;要先建工作区,用 ho_workspace(action="create", path="<目录>")。');
|
|
126
|
-
}
|
|
127
|
-
return wid;
|
|
128
|
-
};
|
|
129
|
-
const wantGlobalWs = title.indexOf('全局') !== -1 || args.role === 'global' || args.kind === 'global';
|
|
130
|
-
let workspaceId = '';
|
|
131
|
-
if (args.workspace) {
|
|
132
|
-
workspaceId = pickWorkspace(args.workspace);
|
|
133
|
-
} else if (args.cwd) {
|
|
134
|
-
if (!isDir(args.cwd)) throw new Error('cwd 不存在:' + args.cwd);
|
|
135
|
-
// cwd 若正好是已注册工作区的根,必须按 workspaceId 建;否则会话会落进「未分组」。
|
|
136
|
-
let wid = null;
|
|
137
|
-
try { wid = findWorkspaceId(args.cwd); } catch { wid = null; }
|
|
138
|
-
if (!wid) {
|
|
139
|
-
throw new Error('cwd 不在任何已注册工作区里:' + args.cwd + NL + '已注册的工作区:' + NL + choices
|
|
140
|
-
+ NL + '要么把 cwd 换成某个工作区的根,要么先注册它:ho_workspace(action="create", path="' + args.cwd + '")。');
|
|
141
|
-
}
|
|
142
|
-
workspaceId = wid;
|
|
143
|
-
} else if (args.project) {
|
|
144
|
-
// 项目会话:按项目名找同名工作区,或路径落在 Projects/<项目>、_Project_Meta/<项目> 下的工作区。
|
|
145
|
-
const want = String(args.project).toLowerCase();
|
|
146
|
-
const hit = rows.find((r) => {
|
|
147
|
-
if (String(r.title || '') === String(args.project)) return true;
|
|
148
|
-
const p = normPath(r.path);
|
|
149
|
-
return p.indexOf('/projects/' + want) !== -1 || p.indexOf('/_project_meta/' + want) !== -1;
|
|
150
|
-
});
|
|
151
|
-
if (!hit) {
|
|
152
|
-
throw new Error('项目「' + args.project + '」还没有对应的工作区。' + NL + '已注册的工作区:' + NL + choices
|
|
153
|
-
+ NL + '做法一:ho_workspace(action="create", path="<该项目目录>", title="' + args.project + '") 后再建会话;'
|
|
154
|
-
+ NL + '做法二:把 workspace 传成上面某个工作区。');
|
|
155
|
-
}
|
|
156
|
-
workspaceId = String(hit.id);
|
|
157
|
-
} else if (wantGlobalWs) {
|
|
158
|
-
workspaceId = pickWorkspace(ws);
|
|
159
|
-
} else {
|
|
160
|
-
throw new Error('新建会话必须指明工作区:传 workspace(id、标题或路径),或 cwd(某个已注册工作区的根)。' + NL
|
|
161
|
-
+ '已注册的工作区:' + NL + choices + NL
|
|
162
|
-
+ '全局会话落在标题含「全局」的工作区;项目会话同时传 project 与该项目的工作区;杂项会话传该项目外的独立工作区。');
|
|
180
|
+
// 建会话之前先读在任登记表:同类已有低水位在任会话时,复用比新建更省。校验在一切写入之前。
|
|
181
|
+
if (args.force !== true && officeKind) {
|
|
182
|
+
const block = reuseBlocker(env, ws, officeKind, lineageKeyOf(officeKind, project, task));
|
|
183
|
+
if (block) throw new Error(block);
|
|
163
184
|
}
|
|
164
185
|
|
|
186
|
+
// ---- 工作区解析:新建会话必须落到明确的工作区,不允许静默落进调用者所在的工作区。
|
|
187
|
+
const workspaceId = resolveWorkspaceFor(env, exec, finalRole, project, task, {
|
|
188
|
+
workspace: args.workspace,
|
|
189
|
+
cwd: args.cwd,
|
|
190
|
+
wantGlobal: title.indexOf('全局') !== -1 || args.role === 'global' || args.kind === 'global',
|
|
191
|
+
});
|
|
192
|
+
|
|
165
193
|
const made = await newSessionIn(env, { workspaceId, title, role: finalRole, project, task });
|
|
166
194
|
const sessionId = made.sessionId || '';
|
|
167
195
|
if (!sessionId) throw new Error(made.error || 'create 没有返回 sessionId');
|
|
168
196
|
out.push('已建会话 ' + sessionId + (finalRole ? ',角色 ' + finalRole + (project ? '(' + project + ')' : '') : ''));
|
|
197
|
+
// 建会话即登记在任:全局手工建/换会话与自服务换任共用一张登记表,不让注册表漏记或与实况分叉。
|
|
198
|
+
try {
|
|
199
|
+
if (officeKind) {
|
|
200
|
+
const reg = registerHolder(ws, { kind: officeKind, project, task, name: title, sessionId, workspace: ws });
|
|
201
|
+
out.push('在任登记:' + reg.key + ' → ' + title + (reg.previous ? '(原 ' + reg.previous.name + ' 进历史)' : ''));
|
|
202
|
+
}
|
|
203
|
+
} catch (e) {
|
|
204
|
+
out.push('在任登记失败:' + String(e && e.message ? e.message : e) + '(不影响会话本身)');
|
|
205
|
+
}
|
|
169
206
|
const meId = env.selfId(exec) || '(未知)';
|
|
170
207
|
// 交接路径按**接任者自身的角色**取用:项目换任读取项目交接,不再退回全局交接。
|
|
171
208
|
const hp = handoverPathFor(ws, finalRole === 'project' && project ? { role: 'project', project } : { role: 'global' });
|
|
172
209
|
const handover = env.cfg.handoverPath || (hp.path || '');
|
|
173
210
|
const text = args.message || [
|
|
174
|
-
'
|
|
175
|
-
'
|
|
211
|
+
'先读交接:' + handover + '。',
|
|
212
|
+
'读毕先向用户报告,不得自行开展作业:一、当前停在哪一步;二、交接列有哪些待办;三、建议先做哪一条。待用户指派后再动手。交接无待办时如实说明,不得编造任务。',
|
|
176
213
|
'本会话 id:' + sessionId,
|
|
177
|
-
(finalRole === 'project' ? '
|
|
178
|
-
'默认沉默:仅在三种时机各发一条 peer_send
|
|
214
|
+
(finalRole === 'project' ? '全局会话 id:' : '创建者会话 id:') + meId,
|
|
215
|
+
'默认沉默:仅在三种时机各发一条 peer_send 给全局:水位不低于 ' + watermark(env.cfg).warn + ' 且已写交接、需用户决定、全局口径有误。任务完成不报全局,任务属本会话内部事务。会话间可直接 peer_send,不必经全局转述;不得每轮回执,不得发送「收到」「已完成」一类确认,全局以文件末态为准。',
|
|
179
216
|
].join(NL);
|
|
180
217
|
const r = await sendPrompt(env.controller(), sessionId, text, 'queue');
|
|
181
218
|
out.push('已投启动提示词 accepted=' + String(r && r.accepted) + '(' + text.length + ' 字)');
|
|
@@ -184,8 +221,15 @@ export async function sessionNew(env, args, exec) {
|
|
|
184
221
|
|
|
185
222
|
// ---------------------------------------------------------------- action: prompt
|
|
186
223
|
|
|
187
|
-
|
|
188
|
-
|
|
224
|
+
/**
|
|
225
|
+
* 生成某角色会话的启动提示词:工作区没有模板就退回插件自带那份(dist/templates/Prompt_Templates.md)。
|
|
226
|
+
* @param {object} env 插件环境
|
|
227
|
+
* @param {object} opts { ws, kind, project, task, title, sessionId, globalSessionId, extra }
|
|
228
|
+
* @returns {string} 可直接投递的提示词正文
|
|
229
|
+
*/
|
|
230
|
+
export function promptFor(env, opts) {
|
|
231
|
+
const o = opts || {};
|
|
232
|
+
const ws = String(o.ws || env.resolveWs());
|
|
189
233
|
const file = path.join(ws, '_Control_Center', 'templates', 'Prompt_Templates.md');
|
|
190
234
|
// 工作区没有模板就退回插件自带那份(dist/templates/Prompt_Templates.md),功能不打折。
|
|
191
235
|
let tpl = '';
|
|
@@ -198,13 +242,13 @@ export function sessionPrompt(env, args, exec) {
|
|
|
198
242
|
fromBundle = true;
|
|
199
243
|
}
|
|
200
244
|
if (fromBundle) {
|
|
201
|
-
tpl = '>
|
|
245
|
+
tpl = '> (插件内置模板;本地覆盖写入 ' + file + ')' + NL + NL + tpl;
|
|
202
246
|
}
|
|
203
247
|
const skeleton = blockAfter(tpl, '1 共用骨架');
|
|
204
248
|
if (!skeleton) throw new Error('模板缺 §1 共用骨架,拒绝渲染:' + file);
|
|
205
|
-
const kind =
|
|
206
|
-
const project =
|
|
207
|
-
const task =
|
|
249
|
+
const kind = o.kind || 'project';
|
|
250
|
+
const project = o.project || '';
|
|
251
|
+
const task = o.task || '';
|
|
208
252
|
const target = handoverPathFor(ws, kind === 'project'
|
|
209
253
|
? { role: 'project', project }
|
|
210
254
|
: kind === 'misc' && task ? { role: 'misc', task } : { role: 'global' });
|
|
@@ -214,7 +258,7 @@ export function sessionPrompt(env, args, exec) {
|
|
|
214
258
|
: kind === 'misc' ? (target.path || path.join(ws, '_Control_Center', 'rules', 'Misc_Task_Standard.md'))
|
|
215
259
|
: path.join(ws, '_Control_Center', 'rules', 'Migration_Rules.md');
|
|
216
260
|
if (!firstPath) throw new Error('kind=' + kind + ' 需要 project、task 参数才能算交接路径');
|
|
217
|
-
const numMatch = String(
|
|
261
|
+
const numMatch = String(o.title || '').match(/([0-9]+)/);
|
|
218
262
|
const ordinal = numMatch ? numMatch[1] : 'N';
|
|
219
263
|
const roleText = kind === 'global'
|
|
220
264
|
? '第 ' + ordinal + ' 任全局(管理会话)'
|
|
@@ -227,8 +271,8 @@ export function sessionPrompt(env, args, exec) {
|
|
|
227
271
|
const filled = fill(skeleton, {
|
|
228
272
|
'<角色>': roleText,
|
|
229
273
|
'<绝对路径>': firstPath,
|
|
230
|
-
'<本会话 id>':
|
|
231
|
-
'<全局 id>':
|
|
274
|
+
'<本会话 id>': o.sessionId || '(建完后填)',
|
|
275
|
+
'<全局 id>': o.globalSessionId || '(现任全局 id)',
|
|
232
276
|
'<范围>': scopeText,
|
|
233
277
|
});
|
|
234
278
|
const stripHint = (l) => {
|
|
@@ -242,16 +286,30 @@ export function sessionPrompt(env, args, exec) {
|
|
|
242
286
|
const body = filled.split(NL).map(stripHint).join(NL);
|
|
243
287
|
const extraLines = [];
|
|
244
288
|
if (kind === 'project' && project) {
|
|
245
|
-
extraLines.push('库根:Projects/' + project + '
|
|
246
|
-
extraLines.push('
|
|
247
|
-
extraLines.push('
|
|
289
|
+
extraLines.push('库根:Projects/' + project + '/;元数据:_Project_Meta/' + project + '/;快照:_Temp/' + project + '/Snapshots/');
|
|
290
|
+
extraLines.push('作业前读库根 SKILL.md;修改文档时另读 _Control_Center/rules/ 对应规范。');
|
|
291
|
+
extraLines.push('进入预警区后写交接;请用户在聊天框发送 /allow-succeed 授权,再由本会话调用 ho_succeed 完成换任,不向全局请求。');
|
|
248
292
|
}
|
|
249
|
-
if (kind === 'misc') extraLines.push('标准见 rules/Misc_Task_Standard.md
|
|
250
|
-
if (kind === 'migration') extraLines.push('迁移口径见 rules/Migration_Rules.md
|
|
251
|
-
if (
|
|
293
|
+
if (kind === 'misc') extraLines.push('标准见 rules/Misc_Task_Standard.md;换任须用户授权,再由本会话调用 ho_succeed。');
|
|
294
|
+
if (kind === 'migration') extraLines.push('迁移口径见 rules/Migration_Rules.md;通过顶层闸门后再开始作业。');
|
|
295
|
+
if (o.extra) extraLines.push(String(o.extra));
|
|
252
296
|
return [body].concat(extraLines.length ? ['', ...extraLines] : []).join(NL);
|
|
253
297
|
}
|
|
254
298
|
|
|
299
|
+
/** ho_sessions(action="prompt") 的实现:按调用者角色的工作区生成提示词。 */
|
|
300
|
+
export function sessionPrompt(env, args, exec) {
|
|
301
|
+
return promptFor(env, {
|
|
302
|
+
ws: env.resolveWs(exec),
|
|
303
|
+
kind: args.kind,
|
|
304
|
+
project: args.project,
|
|
305
|
+
task: args.task,
|
|
306
|
+
title: args.title,
|
|
307
|
+
sessionId: args.sessionId,
|
|
308
|
+
globalSessionId: args.globalSessionId,
|
|
309
|
+
extra: args.extra,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
255
313
|
// ---------------------------------------------------------------- action: archive / unarchive
|
|
256
314
|
|
|
257
315
|
export async function sessionArchive(env, args, exec) {
|
|
@@ -303,11 +361,11 @@ function orderHandoverText(env, sid, extra) {
|
|
|
303
361
|
lines.push("【全局指令:执行交接流程】将当前工作写成交接,写毕即停止操作,等待换任。本轮不再进行项目作业。");
|
|
304
362
|
lines.push("");
|
|
305
363
|
lines.push("1) 目标文件:" + (hp && hp.path ? hp.path : "(角色未定,须先由全局设定角色)"));
|
|
306
|
-
lines.push("
|
|
307
|
-
lines.push("2) 必写:①
|
|
308
|
-
lines.push("3)
|
|
309
|
-
lines.push("4) 写完只回一条 peer_send 给全局:`交接已写
|
|
310
|
-
lines.push("5)
|
|
364
|
+
lines.push(" 直接调用 ho_handover(action=\"write\", text=...),不要传 path:路径已按你的角色(" + (info && info.role ? info.role : "unknown") + ")解析。");
|
|
365
|
+
lines.push("2) 必写:① 当前进度(版本、产物、是否通过)② 环境与路径(版本号、安装位置、下载源)③ 下一步清单,按顺序列出,每条可直接执行 ④ 已完成待验收、要求未实现的部分 ⑤ 已知问题与已踩的错。");
|
|
366
|
+
lines.push("3) 机密只写位置,不写值:密码、token、密钥均写明所在文件。");
|
|
367
|
+
lines.push("4) 写完只回一条 peer_send 给全局:`交接已写 <绝对路径>`,附一句状态,不复述全文。");
|
|
368
|
+
lines.push("5) 不要压缩上下文,不要新建会话(新建会话属全局权限)。");
|
|
311
369
|
if (extra) lines.push("6) 全局附加:" + extra);
|
|
312
370
|
return lines.join(NL);
|
|
313
371
|
}
|
|
@@ -355,7 +413,7 @@ export async function sessionOrder(env, args) {
|
|
|
355
413
|
const max = (env.cfg && env.cfg.maxCompactions) || 3;
|
|
356
414
|
const n = used + 1;
|
|
357
415
|
if (n > max) {
|
|
358
|
-
out.push("已压满 " + used + "/" + max + ":不再压缩。应执行 ho_sessions(action=\"new\")
|
|
416
|
+
out.push("已压满 " + used + "/" + max + ":不再压缩。应执行 ho_succeed 自行换任(先请用户发送 /allow-succeed 授权);全局会话自身也可用 ho_sessions(action=\"new\")。新会话读毕交接会先向你报告,等待指派。");
|
|
359
417
|
return out.join(NL);
|
|
360
418
|
}
|
|
361
419
|
const recentSec = st && st.lastSummaryMs ? (Date.now() - st.lastSummaryMs) / 1000 : null;
|