@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
package/src/client.js
CHANGED
|
@@ -488,31 +488,31 @@ window.__ModuleLoader__.load({
|
|
|
488
488
|
// 压缩授权:只有用户在这里点一下,压缩才排得进去(AI 不能自己批)
|
|
489
489
|
const [csSid, setCsSid] = React.useState("");
|
|
490
490
|
const [csMsg, setCsMsg] = React.useState("");
|
|
491
|
-
const doGrant = React.useCallback(() => {
|
|
491
|
+
const doGrant = React.useCallback((kind) => {
|
|
492
492
|
setWorking(true); setCsMsg("");
|
|
493
|
-
post("consent.grant", { session_id: csSid }, 15000)
|
|
493
|
+
post("consent.grant", { session_id: csSid, kind: kind === "succeed" ? "succeed" : "compact" }, 15000)
|
|
494
494
|
.then((r) => {
|
|
495
|
-
if (r && r.ok) { setCsMsg("已授权会话 " + r.sessionId + ",5
|
|
495
|
+
if (r && r.ok) { setCsMsg("已授权会话 " + r.sessionId + ",5 分钟内可" + (r.kind === "succeed" ? "换任" : "压缩") + "一次,用后失效。"); load(); }
|
|
496
496
|
else setCsMsg("授权未完成:" + ((r && r.error) || "原因未知"));
|
|
497
497
|
})
|
|
498
498
|
.catch((e) => setCsMsg("请求未完成:" + (e && e.message ? e.message : String(e))))
|
|
499
499
|
.then(() => setWorking(false));
|
|
500
500
|
}, [csSid, load]);
|
|
501
|
-
const doGrantSid = React.useCallback((sid) => {
|
|
501
|
+
const doGrantSid = React.useCallback((sid, kind) => {
|
|
502
502
|
setWorking(true); setCsMsg("");
|
|
503
|
-
post("consent.grant", { session_id: sid }, 15000)
|
|
503
|
+
post("consent.grant", { session_id: sid, kind: kind === "succeed" ? "succeed" : "compact" }, 15000)
|
|
504
504
|
.then((r) => {
|
|
505
|
-
if (r && r.ok) { setCsMsg("已授权会话 " + r.sessionId + ",5
|
|
505
|
+
if (r && r.ok) { setCsMsg("已授权会话 " + r.sessionId + ",5 分钟内可" + (r.kind === "succeed" ? "换任" : "压缩") + "一次,用后失效。"); load(); }
|
|
506
506
|
else setCsMsg("授权未完成:" + ((r && r.error) || "原因未知"));
|
|
507
507
|
})
|
|
508
508
|
.catch((e) => setCsMsg("请求未完成:" + (e && e.message ? e.message : String(e))))
|
|
509
509
|
.then(() => setWorking(false));
|
|
510
510
|
}, [load]);
|
|
511
511
|
|
|
512
|
-
const doRevoke = React.useCallback((sid) => {
|
|
512
|
+
const doRevoke = React.useCallback((sid, kind) => {
|
|
513
513
|
setWorking(true); setCsMsg("");
|
|
514
|
-
post("consent.revoke", { session_id: sid }, 15000)
|
|
515
|
-
.then(() => { setCsMsg("已撤销 " + sid + "
|
|
514
|
+
post("consent.revoke", { session_id: sid, kind: kind === "succeed" ? "succeed" : "compact" }, 15000)
|
|
515
|
+
.then(() => { setCsMsg("已撤销 " + sid + " 的" + (kind === "succeed" ? "换任" : "压缩") + "授权。"); load(); })
|
|
516
516
|
.catch(() => { /* 撤销失败不影响别的 */ })
|
|
517
517
|
.then(() => setWorking(false));
|
|
518
518
|
}, [load]);
|
|
@@ -617,7 +617,7 @@ window.__ModuleLoader__.load({
|
|
|
617
617
|
globalMsg ? h("div", { className: globalBad ? "dshh-bad" : "dshh-ok" }, globalMsg) : null)));
|
|
618
618
|
}
|
|
619
619
|
|
|
620
|
-
//
|
|
620
|
+
// 二、压缩与换任授权:唯一合法的「用户同意」入口,置顶。
|
|
621
621
|
const consentRows = Array.isArray(data.consent) ? data.consent : [];
|
|
622
622
|
sections.push(card("压缩授权",
|
|
623
623
|
h("div", null,
|
|
@@ -629,8 +629,18 @@ window.__ModuleLoader__.load({
|
|
|
629
629
|
key: i,
|
|
630
630
|
disabled: working,
|
|
631
631
|
title: r.title || r.sid,
|
|
632
|
-
onClick: () => doGrantSid(r.sid),
|
|
632
|
+
onClick: () => doGrantSid(r.sid, "compact"),
|
|
633
633
|
}, "允许 " + shortSid(r.sid)))),
|
|
634
|
+
h("div", { className: "dshh-muted" }, "换任由在任会话自己执行,同样须由你授权:点「允许换任」后,该会话可在 5 分钟内换任一次。也可在对话中发送 /allow-succeed。"),
|
|
635
|
+
h("div", { className: "dshh-pick" },
|
|
636
|
+
byActivity.slice(0, 10).map((r, i) => h("button", {
|
|
637
|
+
className: "dshh-btn",
|
|
638
|
+
type: "button",
|
|
639
|
+
key: i,
|
|
640
|
+
disabled: working,
|
|
641
|
+
title: r.title || r.sid,
|
|
642
|
+
onClick: () => doGrantSid(r.sid, "succeed"),
|
|
643
|
+
}, "允许换任 " + shortSid(r.sid)))),
|
|
634
644
|
h("div", { className: "dshh-row" },
|
|
635
645
|
h("input", {
|
|
636
646
|
className: "dshh-inputSm",
|
|
@@ -639,15 +649,16 @@ window.__ModuleLoader__.load({
|
|
|
639
649
|
placeholder: "或输入会话 id 或 8 位短号",
|
|
640
650
|
onChange: (e) => setCsSid(e.target.value),
|
|
641
651
|
}),
|
|
642
|
-
h("button", { className: "dshh-btn", type: "button", disabled: working || !csSid, onClick: doGrant }, working ? "正在授权" : "授权")
|
|
652
|
+
h("button", { className: "dshh-btn", type: "button", disabled: working || !csSid, onClick: () => doGrant("compact") }, working ? "正在授权" : "授权"),
|
|
653
|
+
h("button", { className: "dshh-btn", type: "button", disabled: working || !csSid, onClick: () => doGrant("succeed") }, "授权换任")),
|
|
643
654
|
consentRows.length
|
|
644
655
|
? h("table", { className: "dshh-table" },
|
|
645
656
|
h("colgroup", null, h("col", { style: { width: "62%" } }), h("col", { style: { width: "18%" } }), h("col", { style: { width: "20%" } })),
|
|
646
657
|
h("thead", null, h("tr", null, h("th", null, "已授权会话"), h("th", null, "剩余"), h("th", null, "操作"))),
|
|
647
658
|
h("tbody", null, consentRows.map((r, i) => h("tr", { key: i },
|
|
648
|
-
h("td", { className: "dshh-code", title: r.sessionId }, shortSid(r.sessionId)),
|
|
659
|
+
h("td", { className: "dshh-code", title: r.sessionId }, shortSid(r.sessionId) + (r.kind === "succeed" ? " · 换任" : " · 压缩")),
|
|
649
660
|
h("td", { className: "dshh-num" }, Math.round((r.leftMs || 0) / 1000) + " 秒"),
|
|
650
|
-
h("td", null, h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: () => doRevoke(r.sessionId) }, "撤销"))))))
|
|
661
|
+
h("td", null, h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: () => doRevoke(r.sessionId, r.kind) }, "撤销"))))))
|
|
651
662
|
: h("div", { className: "dshh-muted" }, "当前无有效授权。"),
|
|
652
663
|
csMsg ? h("div", { className: "dshh-ok" }, csMsg) : null)));
|
|
653
664
|
|
package/src/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-handover —— 全局运行期插件。
|
|
3
3
|
* 判据进代码、流程进工具;规则正文留磁盘,需要时用 ho_charter 取节(完整流程 doc="runtime")。
|
|
4
|
-
* 按会话角色载入工具与提示段:全局
|
|
4
|
+
* 按会话角色载入工具与提示段:全局 24 个工具、项目 11、杂项 11、普通 0。
|
|
5
5
|
* 源码不热加载:改完要重启 DSH。
|
|
6
6
|
*/
|
|
7
7
|
import fs from 'node:fs';
|
|
@@ -27,6 +27,8 @@ import { pmPackTool } from './tools/ho_pack.mjs';
|
|
|
27
27
|
import { pmWorkspaceTool } from './tools/ho_workspace.mjs';
|
|
28
28
|
import { pmInitTool } from './tools/ho_init.mjs';
|
|
29
29
|
import { pmProfileTool } from './tools/ho_profile.mjs';
|
|
30
|
+
import { pmOfficeTool } from './tools/ho_office.mjs';
|
|
31
|
+
import { pmSucceedTool } from './tools/ho_succeed.mjs';
|
|
30
32
|
import { pmPeerListTool } from './tools/peer_list.mjs';
|
|
31
33
|
import { pmPeerStatusTool } from './tools/peer_status.mjs';
|
|
32
34
|
import { pmPeerSendTool } from './tools/peer_send.mjs';
|
|
@@ -470,7 +472,7 @@ export function apply(ctx, config) {
|
|
|
470
472
|
};
|
|
471
473
|
try { applyExtraPrompt(readExtraPrompt()); } catch (e) { initErrors.push('全局前置提示词:' + msgOf(e)); }
|
|
472
474
|
|
|
473
|
-
// ---------------- 工具注册(
|
|
475
|
+
// ---------------- 工具注册(18 个 ho_* + 6 个 peer_*)
|
|
474
476
|
// 0.2.2 起工具按「步」解析:可用工具集合每一步重算。子会话排除与角色过滤都在这里一次做完,
|
|
475
477
|
// 不再另用 agent 作用域 restrict —— 两套机制并行时,换角色要么等到重启、要么互相覆盖。
|
|
476
478
|
const roleToolsets = new Map(ROLES.map((r) => [r, new Set(toolsetFor(r).allow)]));
|
|
@@ -512,8 +514,10 @@ export function apply(ctx, config) {
|
|
|
512
514
|
pmWorkspaceTool(env),
|
|
513
515
|
pmInitTool(env),
|
|
514
516
|
pmProfileTool(env),
|
|
517
|
+
pmOfficeTool(env),
|
|
518
|
+
pmSucceedTool(env),
|
|
515
519
|
];
|
|
516
|
-
const keys = ['brief', 'gate', 'sessions', 'phase', 'charter', 'handover', 'checkpoint', 'snapshot', 'diag', 'drift', 'changelog', 'tokens', 'pack', 'workspace', 'init', 'profile'];
|
|
520
|
+
const keys = ['brief', 'gate', 'sessions', 'phase', 'charter', 'handover', 'checkpoint', 'snapshot', 'diag', 'drift', 'changelog', 'tokens', 'pack', 'workspace', 'init', 'profile', 'office', 'succeed'];
|
|
517
521
|
for (let i = 0; i < tools.length; i++) {
|
|
518
522
|
const key = keys[i];
|
|
519
523
|
try {
|
|
@@ -558,7 +562,7 @@ export function apply(ctx, config) {
|
|
|
558
562
|
const mk = (allow, why) => {
|
|
559
563
|
guardLast = { sid, allow, source: why, title: titleOf(sid) };
|
|
560
564
|
if (allow) return undefined;
|
|
561
|
-
if (nm === 'peer_new') return '
|
|
565
|
+
if (nm === 'peer_new') return '会话创建仅属全局:非全局会话不得新建平级会话(杂项落到 Misc 除外)。';
|
|
562
566
|
return '会话间通信受限(directPeer=off):非全局会话只能向现任全局报告,不得唤醒其他会话。';
|
|
563
567
|
};
|
|
564
568
|
if (info.role === 'global') return mk(true, 'global/' + info.source);
|
|
@@ -571,7 +575,7 @@ export function apply(ctx, config) {
|
|
|
571
575
|
if (cfg.directPeer !== 'off') return mk(true, 'direct/' + info.role + ' → ' + (t ? t.role : 'unknown'));
|
|
572
576
|
return mk(false, info.source + '/' + info.role + ' → ' + (t ? t.role : 'unknown'));
|
|
573
577
|
}
|
|
574
|
-
// peer_new
|
|
578
|
+
// peer_new:只有杂项可以落到 Misc
|
|
575
579
|
const target = String(args.workspace || args.cwd || '').toLowerCase();
|
|
576
580
|
if (info.role === 'misc' && target.indexOf('misc') !== -1) return mk(true, 'misc/' + info.source);
|
|
577
581
|
return mk(false, info.source + '/' + info.role);
|
|
@@ -689,36 +693,56 @@ export function apply(ctx, config) {
|
|
|
689
693
|
initErrors.push('设置页路由:' + msgOf(e));
|
|
690
694
|
}
|
|
691
695
|
|
|
692
|
-
// ---------------- 聊天页命令:用户在自己的输入框敲 /allow-compact
|
|
696
|
+
// ---------------- 聊天页命令:用户在自己的输入框敲 /allow-compact、/allow-succeed 即可授权
|
|
693
697
|
// 为什么要有它:设置页要切页面、还要填会话 id,太绕。斜杠命令是「用户亲手敲的一行」,
|
|
694
698
|
// 天然满足「只有用户能产生同意」这条判据,且不产生模型消息、不花 token。
|
|
695
699
|
try {
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
const d = service.register({
|
|
700
|
+
const allowCommands = [
|
|
701
|
+
{
|
|
699
702
|
name: 'allow-compact',
|
|
700
703
|
description: '允许本会话压缩一次(用户授权,5 分钟有效;可带会话短号授权别的会话)',
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
704
|
+
hint: '可留空=本会话;填 8 位短号或 session-id 则授权那个会话',
|
|
705
|
+
kind: 'compact',
|
|
706
|
+
note: '聊天页命令 /allow-compact',
|
|
707
|
+
action: '压缩',
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
name: 'allow-succeed',
|
|
711
|
+
description: '允许本会话换任一次(用户授权,5 分钟有效;可带会话短号授权别的会话)',
|
|
712
|
+
hint: '可留空=本会话;填 8 位短号或 session-id 则授权那个会话',
|
|
713
|
+
kind: 'succeed',
|
|
714
|
+
note: '聊天页命令 /allow-succeed',
|
|
715
|
+
action: '换任',
|
|
716
|
+
},
|
|
717
|
+
];
|
|
718
|
+
const registerAllow = (service) => {
|
|
719
|
+
if (!service || typeof service.register !== 'function') return;
|
|
720
|
+
for (const cmd of allowCommands) {
|
|
721
|
+
const d = service.register({
|
|
722
|
+
name: cmd.name,
|
|
723
|
+
description: cmd.description,
|
|
724
|
+
input: { hint: cmd.hint },
|
|
725
|
+
handler: ({ agent, rawInput }) => {
|
|
726
|
+
const arg = String(rawInput || '').trim();
|
|
727
|
+
const sid = arg ? resolveShortSid(env, arg) : (agent && agent.id ? String(agent.id) : '');
|
|
728
|
+
if (!sid) return { kind: 'error', text: '拿不到会话 id:请写成 /' + cmd.name + ' session-xxxx' };
|
|
729
|
+
const g = grantConsent(sid, cmd.note, cmd.kind);
|
|
730
|
+
try { env.log(cmd.action + '授权(命令):' + sid); } catch { /* ignore */ }
|
|
731
|
+
return { kind: 'success', text: '已允许 ' + sid + ' 在 ' + g.minutes + ' 分钟内' + cmd.action + '一次(用掉即失效)。' };
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
if (typeof d === 'function' && typeof ctx.effect === 'function') {
|
|
735
|
+
ctx.effect(() => d, 'dsh-handover: /' + cmd.name);
|
|
736
|
+
}
|
|
713
737
|
}
|
|
714
738
|
};
|
|
715
739
|
// 可选服务:能用 ctx.inject 就走注入(老宿主没有 commands 时插件照常激活),否则退回直取。
|
|
716
740
|
if (typeof ctx.inject === 'function') {
|
|
717
741
|
ctx.inject(['commands'], (scope) => {
|
|
718
|
-
try {
|
|
742
|
+
try { registerAllow(scope && scope.commands); } catch (e) { initErrors.push('命令注册:' + msgOf(e)); }
|
|
719
743
|
});
|
|
720
744
|
} else {
|
|
721
|
-
|
|
745
|
+
registerAllow(ctx.commands);
|
|
722
746
|
}
|
|
723
747
|
} catch (e) {
|
|
724
748
|
initErrors.push('命令注册:' + msgOf(e));
|
package/src/lib/api.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* 设置页数据与动作的唯一实现:HTTP 路由(旧通道)和 /api 载体(新通道)都调它。
|
|
3
3
|
* 返回统一信封:{ ok:true, value } | { ok:false, error:{code,message,details}, http }。
|
|
4
4
|
* http 只是给旧 HTTP 通道用的状态码提示,载体通道不用它。
|
|
@@ -240,8 +240,9 @@ export function createApi(env, info) {
|
|
|
240
240
|
if (method === 'consent.grant') {
|
|
241
241
|
const sid = resolveShortSid(env, p.session_id);
|
|
242
242
|
if (!sid) return bad('bad-request', '缺少 session_id');
|
|
243
|
-
const
|
|
244
|
-
|
|
243
|
+
const kind = p.kind === 'succeed' ? 'succeed' : 'compact';
|
|
244
|
+
const g = grantConsent(sid, kind === 'succeed' ? '设置页点击换任' : '设置页点击', kind);
|
|
245
|
+
env.log((kind === 'succeed' ? '换任' : '压缩') + '授权:' + sid + '(5 分钟)');
|
|
245
246
|
invalidateStatus(env);
|
|
246
247
|
return ok(g);
|
|
247
248
|
}
|
|
@@ -250,7 +251,7 @@ export function createApi(env, info) {
|
|
|
250
251
|
|
|
251
252
|
if (method === 'consent.revoke') {
|
|
252
253
|
const sid = String(p.session_id || '').trim();
|
|
253
|
-
if (sid) consumedConsent(sid);
|
|
254
|
+
if (sid) consumedConsent(sid, p.kind === 'succeed' ? 'succeed' : 'compact');
|
|
254
255
|
invalidateStatus(env);
|
|
255
256
|
return ok({ revoked: sid });
|
|
256
257
|
}
|
package/src/lib/consent.mjs
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 一次性授权:压缩会丢历史,换任会换掉在任会话,所以「同意」必须由用户自己产生,AI 不能自己给自己批。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 授权一次有效 5
|
|
4
|
+
* 做法:授权入口只有用户亲手产生的那一次(设置页点击或聊天页斜杠命令),
|
|
5
|
+
* 授权一次有效 5 分钟、只对指定会话与指定种类生效;压缩与换任各有各的授权,互不顶替。
|
|
6
|
+
* 种类取 compact(压缩)或 succeed(换任),缺省为 compact;旧文件里没有 kind 的条目按 compact 处理。
|
|
6
7
|
*/
|
|
7
8
|
import fs from 'node:fs';
|
|
8
9
|
import os from 'node:os';
|
|
9
10
|
import path from 'node:path';
|
|
10
11
|
|
|
11
12
|
export const CONSENT_TTL_MS = 5 * 60 * 1000;
|
|
13
|
+
export const CONSENT_KINDS = ['compact', 'succeed'];
|
|
14
|
+
const DEFAULT_KIND = 'compact';
|
|
12
15
|
|
|
13
16
|
function consentPath() {
|
|
14
17
|
const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
|
|
@@ -37,48 +40,84 @@ function stampNow() {
|
|
|
37
40
|
return d.getUTCFullYear() + '-' + p(d.getUTCMonth() + 1) + '-' + p(d.getUTCDate()) + ' ' + p(d.getUTCHours()) + ':' + p(d.getUTCMinutes()) + ' +0800';
|
|
38
41
|
}
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
function kindOf(kind) {
|
|
44
|
+
const k = String(kind === undefined || kind === null ? '' : kind).trim();
|
|
45
|
+
if (!k) return DEFAULT_KIND;
|
|
46
|
+
if (CONSENT_KINDS.indexOf(k) === -1) throw new Error('未知授权种类:' + k + '(只能是 compact 或 succeed)');
|
|
47
|
+
return k;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 存储键:压缩沿用会话 id 本身(兼容旧文件),换任另加后缀,两种授权可以同时存在。
|
|
52
|
+
*/
|
|
53
|
+
function keyFor(sid, kind) {
|
|
54
|
+
return kind === DEFAULT_KIND ? sid : sid + '|' + kind;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sidOfKey(key) {
|
|
58
|
+
return key.indexOf('|') === -1 ? key : key.slice(0, key.indexOf('|'));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 条目自身的种类优先;旧条目没有 kind,按 compact 处理。 */
|
|
62
|
+
function kindOfEntry(key, entry) {
|
|
63
|
+
const k = entry && entry.kind ? String(entry.kind) : '';
|
|
64
|
+
if (CONSENT_KINDS.indexOf(k) !== -1) return k;
|
|
65
|
+
return key.indexOf('|succeed') === -1 ? DEFAULT_KIND : 'succeed';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 用户授权一次:kind 缺省为压缩。 */
|
|
69
|
+
export function grantConsent(sessionId, note, kind) {
|
|
42
70
|
const sid = String(sessionId || '');
|
|
43
71
|
if (!sid) throw new Error('缺少会话 id');
|
|
72
|
+
const k = kindOf(kind);
|
|
44
73
|
const all = readAll();
|
|
45
74
|
const now = Date.now();
|
|
46
|
-
|
|
75
|
+
const key = keyFor(sid, k);
|
|
76
|
+
all[key] = { at: now, until: now + CONSENT_TTL_MS, atText: stampNow(), note: String(note || ''), kind: k };
|
|
47
77
|
writeAll(all);
|
|
48
|
-
return { sessionId: sid, until: new Date(all[
|
|
78
|
+
return { sessionId: sid, kind: k, until: new Date(all[key].until).toISOString(), minutes: CONSENT_TTL_MS / 60000, atText: all[key].atText };
|
|
49
79
|
}
|
|
50
80
|
|
|
51
81
|
/** 授权是否有效(每次读取时一并清除已过期者)。 */
|
|
52
|
-
export function checkConsent(sessionId) {
|
|
82
|
+
export function checkConsent(sessionId, kind) {
|
|
53
83
|
const sid = String(sessionId || '');
|
|
84
|
+
const k = kindOf(kind);
|
|
54
85
|
if (!sid) return { ok: false, reason: '没有会话 id' };
|
|
55
86
|
const all = readAll();
|
|
56
|
-
const
|
|
87
|
+
const key = keyFor(sid, k);
|
|
88
|
+
const hit = all[key];
|
|
57
89
|
const now = Date.now();
|
|
58
90
|
let dirty = false;
|
|
59
|
-
for (const
|
|
60
|
-
if (!all[
|
|
91
|
+
for (const keyName of Object.keys(all)) {
|
|
92
|
+
if (!all[keyName] || !all[keyName].until || all[keyName].until <= now) { delete all[keyName]; dirty = true; }
|
|
61
93
|
}
|
|
62
94
|
if (dirty) { try { writeAll(all); } catch { /* ignore */ } }
|
|
63
95
|
if (!hit || !hit.until || hit.until <= now) {
|
|
64
|
-
return {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
kind: k,
|
|
99
|
+
reason: k === 'succeed' ? '用户还没有授权换任(聊天页命令 /allow-succeed)' : '用户还没有在设置页点「允许压缩」',
|
|
100
|
+
};
|
|
65
101
|
}
|
|
66
|
-
return { ok: true, until: hit.until, leftMs: hit.until - now, atText: hit.atText || '' };
|
|
102
|
+
return { ok: true, kind: k, until: hit.until, leftMs: hit.until - now, atText: hit.atText || '' };
|
|
67
103
|
}
|
|
68
104
|
|
|
69
|
-
export function consumedConsent(sessionId) {
|
|
105
|
+
export function consumedConsent(sessionId, kind) {
|
|
70
106
|
const all = readAll();
|
|
71
107
|
const sid = String(sessionId || '');
|
|
72
|
-
|
|
108
|
+
const key = keyFor(sid, kindOf(kind));
|
|
109
|
+
if (all[key]) { delete all[key]; try { writeAll(all); } catch { /* ignore */ } }
|
|
73
110
|
}
|
|
74
111
|
|
|
75
112
|
export function listConsent() {
|
|
76
113
|
const all = readAll();
|
|
77
114
|
const now = Date.now();
|
|
78
|
-
|
|
79
|
-
sessionId:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
115
|
+
const rows = Object.keys(all).map((key) => ({
|
|
116
|
+
sessionId: sidOfKey(key),
|
|
117
|
+
kind: kindOfEntry(key, all[key]),
|
|
118
|
+
leftMs: Math.max(0, (all[key].until || 0) - now),
|
|
119
|
+
atText: all[key].atText || '',
|
|
120
|
+
note: all[key].note || '',
|
|
83
121
|
})).filter((r) => r.leftMs > 0);
|
|
122
|
+
return rows.sort((a, b) => a.sessionId.localeCompare(b.sessionId, 'zh') || a.kind.localeCompare(b.kind));
|
|
84
123
|
}
|
package/src/lib/facts.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 口径单一来源:水位阈值、压缩次数与提示段 token
|
|
3
|
-
* 运行期文本一律由本模块渲染;改数值只改 config.mjs
|
|
2
|
+
* 口径单一来源:水位阈值、压缩次数与提示段 token 参考值。
|
|
3
|
+
* 运行期文本一律由本模块渲染;改数值只改 config.mjs 的默认值或本文件的参考表。
|
|
4
4
|
* 文档与测试引用同一组常量,避免同一事实多处各写一遍。
|
|
5
5
|
*/
|
|
6
6
|
import { DEFAULTS } from './config.mjs';
|
|
@@ -57,9 +57,10 @@ function isWide(cp) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
*
|
|
60
|
+
* 上下文成本参考值(加权 token)。
|
|
61
61
|
* 全局段按短工作区路径计;worst 档按真实长路径加迁移提示行计。
|
|
62
|
-
*
|
|
62
|
+
* 仅供参考:用于设置页展示与冒烟实测,不作为通过条件,也不参与任何拒绝或截断。
|
|
63
|
+
* 提示词效果优先于字数,实测值超出参考值时如实展示即可。
|
|
63
64
|
*/
|
|
64
65
|
export const TOKEN_BUDGET = {
|
|
65
66
|
globalTokens: 320,
|
package/src/lib/guide.mjs
CHANGED
|
@@ -32,7 +32,7 @@ export function buildGuide(cfg) {
|
|
|
32
32
|
{ when: '查阅规则', say: '把 Charter 第 7.2.3 节取出来', what: '按节返回规则正文,避免整篇读取(ho_charter;完整流程用 doc="runtime")' },
|
|
33
33
|
{ when: '交接与压缩续跑', say: '先把交接文档写好,再压缩并继续', what: '写自己角色的交接、体检、登记压缩,并由助手选择压缩后去向:继续工作时用 resume=continue,任务已收尾时用 resume=wait(压缩后不唤醒)(ho_handover、ho_checkpoint)' },
|
|
34
34
|
{ when: '接近上下文上限', say: '查一下本会话的上下文水位', what: compact + '(ho_phase)' },
|
|
35
|
-
{ when: '换任', say: '
|
|
35
|
+
{ when: '换任', say: '让在任会话自己完成换任', what: '用户先发送 /allow-succeed 授权,在任会话随后校验交接、建接任会话并登记在任(ho_succeed);全局仍可用 ho_sessions action=new 手动新建会话' },
|
|
36
36
|
{ when: '会话角色', say: '把 Demo1 设成项目角色,并让它整理自己的文件', what: '写角色表、当场收窄该会话的工具与提示、投递一次性整理指令(ho_sessions action=role / 本页会话角色)' },
|
|
37
37
|
{ when: '通知项目会话', say: '通知 Demo 更新交接文档', what: '投递一条消息;会话之间可直接对话,不必经全局转述;会消耗目标会话一整轮 token,应尽量少用(peer_send)' },
|
|
38
38
|
{ when: '全局前置提示词', say: '把这段设为全局前置提示词', what: '保存到本机;仅在你开启且内容非空时注入全部会话,关闭或留空时不注册提示段' },
|
|
@@ -44,6 +44,7 @@ export function buildGuide(cfg) {
|
|
|
44
44
|
{ when: '建立工作区', say: '把这个目录注册为工作区', what: '注册已存在的目录;删除只删注册,目录与会话日志均保留(ho_workspace)' },
|
|
45
45
|
{ when: '首次安装', say: '把某个目录建成全局根目录', what: '在该目录下建立整套全局文件体系,并注册为工作区(ho_init)' },
|
|
46
46
|
{ when: '自检', say: '检查插件当前状态', what: '服务可解析性、压缩调度器、压缩计数、本会话角色与工具集(ho_diag)' },
|
|
47
|
+
{ when: '查在任', say: '现在 Mingent 是谁在任', what: '按全局、项目、杂项查现任会话的名字与会话 id(ho_office);不起会话也可用命令行 node scripts/office.mjs 同名查询' },
|
|
47
48
|
],
|
|
48
49
|
groups: [
|
|
49
50
|
{
|
|
@@ -57,6 +58,7 @@ export function buildGuide(cfg) {
|
|
|
57
58
|
['ho_drift', '交接漂移体检:Δ 为当前轮与交接轮之差,大于 3 需刷新'],
|
|
58
59
|
['ho_diag', '插件自检:服务、调度器、压缩计数、角色判定与工具集'],
|
|
59
60
|
['ho_tokens', '会话 token 体检:轮数、步数、每步 token、最贵的单条返回'],
|
|
61
|
+
['ho_office', '在任登记表:查全局、项目、杂项现在由哪个会话在任(名字与会话 id)'],
|
|
60
62
|
],
|
|
61
63
|
},
|
|
62
64
|
{
|
|
@@ -70,6 +72,7 @@ export function buildGuide(cfg) {
|
|
|
70
72
|
['ho_workspace', '工作区列表、创建、删除;删除只删注册'],
|
|
71
73
|
['ho_init', '首次安装:在指定目录下建立整套全局文件体系,可顺带注册工作区'],
|
|
72
74
|
['ho_profile', '项目或杂项骨架:读取、注册、校验、取预设'],
|
|
75
|
+
['ho_succeed', '换任自服务:在任会话经用户授权后自行校验交接、建接任会话并登记在任'],
|
|
73
76
|
],
|
|
74
77
|
},
|
|
75
78
|
{
|
package/src/lib/manual.mjs
CHANGED
|
@@ -10,7 +10,16 @@ import { handoverRuleText, watermark } from './facts.mjs';
|
|
|
10
10
|
const NL = String.fromCharCode(10);
|
|
11
11
|
|
|
12
12
|
/** 完整流程的指针句(三档角色共用)。 */
|
|
13
|
-
const RUNTIME_POINTER = '
|
|
13
|
+
const RUNTIME_POINTER = '细节按节取:ho_charter(doc="runtime", section=…)。';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 复用准则:全局、项目、杂项三处必须同一句。
|
|
17
|
+
* 派活与新建会话前先查在任,低水位在任直接复用;确需另建才新建。
|
|
18
|
+
*/
|
|
19
|
+
function reuseRule(cfg) {
|
|
20
|
+
return '指派任务或新建会话前,先用 ho_office(空参数)查一次在任;同一类别已有在任且其水位低于 '
|
|
21
|
+
+ watermark(cfg).warn + ' 时,直接用 peer_send 复用,不新建;确需另建(原会话不可用、需并行、用户明确要求)时才新建。';
|
|
22
|
+
}
|
|
14
23
|
|
|
15
24
|
/** 全局会话的常驻段:五节骨架(身份、权限、节奏、交接、按需取用)。 */
|
|
16
25
|
export function globalManual(opts) {
|
|
@@ -19,18 +28,19 @@ export function globalManual(opts) {
|
|
|
19
28
|
'dsh-handover 运行期(全局)',
|
|
20
29
|
'',
|
|
21
30
|
'一、身份',
|
|
22
|
-
'
|
|
31
|
+
'本会话为全局管理会话,协调会话、维护规则与交接。',
|
|
23
32
|
'',
|
|
24
33
|
'二、权限',
|
|
25
|
-
'可写 _Control_Center、_Project_Meta
|
|
26
|
-
'
|
|
34
|
+
'可写 _Control_Center、_Project_Meta 与项目结构;Projects 及各项目库内文件只读,不得代写;项目内作业不代办。',
|
|
35
|
+
'新建会话与设定角色仅属全局;会话间可直接 peer_send;换任须用户授权(/allow-succeed 或设置页)后由在任会话调用 ho_succeed,不再请求全局。',
|
|
36
|
+
reuseRule(o.cfg),
|
|
27
37
|
'',
|
|
28
38
|
'三、节奏',
|
|
29
39
|
handoverRuleText(o.cfg),
|
|
30
|
-
'
|
|
40
|
+
'压缩须用户点「压缩授权」,再写交接并 ho_checkpoint(confirm=true);任务未完成传 resume=continue,已收尾传 resume=wait,不再唤醒。confirm 仅表意图,授权以点击为准。关键步骤传 at_risk=true 暂缓。',
|
|
31
41
|
'',
|
|
32
42
|
'四、交接',
|
|
33
|
-
'
|
|
43
|
+
'交接:_Control_Center/records/Global_Handover.md。',
|
|
34
44
|
'',
|
|
35
45
|
'五、按需取用',
|
|
36
46
|
RUNTIME_POINTER,
|
|
@@ -62,8 +72,9 @@ export function scopedManual(info, opts) {
|
|
|
62
72
|
hp.path
|
|
63
73
|
? '交接:' + hp.path + '(ho_handover 默认)。'
|
|
64
74
|
: '交接路径未解析(缺角色或项目名),请先在设置页设定角色。',
|
|
65
|
-
handoverRuleText(o.cfg) + '
|
|
66
|
-
'会话间可直接 peer_send
|
|
75
|
+
handoverRuleText(o.cfg) + '压缩须用户点「压缩授权」,再由本会话执行 ho_checkpoint(confirm=true);收尾时传 resume=wait,不再唤醒。',
|
|
76
|
+
'会话间可直接 peer_send;默认沉默:仅在水位 ' + watermark(o.cfg).warn + ' 且已写交接、需用户决定、全局口径有误时向全局报告;任务完成不报全局。',
|
|
77
|
+
reuseRule(o.cfg),
|
|
67
78
|
RUNTIME_POINTER,
|
|
68
79
|
];
|
|
69
80
|
const mv = moveLine(o);
|
|
@@ -90,12 +101,12 @@ export function organizeMessageFor(info, opts) {
|
|
|
90
101
|
const o = opts || {};
|
|
91
102
|
const role = info && info.role ? info.role : 'plain';
|
|
92
103
|
if (role === 'plain') return '';
|
|
93
|
-
const head = '【dsh-handover
|
|
104
|
+
const head = '【dsh-handover 角色设定】本会话已设定为「' + scopeLabel(info) + '」。请按下列结构核对自身文件:缺失的目录应建立,位置不当的文件应归位;既有文件就地保留,不得删除或修改内容。';
|
|
94
105
|
if (role === 'project') {
|
|
95
106
|
const p = info.project;
|
|
96
107
|
return [
|
|
97
108
|
head,
|
|
98
|
-
'
|
|
109
|
+
'先通读本库,确定本项目的骨架:代码与文档主体、必需文件、临时与归档位置。构建型、动画型等项目不必套用插件类项目的文档集。',
|
|
99
110
|
'把结论写入 _Project_Meta/' + p + '/project.profile.json;字段格式见 ho_profile(action=presets)。',
|
|
100
111
|
'写入后调用 ho_profile(action=register) 校验并登记;返回缺失项时按清单补齐。',
|
|
101
112
|
'约束:仅可改动本库与 _Project_Meta/' + p + '/;不得改动 _Control_Center 及其他项目;移动较大文件前先执行 ho_snapshot。可用工具:ho_snapshot、ho_handover、ho_phase、ho_checkpoint、ho_profile、peer_send。',
|
|
@@ -106,10 +117,10 @@ export function organizeMessageFor(info, opts) {
|
|
|
106
117
|
const t = info.task;
|
|
107
118
|
return [
|
|
108
119
|
head,
|
|
109
|
-
'
|
|
120
|
+
'先通读本任务目录,确定本任务的骨架:工作文件、规则文件、交接、快照与归档位置。',
|
|
110
121
|
'把结论写入 _Misc/tasks/' + t + '/task.profile.json;字段格式见 ho_profile(action=presets)。',
|
|
111
122
|
'写入后调用 ho_profile(action=register) 校验并登记。',
|
|
112
|
-
'约束:仅可改动 _Misc/tasks/' + t + '/、_Temp/_Misc/' + t + '/、_Archives/_Misc/' + t + '/;_Control_Center、Projects
|
|
123
|
+
'约束:仅可改动 _Misc/tasks/' + t + '/、_Temp/_Misc/' + t + '/、_Archives/_Misc/' + t + '/;_Control_Center、Projects 与其他任务不得写入。工具:ho_snapshot、ho_handover、ho_phase、ho_checkpoint、ho_profile、peer_send。',
|
|
113
124
|
'完成后以一句话说明改动即可。',
|
|
114
125
|
].join(NL);
|
|
115
126
|
}
|
package/src/lib/names.mjs
CHANGED
|
@@ -7,7 +7,7 @@ const PREFIX = 'ho';
|
|
|
7
7
|
export const nameOf = (key) => PREFIX + '_' + key;
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* ho_*
|
|
10
|
+
* ho_* 18 个。合并过的家族(刻意保持旧名,参数里用 action 分流):
|
|
11
11
|
* ho_pack —— 打包 / 校验 / 预演 / 恢复(原 ho_pack_verify、ho_pack_restore、ho_pack_precheck)
|
|
12
12
|
* ho_sessions —— 清点 / 设定角色 / 建会话 / 出提示词 / 归档 / 解归档(原 ho_session_new、ho_prompt、ho_session_archive、ho_session_unarchive)
|
|
13
13
|
* ho_profile —— 骨架读取 / 注册 / 校验 / 预设
|
|
@@ -16,7 +16,7 @@ const KEYS = [
|
|
|
16
16
|
'brief', 'gate', 'sessions', 'phase', 'charter',
|
|
17
17
|
'handover', 'checkpoint', 'snapshot', 'diag',
|
|
18
18
|
'drift', 'changelog', 'tokens',
|
|
19
|
-
'pack', 'workspace', 'init', 'profile',
|
|
19
|
+
'pack', 'workspace', 'init', 'profile', 'office', 'succeed',
|
|
20
20
|
];
|
|
21
21
|
|
|
22
22
|
export const NAMES = KEYS.reduce((acc, k) => { acc[k] = nameOf(k); return acc; }, {});
|