@mingmaple/dsh-handover 0.0.0-stage → 0.9.6
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/LICENSE +21 -0
- package/README.md +118 -2
- package/docs/migrate.md +61 -0
- package/docs/troubleshoot.md +48 -0
- package/package.json +55 -3
- package/scripts/blank-dsh.mjs +203 -0
- package/scripts/blank-smoke.mjs +820 -0
- package/scripts/build.mjs +58 -0
- package/scripts/check-docs.mjs +74 -0
- package/scripts/e2e-migration.mjs +127 -0
- package/scripts/lib/register.mjs +103 -0
- package/src/client.js +887 -0
- package/src/cordis.patch.yml +16 -0
- package/src/index.mjs +631 -0
- package/src/lib/absrefs.mjs +112 -0
- package/src/lib/api.mjs +234 -0
- package/src/lib/archive.mjs +198 -0
- package/src/lib/brief.mjs +74 -0
- package/src/lib/charter.mjs +28 -0
- package/src/lib/config.mjs +36 -0
- package/src/lib/consent.mjs +84 -0
- package/src/lib/drift.mjs +65 -0
- package/src/lib/events.mjs +89 -0
- package/src/lib/extra-prompt.mjs +41 -0
- package/src/lib/facts.mjs +70 -0
- package/src/lib/gates.mjs +148 -0
- package/src/lib/guide.mjs +99 -0
- package/src/lib/handover-io.mjs +49 -0
- package/src/lib/lint.mjs +152 -0
- package/src/lib/manual.mjs +124 -0
- package/src/lib/mover.mjs +663 -0
- package/src/lib/names.mjs +33 -0
- package/src/lib/output.mjs +11 -0
- package/src/lib/pack-ops.mjs +329 -0
- package/src/lib/peer.mjs +260 -0
- package/src/lib/phase.mjs +34 -0
- package/src/lib/profile.mjs +204 -0
- package/src/lib/registry.mjs +53 -0
- package/src/lib/retarget-helper.tmpl +46 -0
- package/src/lib/role.mjs +217 -0
- package/src/lib/rpc-carrier.mjs +65 -0
- package/src/lib/scaffold.mjs +118 -0
- package/src/lib/schedule.mjs +227 -0
- package/src/lib/session-ops.mjs +362 -0
- package/src/lib/sessions.mjs +142 -0
- package/src/lib/sevenzip.mjs +48 -0
- package/src/lib/status.mjs +171 -0
- package/src/lib/text.mjs +74 -0
- package/src/lib/webroute.mjs +130 -0
- package/src/lib/workspace.mjs +23 -0
- package/src/lib/zstd-log.mjs +65 -0
- package/src/selftest.mjs +84 -0
- package/src/templates/Changelog.skeleton.md +10 -0
- package/src/templates/Charter.skeleton.md +53 -0
- package/src/templates/Handover.global.skeleton.md +56 -0
- package/src/templates/Prompt_Templates.md +96 -0
- package/src/templates/Runtime_Manual.md +166 -0
- package/src/templates/rules/Handover_Rules.md +43 -0
- package/src/templates/rules/Migration_Rules.md +32 -0
- package/src/templates/rules/Misc_Task_Standard.md +23 -0
- package/src/templates/rules/Writing_Rules.md +24 -0
- package/src/tools/ho_brief.mjs +57 -0
- package/src/tools/ho_changelog.mjs +59 -0
- package/src/tools/ho_charter.mjs +89 -0
- package/src/tools/ho_checkpoint.mjs +125 -0
- package/src/tools/ho_diag.mjs +85 -0
- package/src/tools/ho_drift.mjs +41 -0
- package/src/tools/ho_gate.mjs +69 -0
- package/src/tools/ho_handover.mjs +83 -0
- package/src/tools/ho_init.mjs +82 -0
- package/src/tools/ho_pack.mjs +38 -0
- package/src/tools/ho_phase.mjs +37 -0
- package/src/tools/ho_profile.mjs +97 -0
- package/src/tools/ho_sessions.mjs +114 -0
- package/src/tools/ho_snapshot.mjs +103 -0
- package/src/tools/ho_tokens.mjs +57 -0
- package/src/tools/ho_workspace.mjs +213 -0
- package/src/tools/peer_list.mjs +37 -0
- package/src/tools/peer_new.mjs +75 -0
- package/src/tools/peer_peek.mjs +41 -0
- package/src/tools/peer_rename.mjs +31 -0
- package/src/tools/peer_send.mjs +52 -0
- package/src/tools/peer_status.mjs +46 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** dsh-handover 配置:全部可被 profile patch 的 config 覆盖。 */
|
|
2
|
+
export const DEFAULTS = {
|
|
3
|
+
workspaceRoot: '', // 留空则从会话 cwd 推导(向上找 _Control_Center 目录)
|
|
4
|
+
warnRatio: 0.32,
|
|
5
|
+
handoverRatio: 0.35,
|
|
6
|
+
hardRatio: 0.85,
|
|
7
|
+
maxCompactions: 3,
|
|
8
|
+
autoResume: true,
|
|
9
|
+
sessionScanHours: 12,
|
|
10
|
+
libraries: [],
|
|
11
|
+
handoverPath: '', // 留空 = 按会话角色解析(全局 records/Global_Handover.md;项目 _Project_Meta/<项目>/Handover.md;杂项 _Misc/tasks/<任务>/Handover.md)。非空则强制该路径(演练用)
|
|
12
|
+
rolesPath: '', // 留空 = ~/.dsh/storages/dsh-handover.roles.json
|
|
13
|
+
// 续跑提示:{n} {max} {handover} {role} 会被替换成真值;{handover} 是本会话自己的交接绝对路径。
|
|
14
|
+
resumePrompt: '【dsh-handover 自动续跑·第 {n}/{max} 次压缩】压缩已完成,历史已被摘要替换。第一步:ho_handover(action=read) 取回本会话的交接({handover});然后按 §2「下一步」继续。不要复述交接内容,不要重做已完成的事。完整流程:ho_charter(doc="runtime")。',
|
|
15
|
+
resumePromptUnknown: '【dsh-handover 自动续跑·第 {n}/{max} 次压缩】压缩已完成,历史已被摘要替换。本会话是普通会话,不参与交接;如需继续,请由全局会话为其指定角色。',
|
|
16
|
+
compactTimeoutMs: 120000,
|
|
17
|
+
compactAttempts: 3,
|
|
18
|
+
waitIdleMs: 180000, // 等会话空闲的上限(超过就放弃并回告)
|
|
19
|
+
driftSlack: 3, // Δ交接容忍轮数(超过即需刷交接)
|
|
20
|
+
wakeAuthority: 'on', // 唤醒权硬拦:on / off
|
|
21
|
+
roleGating: 'on', // 按角色裁剪工具与提示段:on / off(off=所有会话都看得见全部工具)
|
|
22
|
+
reuseCheck: true, // 建新会话前查在任会话水位(低于 warnRatio 拒绝)
|
|
23
|
+
prepackMinAgeMin: 90, // 打包前要求会话静止分钟数
|
|
24
|
+
packFormat: 'auto', // 归档格式:auto(有 7-Zip 用 7z,否则内置 tar.gz)、7z、tar
|
|
25
|
+
sevenZipPath: '', // 留空则自动探测 C:\Program Files\7-Zip\7z.exe
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function resolveConfig(raw) {
|
|
29
|
+
const out = { ...DEFAULTS };
|
|
30
|
+
if (raw && typeof raw === "object") {
|
|
31
|
+
for (const k of Object.keys(DEFAULTS)) {
|
|
32
|
+
if (raw[k] !== undefined && raw[k] !== null) out[k] = raw[k];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 压缩授权:压缩会丢历史,所以「同意」必须由用户自己产生,AI 不能自己给自己批。
|
|
3
|
+
*
|
|
4
|
+
* 做法:唯一的授权入口是设置页上的一次点击(用户在场、只能是用户按),
|
|
5
|
+
* 授权一次有效 5 分钟、只对指定会话生效;`ho_checkpoint`、全局代排与调度器三处都校验它。
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
export const CONSENT_TTL_MS = 5 * 60 * 1000;
|
|
12
|
+
|
|
13
|
+
function consentPath() {
|
|
14
|
+
const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
|
|
15
|
+
return path.join(home, '.dsh', 'storages', 'dsh-handover.consent.json');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readAll() {
|
|
19
|
+
try {
|
|
20
|
+
const t = fs.readFileSync(consentPath(), 'utf8').replace(/^\uFEFF/, '');
|
|
21
|
+
const v = JSON.parse(t);
|
|
22
|
+
return v && typeof v === 'object' ? v : {};
|
|
23
|
+
} catch { return {}; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function writeAll(v) {
|
|
27
|
+
const p = consentPath();
|
|
28
|
+
try { fs.mkdirSync(path.dirname(p), { recursive: true }); } catch { /* ignore */ }
|
|
29
|
+
const tmp = p + '.tmp';
|
|
30
|
+
fs.writeFileSync(tmp, JSON.stringify(v, null, 2), 'utf8');
|
|
31
|
+
fs.renameSync(tmp, p);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function stampNow() {
|
|
35
|
+
const d = new Date(Date.now() + 8 * 3600 * 1000);
|
|
36
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
37
|
+
return d.getUTCFullYear() + '-' + p(d.getUTCMonth() + 1) + '-' + p(d.getUTCDate()) + ' ' + p(d.getUTCHours()) + ':' + p(d.getUTCMinutes()) + ' +0800';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 用户点了一次「允许该会话压缩一次」。 */
|
|
41
|
+
export function grantConsent(sessionId, note) {
|
|
42
|
+
const sid = String(sessionId || '');
|
|
43
|
+
if (!sid) throw new Error('缺少会话 id');
|
|
44
|
+
const all = readAll();
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
all[sid] = { at: now, until: now + CONSENT_TTL_MS, atText: stampNow(), note: String(note || '') };
|
|
47
|
+
writeAll(all);
|
|
48
|
+
return { sessionId: sid, until: new Date(all[sid].until).toISOString(), minutes: CONSENT_TTL_MS / 60000, atText: all[sid].atText };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 授权是否有效(每次读取时一并清除已过期者)。 */
|
|
52
|
+
export function checkConsent(sessionId) {
|
|
53
|
+
const sid = String(sessionId || '');
|
|
54
|
+
if (!sid) return { ok: false, reason: '没有会话 id' };
|
|
55
|
+
const all = readAll();
|
|
56
|
+
const hit = all[sid];
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
let dirty = false;
|
|
59
|
+
for (const k of Object.keys(all)) {
|
|
60
|
+
if (!all[k] || !all[k].until || all[k].until <= now) { delete all[k]; dirty = true; }
|
|
61
|
+
}
|
|
62
|
+
if (dirty) { try { writeAll(all); } catch { /* ignore */ } }
|
|
63
|
+
if (!hit || !hit.until || hit.until <= now) {
|
|
64
|
+
return { ok: false, reason: '用户还没有在设置页点「允许压缩」' };
|
|
65
|
+
}
|
|
66
|
+
return { ok: true, until: hit.until, leftMs: hit.until - now, atText: hit.atText || '' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function consumedConsent(sessionId) {
|
|
70
|
+
const all = readAll();
|
|
71
|
+
const sid = String(sessionId || '');
|
|
72
|
+
if (all[sid]) { delete all[sid]; try { writeAll(all); } catch { /* ignore */ } }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function listConsent() {
|
|
76
|
+
const all = readAll();
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
return Object.keys(all).map((sid) => ({
|
|
79
|
+
sessionId: sid,
|
|
80
|
+
leftMs: Math.max(0, (all[sid].until || 0) - now),
|
|
81
|
+
atText: all[sid].atText || '',
|
|
82
|
+
note: all[sid].note || '',
|
|
83
|
+
})).filter((r) => r.leftMs > 0);
|
|
84
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** 交接漂移(替代 handover_check.mjs):Δ = 会话当前轮 − 交接写入轮;> slack 即需刷交接。 */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { readJsonDir, pick } from './text.mjs';
|
|
5
|
+
import { projectOf, projCacheDir, isSubagent } from './sessions.mjs';
|
|
6
|
+
import { findLog, decodeLog, turnStarts } from './zstd-log.mjs';
|
|
7
|
+
|
|
8
|
+
export function computeDrift(opts) {
|
|
9
|
+
const ws = opts.ws;
|
|
10
|
+
const slack = opts.slack === undefined ? 3 : opts.slack;
|
|
11
|
+
const all = opts.all === true;
|
|
12
|
+
const cacheDir = opts.cacheDir || projCacheDir();
|
|
13
|
+
const rows = [];
|
|
14
|
+
const seen = new Map();
|
|
15
|
+
for (const f of readJsonDir(cacheDir)) {
|
|
16
|
+
const proj = projectOf(f.data).project;
|
|
17
|
+
if (!proj || proj === '-' || proj.indexOf('<') !== -1 || proj.indexOf('>') !== -1) continue;
|
|
18
|
+
if (!fs.existsSync(path.join(ws, 'Projects', proj))) continue;
|
|
19
|
+
const sid = f.name.replace('.json', '');
|
|
20
|
+
const turns = Number(pick(f.data, 'record', 'rows', 'sessionStats', 'val', 'turns') || 0);
|
|
21
|
+
rows.push({ sid, project: proj, turns, mtime: f.mtimeMs, kind: isSubagent(f.data) ? '子会话' : '平级' });
|
|
22
|
+
}
|
|
23
|
+
let picked;
|
|
24
|
+
if (all) picked = rows;
|
|
25
|
+
else {
|
|
26
|
+
for (const r of rows) {
|
|
27
|
+
const cur = seen.get(r.project);
|
|
28
|
+
if (!cur || r.mtime > cur.mtime) seen.set(r.project, r);
|
|
29
|
+
}
|
|
30
|
+
picked = Array.from(seen.values());
|
|
31
|
+
}
|
|
32
|
+
const out = [];
|
|
33
|
+
let fresh = 0, need = 0, unknown = 0;
|
|
34
|
+
for (const s of picked.slice().sort((a, b) => b.mtime - a.mtime)) {
|
|
35
|
+
const hand = path.join(ws, '_Project_Meta', s.project, 'Handover.md');
|
|
36
|
+
let hm = 0;
|
|
37
|
+
try { hm = fs.statSync(hand).mtimeMs; } catch { /* 没有交接 */ }
|
|
38
|
+
const log = findLog(s.sid, opts.logsRoot);
|
|
39
|
+
const recs = log ? decodeLog(log) : null;
|
|
40
|
+
let handoverTurn = null;
|
|
41
|
+
if (recs) {
|
|
42
|
+
for (const t of turnStarts(recs)) if (t.time <= hm) handoverTurn = t.turn;
|
|
43
|
+
if (handoverTurn === null) handoverTurn = 0;
|
|
44
|
+
}
|
|
45
|
+
let verdict = '', delta = null;
|
|
46
|
+
if (recs) {
|
|
47
|
+
delta = s.turns - (handoverTurn || 0);
|
|
48
|
+
verdict = delta <= slack ? '已最新' : '需刷交接';
|
|
49
|
+
} else {
|
|
50
|
+
delta = null;
|
|
51
|
+
verdict = hm && s.mtime <= hm ? '已最新(仅时间戳)' : '需人工确认';
|
|
52
|
+
}
|
|
53
|
+
if (verdict === '已最新' || verdict === '已最新(仅时间戳)') fresh++;
|
|
54
|
+
else if (verdict === '需刷交接') need++;
|
|
55
|
+
else unknown++;
|
|
56
|
+
out.push({ project: s.project, kind: s.kind, sid: s.sid, turns: s.turns, handoverTurn, delta, verdict });
|
|
57
|
+
}
|
|
58
|
+
return { rows: out, fresh, need, unknown, slack };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function driftVerdict(res) {
|
|
62
|
+
return '判定:' + res.fresh + ' 个已最新、' + res.need + ' 个需刷交接' +
|
|
63
|
+
(res.unknown ? '、' + res.unknown + ' 个需人工确认' : '') +
|
|
64
|
+
(res.need || res.unknown ? ';先让需刷的会话写交接,再搬' : ';可以直接搬');
|
|
65
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** 会话事件派生:已压缩次数与压缩锁状态。主数据源=会话日志(eventsOf 在本运行时常常为空),事件数组只作兜底。 */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { findLog, decodeLog } from './zstd-log.mjs';
|
|
5
|
+
import { projCacheDir } from './sessions.mjs';
|
|
6
|
+
|
|
7
|
+
/** 取本会话事件数组;无法获取返回 []。 */
|
|
8
|
+
export function eventsOf(exec) {
|
|
9
|
+
try {
|
|
10
|
+
const s = exec && exec.agent && exec.agent.session;
|
|
11
|
+
if (s && Array.isArray(s.events)) return s.events;
|
|
12
|
+
} catch { /* ignore */ }
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 返回 { used, lock }:
|
|
18
|
+
* used = compaction/summary 次数(= 已压缩次数)
|
|
19
|
+
* lock = 'clean' | 'open'(有未闭合的 compaction/start,且晚于最近一次 session/end-seed)
|
|
20
|
+
*/
|
|
21
|
+
export function countCompactions(events) {
|
|
22
|
+
let used = 0;
|
|
23
|
+
let pendingStart = null;
|
|
24
|
+
let lastEndSeed = -1;
|
|
25
|
+
for (let i = 0; i < events.length; i++) {
|
|
26
|
+
const e = events[i];
|
|
27
|
+
const t = e && e.type;
|
|
28
|
+
if (t === 'session/end-seed') lastEndSeed = i;
|
|
29
|
+
else if (t === 'compaction/start') pendingStart = i;
|
|
30
|
+
else if (t === 'compaction/summary') used++;
|
|
31
|
+
else if (t === 'compaction/end') pendingStart = null;
|
|
32
|
+
}
|
|
33
|
+
const open = pendingStart !== null && pendingStart > lastEndSeed;
|
|
34
|
+
return { used, lock: open ? 'open' : 'clean' };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 按 (sid, 日志 size+mtime) 缓存,避免每轮解几 MB 日志。
|
|
38
|
+
const cache = new Map();
|
|
39
|
+
|
|
40
|
+
/** 从会话日志读压缩状态:{ used, lock, lastSummaryMs, source }。读不到日志时退回事件数组。 */
|
|
41
|
+
export function compactionStateOf(sessionId, exec) {
|
|
42
|
+
const fromEvents = () => {
|
|
43
|
+
const c = countCompactions(eventsOf(exec));
|
|
44
|
+
return { used: c.used, lock: c.lock, lastSummaryMs: null, source: 'events' };
|
|
45
|
+
};
|
|
46
|
+
const sid = sessionId ? String(sessionId) : '';
|
|
47
|
+
if (!sid) return fromEvents();
|
|
48
|
+
const file = findLog(sid);
|
|
49
|
+
if (!file) return fromEvents();
|
|
50
|
+
let st = null;
|
|
51
|
+
try { st = fs.statSync(file); } catch { return fromEvents(); }
|
|
52
|
+
const hit = cache.get(sid);
|
|
53
|
+
if (hit && hit.size === st.size && hit.mtimeMs === st.mtimeMs) return hit.value;
|
|
54
|
+
let recs = null;
|
|
55
|
+
try { recs = decodeLog(file); } catch { recs = null; }
|
|
56
|
+
if (!recs || !recs.length) return fromEvents();
|
|
57
|
+
const c = countCompactions(recs);
|
|
58
|
+
let last = null;
|
|
59
|
+
for (const r of recs) {
|
|
60
|
+
if (r && r.type === 'compaction/summary' && typeof r.time === 'number') last = last === null ? r.time : Math.max(last, r.time);
|
|
61
|
+
}
|
|
62
|
+
const value = { used: c.used, lock: c.lock, lastSummaryMs: last, source: 'log' };
|
|
63
|
+
cache.set(sid, { size: st.size, mtimeMs: st.mtimeMs, value });
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 投影缓存里该会话文件的 mtimeMs(判断「压缩刚完成、缓存未刷新」用)。 */
|
|
68
|
+
function projCacheMtimeOf(sessionId) {
|
|
69
|
+
const sid = sessionId ? String(sessionId) : '';
|
|
70
|
+
if (!sid) return null;
|
|
71
|
+
try {
|
|
72
|
+
const dir = projCacheDir();
|
|
73
|
+
for (const name of fs.readdirSync(dir)) {
|
|
74
|
+
if (!name.endsWith('.json')) continue;
|
|
75
|
+
if (name.replace('.json', '') !== sid) continue;
|
|
76
|
+
return fs.statSync(path.join(dir, name)).mtimeMs;
|
|
77
|
+
}
|
|
78
|
+
} catch { /* ignore */ }
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 压缩后读数滞后提示:日志里已有 summary,但投影缓存比它旧 ⇒ 返回一句中文提示,否则空串。 */
|
|
83
|
+
export function stalePressureNote(sessionId) {
|
|
84
|
+
const st = compactionStateOf(sessionId, null);
|
|
85
|
+
if (!st.lastSummaryMs) return '';
|
|
86
|
+
const m = projCacheMtimeOf(sessionId);
|
|
87
|
+
if (m === null || m >= st.lastSummaryMs) return '';
|
|
88
|
+
return '(压缩刚完成,投影缓存尚未刷新,水位读数滞后一轮;下一次请求后自动正常)';
|
|
89
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 用户全局前置提示词:存在本机 ~/.dsh/storages/ 下。
|
|
3
|
+
* 与插件自身的会话约束无关:仅当用户显式开启且内容非空时注入全部会话;
|
|
4
|
+
* 关闭或留空时不注册提示段,不占用注入点位,避免与其他提示词插件冲突。
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
/** 与官方 SECTION_ORDERS 对齐:FILE_REFERENCE=900,工具段从 1000 起。 */
|
|
11
|
+
export const EXTRA_PROMPT_ORDER = 950;
|
|
12
|
+
export const EXTRA_PROMPT_SECTION = 'dsh-handover:extra-prompt';
|
|
13
|
+
const EXTRA_PROMPT_MAX = 20000;
|
|
14
|
+
|
|
15
|
+
function storePath() {
|
|
16
|
+
return path.join(os.homedir(), '.dsh', 'storages', 'dsh-handover.extra-prompt.json');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 返回 { enabled, text }。旧文件只有 text 时,按「内容非空即开启」迁移。 */
|
|
20
|
+
export function readExtraPrompt() {
|
|
21
|
+
try {
|
|
22
|
+
const j = JSON.parse(fs.readFileSync(storePath(), 'utf8'));
|
|
23
|
+
const text = typeof j.text === 'string' ? j.text : '';
|
|
24
|
+
const enabled = typeof j.enabled === 'boolean' ? j.enabled : text.trim() !== '';
|
|
25
|
+
return { enabled, text };
|
|
26
|
+
} catch {
|
|
27
|
+
return { enabled: false, text: '' };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 保存并返回 { enabled, text };空内容一律视为关闭。 */
|
|
32
|
+
export function writeExtraPrompt(text, enabled) {
|
|
33
|
+
const clean = String(text == null ? '' : text).trim();
|
|
34
|
+
if (clean.length > EXTRA_PROMPT_MAX) throw new Error('全局前置提示词过长(上限 ' + EXTRA_PROMPT_MAX + ' 字符)');
|
|
35
|
+
const on = enabled !== false && clean.length > 0;
|
|
36
|
+
const p = storePath();
|
|
37
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
38
|
+
const body = { enabled: on, text: clean, updatedAt: new Date().toISOString() };
|
|
39
|
+
fs.writeFileSync(p, JSON.stringify(body, null, 2) + '\n', 'utf8');
|
|
40
|
+
return body;
|
|
41
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 口径单一来源:水位阈值、压缩次数与提示段 token 预算。
|
|
3
|
+
* 运行期文本一律由本模块渲染;改数值只改 config.mjs 的默认值或本文件的预算表。
|
|
4
|
+
* 文档与测试引用同一组常量,避免同一事实多处各写一遍。
|
|
5
|
+
*/
|
|
6
|
+
import { DEFAULTS } from './config.mjs';
|
|
7
|
+
|
|
8
|
+
/** 比例转百分比文本:0.32 → "32%"。 */
|
|
9
|
+
function pctOf(ratio) {
|
|
10
|
+
const n = Math.round(Number(ratio) * 1000) / 10;
|
|
11
|
+
return (Number.isInteger(n) ? String(n) : n.toFixed(1)) + '%';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** 水位与压缩口径:全部由配置推导。 */
|
|
15
|
+
export function watermark(cfg) {
|
|
16
|
+
const c = cfg || DEFAULTS;
|
|
17
|
+
const warn = pctOf(c.warnRatio !== undefined ? c.warnRatio : DEFAULTS.warnRatio);
|
|
18
|
+
const handover = pctOf(c.handoverRatio !== undefined ? c.handoverRatio : DEFAULTS.handoverRatio);
|
|
19
|
+
const hard = pctOf(c.hardRatio !== undefined ? c.hardRatio : DEFAULTS.hardRatio);
|
|
20
|
+
const max = c.maxCompactions !== undefined ? c.maxCompactions : DEFAULTS.maxCompactions;
|
|
21
|
+
return { warn, handover, hard, max, warnValue: Math.round((c.warnRatio !== undefined ? c.warnRatio : DEFAULTS.warnRatio) * 100) };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 一句式交接规则(全局与项目提示段共用)。 */
|
|
25
|
+
export function handoverRuleText(cfg) {
|
|
26
|
+
const w = watermark(cfg);
|
|
27
|
+
return '水位 ' + w.warn + ' 提醒,' + w.handover + ' 交接线,' + w.hard + ' 硬上限;至多压缩 '
|
|
28
|
+
+ w.max + ' 次,第 ' + (w.max + 1) + ' 次换任。';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 设置页使用说明里的紧写形式。 */
|
|
32
|
+
export function compactSummaryText(cfg) {
|
|
33
|
+
const w = watermark(cfg);
|
|
34
|
+
return w.warn + ' 提醒、' + w.handover + ' 交接线、' + w.hard + ' 硬上限;同一会话最多压缩 ' + w.max + ' 次';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 提示段与工具描述的加权 token 估算:CJK 与全角标点每字符 1 token,其余每 4 字符 1 token。 */
|
|
38
|
+
export function estimateTokens(text) {
|
|
39
|
+
const s = String(text === null || text === undefined ? '' : text);
|
|
40
|
+
let wide = 0;
|
|
41
|
+
let narrow = 0;
|
|
42
|
+
for (const ch of s) {
|
|
43
|
+
const cp = ch.codePointAt(0);
|
|
44
|
+
if (isWide(cp)) wide += 1;
|
|
45
|
+
else narrow += 1;
|
|
46
|
+
}
|
|
47
|
+
return wide + Math.ceil(narrow / 4);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isWide(cp) {
|
|
51
|
+
return (cp >= 0x2000 && cp <= 0x206f) // 通用标点(破折号、省略号、弯引号)
|
|
52
|
+
|| (cp >= 0x3000 && cp <= 0x303f) // CJK 标点
|
|
53
|
+
|| (cp >= 0x2e80 && cp <= 0x9fff) // CJK 部首至统一汉字
|
|
54
|
+
|| (cp >= 0xf900 && cp <= 0xfaff) // 兼容汉字
|
|
55
|
+
|| (cp >= 0xfe30 && cp <= 0xfe4f) // CJK 兼容形式
|
|
56
|
+
|| (cp >= 0xff00 && cp <= 0xffef); // 全角字符
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 上下文成本上限(加权 token)。
|
|
61
|
+
* 全局段按短工作区路径计;worst 档按真实长路径加迁移提示行计。
|
|
62
|
+
* 单条工具描述单独设限,避免每次请求的固定开销无界增长。
|
|
63
|
+
*/
|
|
64
|
+
export const TOKEN_BUDGET = {
|
|
65
|
+
globalTokens: 320,
|
|
66
|
+
globalWorstTokens: 360,
|
|
67
|
+
projectTokens: 300,
|
|
68
|
+
projectWorstTokens: 340,
|
|
69
|
+
toolDescTokens: 70,
|
|
70
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/** 闸门:通用约束条同步复核与交接文档体检。 */
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readUtf8, isFile, isDir } from './text.mjs';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import { charterPath, canonicalBlock, TITLE_KEY, WATER_KEY } from './charter.mjs';
|
|
6
|
+
import { readProfile, listProfiles } from './profile.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 读 Registry.md 的「形态」列 → { 项目名: 形态 }。
|
|
10
|
+
* 口径(Charter §1.6、§2.5):只有 library 才受通用约束条闸门管;artifact、shortcut、product 属工程型,豁免。
|
|
11
|
+
* 缺该列或读不到 → 全部按 library 处理(向后兼容)。
|
|
12
|
+
*/
|
|
13
|
+
function registryForms(ws) {
|
|
14
|
+
const p = path.join(ws, '_Project_Meta', 'Registry.md');
|
|
15
|
+
if (!isFile(p)) return {};
|
|
16
|
+
let text = '';
|
|
17
|
+
try { text = readUtf8(p); } catch { return {}; }
|
|
18
|
+
const out = {};
|
|
19
|
+
let head = null;
|
|
20
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
21
|
+
const s = raw.trim();
|
|
22
|
+
if (!s.startsWith('|')) continue;
|
|
23
|
+
const cells = s.replace(/^\||\|$/g, '').split('|').map((c) => c.trim());
|
|
24
|
+
if (!head) {
|
|
25
|
+
if (cells.indexOf('项目名') !== -1) head = cells;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (head.indexOf('形态') === -1) return {};
|
|
29
|
+
if (cells.every((c) => /^[-:\s]*$/.test(c))) continue;
|
|
30
|
+
const iName = head.indexOf('项目名');
|
|
31
|
+
const iForm = head.indexOf('形态');
|
|
32
|
+
if (cells.length <= Math.max(iName, iForm)) continue;
|
|
33
|
+
out[cells[iName]] = cells[iForm].replace(/`/g, '').trim();
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 工作区下所有「库型」项目库(带 RULES.md):返回 [{ name, root }];工程型按形态跳过。 */
|
|
39
|
+
function libraryRoots(ws) {
|
|
40
|
+
const forms = registryForms(ws);
|
|
41
|
+
const projDir = path.join(ws, 'Projects');
|
|
42
|
+
const out = [];
|
|
43
|
+
let projects = [];
|
|
44
|
+
try { projects = fs.readdirSync(projDir); } catch { return out; }
|
|
45
|
+
for (const name of projects) {
|
|
46
|
+
const pdir = path.join(projDir, name);
|
|
47
|
+
if (!isDir(pdir)) continue;
|
|
48
|
+
const form = forms[name] || 'library';
|
|
49
|
+
if (form !== 'library') continue;
|
|
50
|
+
// 骨架档案可让项目决定是否启用 24 行约束条;未注册时按默认(启用)处理。
|
|
51
|
+
const pf = readProfile(ws, { role: 'project', project: name });
|
|
52
|
+
if (pf.exists && pf.profile.gate && pf.profile.gate.libraryBlock === false) continue;
|
|
53
|
+
let libs = [];
|
|
54
|
+
try { libs = fs.readdirSync(pdir); } catch { continue; }
|
|
55
|
+
for (const lib of libs) {
|
|
56
|
+
const root = path.join(pdir, lib);
|
|
57
|
+
if (!isDir(root)) continue;
|
|
58
|
+
if (isFile(path.join(root, 'RULES.md'))) out.push({ name, root });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 同步复核:返回 { libraries, total, missing, verdict, ok }。 */
|
|
65
|
+
/** 插件内置的 Charter 骨架(dist/templates/Charter.skeleton.md);工作区那份只是覆盖。 */
|
|
66
|
+
function bundledCharter() {
|
|
67
|
+
try {
|
|
68
|
+
const p = new URL('../templates/Charter.skeleton.md', import.meta.url);
|
|
69
|
+
return fs.readFileSync(p, 'utf8');
|
|
70
|
+
} catch {
|
|
71
|
+
return '';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function syncCheck(ws) {
|
|
76
|
+
const charter = charterPath(ws);
|
|
77
|
+
// 口径:**插件自带的这套默认规则才是本体**,工作区里的同名文件是「本地化覆盖」。
|
|
78
|
+
// 所以只有「连 _Control_Center 都没有」才算没接入工作区;有目录但没 Charter 时用内置那份继续跑。
|
|
79
|
+
const hasCenter = isDir(path.join(ws, '_Control_Center'));
|
|
80
|
+
let text = '';
|
|
81
|
+
let builtin = false;
|
|
82
|
+
if (isFile(charter)) {
|
|
83
|
+
text = readUtf8(charter);
|
|
84
|
+
} else if (hasCenter) {
|
|
85
|
+
text = bundledCharter();
|
|
86
|
+
builtin = true;
|
|
87
|
+
} else {
|
|
88
|
+
return {
|
|
89
|
+
error: '这个实例还没接入工作区:读不到 ' + charter,
|
|
90
|
+
path: charter,
|
|
91
|
+
ok: false,
|
|
92
|
+
noWorkspace: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const blk = canonicalBlock(text);
|
|
96
|
+
if (!blk || !blk.body || blk.body.length === 0) {
|
|
97
|
+
return { error: '无法获取 §8.1.1.4 的正文(结构可能变了)', path: charter, ok: false };
|
|
98
|
+
}
|
|
99
|
+
const rows = [];
|
|
100
|
+
let bad = 0;
|
|
101
|
+
for (const lib of libraryRoots(ws)) {
|
|
102
|
+
const rules = readUtf8(path.join(lib.root, 'RULES.md'));
|
|
103
|
+
const missing = blk.body.filter((l) => rules.indexOf(l) === -1);
|
|
104
|
+
const titleOk = rules.indexOf(TITLE_KEY) !== -1;
|
|
105
|
+
const waterOk = rules.indexOf(WATER_KEY) !== -1;
|
|
106
|
+
const ok = titleOk && missing.length === 0 && waterOk;
|
|
107
|
+
if (!ok) bad++;
|
|
108
|
+
rows.push({ name: lib.name, titleOk, waterOk, missing, total: blk.body.length, ok });
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
libraries: rows,
|
|
112
|
+
profiles: listProfiles(ws),
|
|
113
|
+
total: rows.length,
|
|
114
|
+
bodyLines: blk.body.length,
|
|
115
|
+
builtin,
|
|
116
|
+
missing: bad,
|
|
117
|
+
ok: bad === 0,
|
|
118
|
+
verdict: bad === 0
|
|
119
|
+
|
|
120
|
+
? "全部落位。"
|
|
121
|
+
: bad + ' 个库有缺 —— 向对应项目会话派发(由它自己写进本库 RULES.md,全局不得改项目库内文件)。',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function renderSyncCheck(res) {
|
|
126
|
+
const out = [];
|
|
127
|
+
if (res.error) { out.push(res.error + ':' + res.path); return out; }
|
|
128
|
+
out.push('通用约束条同步复核 库数 ' + res.total + ' 正文 ' + res.bodyLines + ' 行 来源 §8.1.1.4' + (res.builtin ? '(工作区没有 Workspace_Charter.md,用插件内置口径)' : ''));
|
|
129
|
+
for (const r of res.libraries) {
|
|
130
|
+
const flag = r.ok ? '通过' : '未通过';
|
|
131
|
+
out.push(' ' + r.name + ' 标题 ' + (r.titleOk ? '通过' : '缺') + ' 正文 ' + (r.total - r.missing.length) + '/' + r.total +
|
|
132
|
+
' 水位自查 ' + (r.waterOk ? '通过' : '缺') + ' ' + flag);
|
|
133
|
+
for (const m of r.missing.slice(0, 3)) out.push(" 缺: " + m.slice(0, 70));
|
|
134
|
+
}
|
|
135
|
+
const ps = res.profiles || [];
|
|
136
|
+
if (ps.length) {
|
|
137
|
+
out.push('');
|
|
138
|
+
out.push('项目与杂项骨架');
|
|
139
|
+
for (const p of ps) {
|
|
140
|
+
const flag = p.exists ? (p.ok ? '通过' : '未通过') : '待注册';
|
|
141
|
+
const miss = p.missing && p.missing.length ? ' 缺 ' + p.missing.join('、') : '';
|
|
142
|
+
out.push(' ' + p.name + ' ' + flag + ' 类别 ' + p.kind + miss);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
out.push('');
|
|
146
|
+
out.push('判定:' + res.verdict);
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 设置页「使用说明」的内容:面向人,不面向模型。
|
|
3
|
+
* 数据放宿主侧,客户端只负责渲染——改文案不用动客户端 bundle。
|
|
4
|
+
* 行文用标准书面语:不用口语化的示例句,示例一律写成可以直接提的要求。
|
|
5
|
+
* 水位与工具数由 facts.mjs、names.mjs 推导,不在这里重复写死。
|
|
6
|
+
*/
|
|
7
|
+
import { compactSummaryText, watermark } from './facts.mjs';
|
|
8
|
+
import { ALL_TOOL_NAMES } from './names.mjs';
|
|
9
|
+
|
|
10
|
+
/** 首次安装、尚未接入工作区时的下一步(设置页与工具输出共用同一份)。 */
|
|
11
|
+
export const SETUP_STEPS = [
|
|
12
|
+
'准备一个长期存在的目录作为工作区的上级目录,不要选临时目录或下载目录。',
|
|
13
|
+
'在本页下方的输入框中粘贴该目录的绝对路径,点击「建立全局文件体系」:插件会在其中创建 _Workspace 目录,并铺好 _Control_Center、Projects 等整套结构。',
|
|
14
|
+
'点击「注册为工作区」完成注册;也可以把页面上显示的绝对路径复制下来,粘贴到 DSH 的新建工作区窗口。',
|
|
15
|
+
'新建一个标题含「全局」的会话。下一轮起,本插件会把该目录接入闸门、交接体检与会话水位三项。',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export function buildGuide(cfg) {
|
|
19
|
+
const w = watermark(cfg);
|
|
20
|
+
const compact = compactSummaryText(cfg);
|
|
21
|
+
const toolCount = ALL_TOOL_NAMES.length;
|
|
22
|
+
return {
|
|
23
|
+
intro: [
|
|
24
|
+
'本插件的全部功能都在对话中调用:不必记命令,也不必操作菜单,用中文说明要做的事,全局会话会选择相应的工具执行。',
|
|
25
|
+
'工具与提示按会话角色载入:全局会话可见全部 ' + toolCount + ' 个工具,项目与杂项会话只载入自己需要的几个,普通会话不载入插件工具,也不注入提示段。角色在下方「会话角色」中指定。',
|
|
26
|
+
'本页除首次安装、全局前置提示词与会话角色外均为只读;需要最新读数时,点击右上角的「刷新」。',
|
|
27
|
+
],
|
|
28
|
+
situations: [
|
|
29
|
+
{ when: '作业开始与结束', say: '先出一份起始简报', what: '汇总上下文水位、通用约束条闸门、会话清点与阶段建议(ho_brief)' },
|
|
30
|
+
{ when: '查阅规则', say: '把 Charter 第 7.2.3 节取出来', what: '按节返回规则正文,避免整篇读取(ho_charter;完整流程用 doc="runtime")' },
|
|
31
|
+
{ when: '交接与压缩续跑', say: '先把交接文档写好,再压缩并继续', what: '写自己角色的交接、体检、登记压缩,回合结束后自动压缩并自动续跑(ho_handover、ho_checkpoint)' },
|
|
32
|
+
{ when: '接近上下文上限', say: '查一下本会话的上下文水位', what: compact + '(ho_phase)' },
|
|
33
|
+
{ when: '换任', say: '建立下一代全局会话', what: '建会话、唤醒并投递启动提示词;在任会话水位低于 ' + w.warn + ' 时拒绝新建(ho_sessions action=new)' },
|
|
34
|
+
{ when: '会话角色', say: '把 Demo1 设成项目角色,并让它整理自己的文件', what: '写角色表、当场收窄该会话的工具与提示、投递一次性整理指令(ho_sessions action=role / 本页会话角色)' },
|
|
35
|
+
{ when: '通知项目会话', say: '通知 Demo 更新交接文档', what: '投递一条消息;会消耗目标会话一整轮 token,应尽量少用(peer_send)' },
|
|
36
|
+
{ when: '全局前置提示词', say: '把这段设为全局前置提示词', what: '保存到本机;仅在你开启且内容非空时注入全部会话,关闭或留空时不注册提示段' },
|
|
37
|
+
{ when: '项目骨架', say: '注册 Demo 的骨架', what: '读取、注册、校验项目或杂项的骨架档案;由该项目会话自己写文件并注册(ho_profile)' },
|
|
38
|
+
{ when: '查看会话', say: '列出当前活跃的会话', what: '水位、运行状态、活跃或沉默、在任或前任(ho_sessions)' },
|
|
39
|
+
{ when: '打包迁移', say: '把 Demo 打包成迁移包', what: '收集文件、逐文件 sha256、7z、自检、绝对路径体检(ho_pack action=pack)' },
|
|
40
|
+
{ when: '在新机器恢复', say: '预演这次恢复', what: '校验、列出覆盖清单、备份、写入;默认仅预演(ho_pack action=restore)' },
|
|
41
|
+
{ when: '删除会话', say: '把这个会话归档', what: '官方仅提供归档;需要再次唤醒时须先解归档(ho_sessions action=archive、unarchive)' },
|
|
42
|
+
{ when: '建立工作区', say: '把这个目录注册为工作区', what: '注册已存在的目录;删除只删注册,目录与会话日志均保留(ho_workspace)' },
|
|
43
|
+
{ when: '首次安装', say: '把某个目录建成全局根目录', what: '在该目录下建立整套全局文件体系,并注册为工作区(ho_init)' },
|
|
44
|
+
{ when: '自检', say: '检查插件当前状态', what: '服务可解析性、压缩调度器、压缩计数、本会话角色与工具集(ho_diag)' },
|
|
45
|
+
],
|
|
46
|
+
groups: [
|
|
47
|
+
{
|
|
48
|
+
title: '只读工具(不消耗任何会话的 token)',
|
|
49
|
+
tools: [
|
|
50
|
+
['ho_brief', '起始与收束的单条命令:水位、闸门、会话清点与阶段建议'],
|
|
51
|
+
['ho_gate', '通用约束条闸门(按库型判定)与交接文档体检'],
|
|
52
|
+
['ho_sessions', '会话清点;也用于设定角色、建会话、出提示词、归档'],
|
|
53
|
+
['ho_phase', '本会话阶段:水位、已压缩次数、锁状态、建议动作'],
|
|
54
|
+
['ho_charter', '按节返回规则正文;doc="runtime" 取完整流程手册'],
|
|
55
|
+
['ho_drift', '交接漂移体检:Δ 为当前轮与交接轮之差,大于 3 需刷新'],
|
|
56
|
+
['ho_diag', '插件自检:服务、调度器、压缩计数、角色判定与工具集'],
|
|
57
|
+
['ho_tokens', '会话 token 体检:轮数、步数、每步 token、最贵的单条返回'],
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
title: '写工具(会改动文件;均带备份或预演)',
|
|
62
|
+
tools: [
|
|
63
|
+
['ho_handover', '读写、追加、归档本会话自己的交接(写前备份、写后按角色体检)'],
|
|
64
|
+
['ho_checkpoint', '写交接、登记压缩,回合结束后自动压缩并自动续跑'],
|
|
65
|
+
['ho_snapshot', '库快照:逐字节复制、sha256 清单、来源说明'],
|
|
66
|
+
['ho_changelog', '按固定表格追加一行台账'],
|
|
67
|
+
['ho_pack', 'action:check 前置闸门、pack 打包、verify 校验、restore 恢复(默认预演)'],
|
|
68
|
+
['ho_workspace', '工作区列表、创建、删除;删除只删注册'],
|
|
69
|
+
['ho_init', '首次安装:在指定目录下建立整套全局文件体系,可顺带注册工作区'],
|
|
70
|
+
['ho_profile', '项目或杂项骨架:读取、注册、校验、取预设'],
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
title: '平级会话工具',
|
|
75
|
+
tools: [
|
|
76
|
+
['peer_list', '会话清点,只读投影缓存,不激活任何会话'],
|
|
77
|
+
['peer_status', '单会话详情:水位、运行状态、轮数、步数、工作目录'],
|
|
78
|
+
['peer_send', '向指定会话投递一条消息,是唯一会消耗对方 token 的动作'],
|
|
79
|
+
['peer_new', '新建平级会话,可指定工作区与标题'],
|
|
80
|
+
['peer_rename', '只改显示标题,不唤醒目标会话'],
|
|
81
|
+
['peer_peek', '查看指定会话近期的对话,会消耗本会话 token'],
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
rules: [
|
|
86
|
+
'交接线:上下文水位达到 ' + w.warn + ' 时提醒,达到 ' + w.handover + ' 时写交接并压缩,' + w.hard + ' 为硬上限;同一会话最多压缩 ' + w.max + ' 次,第四段改用新建会话。',
|
|
87
|
+
'压缩不可逆:历史会被摘要替换,因此必须先写出合格的交接文档;体检未通过时,插件拒绝安排压缩。',
|
|
88
|
+
'交接文件按角色分离:全局写 _Control_Center/records/Global_Handover.md,项目写 _Project_Meta/<项目>/Handover.md,杂项写 _Misc/tasks/<任务>/Handover.md,互不串用。',
|
|
89
|
+
'会话本身不迁移:跨机器、跨平台的连续性依托交接文档。',
|
|
90
|
+
'全局前置提示词属于用户内容,不是插件约束;关闭或留空时不占用注入点位,可与其它提示词插件并存。',
|
|
91
|
+
'项目与杂项的骨架由各自会话注册;构建型、动画型等项目不必套用插件类项目的文档集。',
|
|
92
|
+
'迁移只搬运文件与清单,不改写文件内容:包内写死的绝对路径会以清单形式报出,由用户或项目会话自行修改;库内引用一律使用相对路径。',
|
|
93
|
+
'插件本体为公开件,不含任何用户项目、会话或账号数据;用户数据与角色表都保存在本机。',
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 默认配置下的说明文本;设置页按实际配置调用 buildGuide。 */
|
|
99
|
+
const GUIDE = buildGuide();
|