@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
package/src/client.js
ADDED
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-handover 浏览器半:在「设置 → 交接」注册一个只读状态页。
|
|
3
|
+
* 手工构建的 DSH client bundle(不用 JSX/打包器):只 require react,
|
|
4
|
+
* 数据来自宿主侧注册的 GET /dsh-handover/status.json(只读、不唤醒任何会话)。
|
|
5
|
+
*
|
|
6
|
+
* 改了这个文件要重建 dist 并在 DSH 里重新激活插件(client bundle 不热更)。
|
|
7
|
+
*/
|
|
8
|
+
window.__ModuleLoader__.load({
|
|
9
|
+
id: '@mingmaple/dsh-handover',
|
|
10
|
+
factory: (require) => {
|
|
11
|
+
var module = { exports: {} };
|
|
12
|
+
var exports = module.exports;
|
|
13
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
14
|
+
|
|
15
|
+
const React = require("react");
|
|
16
|
+
const h = React.createElement;
|
|
17
|
+
/** 参与槽位注册的服务;connection=DSH 认证的 /api 载体(设置页取数的主通道)。 */
|
|
18
|
+
const inject = ["slots", "connection"];
|
|
19
|
+
let connection = null;
|
|
20
|
+
|
|
21
|
+
/** 数据接口候选:绝对路径优先,再退相对(页面挂在子路径时仍能取到)。 */
|
|
22
|
+
const ENDPOINTS = ["/dsh-handover/status.json", "dsh-handover/status.json"];
|
|
23
|
+
|
|
24
|
+
const CSS_TAG_ID = "@mingmaple/dsh-handover/SettingsSection.css";
|
|
25
|
+
const CSS = [
|
|
26
|
+
".dshh-root{display:flex;flex-direction:column;gap:14px;max-width:860px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.55}",
|
|
27
|
+
".dshh-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}",
|
|
28
|
+
".dshh-title{margin:0;font-size:16px;font-weight:600}",
|
|
29
|
+
".dshh-muted{color:var(--dsw-alias-label-tertiary)}",
|
|
30
|
+
".dshh-btn{font:inherit;cursor:pointer;background:0 0;color:var(--dsw-alias-label-primary);border:.5px solid var(--dsw-alias-border-l2);border-radius:6px;padding:3px 10px}",
|
|
31
|
+
".dshh-btn:disabled{opacity:.5;cursor:default}",
|
|
32
|
+
".dshh-card{border:.5px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:8px}",
|
|
33
|
+
".dshh-cardTitle{margin:0;font-weight:600;font-size:13px}",
|
|
34
|
+
".dshh-grid{display:grid;grid-template-columns:auto 1fr;gap:4px 14px}",
|
|
35
|
+
".dshh-k{color:var(--dsw-alias-label-tertiary)}",
|
|
36
|
+
".dshh-table{width:100%;border-collapse:collapse;table-layout:fixed}",
|
|
37
|
+
".dshh-table th,.dshh-table td{text-align:left;padding:3px 6px;border-bottom:.5px solid var(--dsw-alias-border-l2);font-size:12px;white-space:normal;overflow-wrap:anywhere;vertical-align:middle}",
|
|
38
|
+
".dshh-table th{white-space:nowrap;font-weight:600}",
|
|
39
|
+
".dshh-num{white-space:nowrap}",
|
|
40
|
+
".dshh-foot{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:6px}",
|
|
41
|
+
".dshh-pick{display:flex;align-items:center;gap:6px;flex-wrap:wrap}",
|
|
42
|
+
".dshh-inputSm{font:inherit;font-size:12px;padding:2px 6px;border:.5px solid var(--dsw-alias-border-l2);border-radius:6px;background:0 0;color:var(--dsw-alias-label-primary);box-sizing:border-box;width:100%}",
|
|
43
|
+
".dshh-ok{color:var(--dsw-alias-state-business-primary,#1a7f37)}",
|
|
44
|
+
".dshh-bad{color:var(--dsw-alias-state-error-primary,#c62828)}",
|
|
45
|
+
".dshh-warn{color:var(--dsw-alias-state-warning-primary,#b26a00)}",
|
|
46
|
+
".dshh-list{margin:0;padding-left:18px}",
|
|
47
|
+
".dshh-sub{margin:6px 0 2px;font-weight:600;color:var(--dsw-alias-label-primary)}",
|
|
48
|
+
".dshh-sum{cursor:pointer;color:var(--dsw-alias-label-tertiary);font-size:12px}",
|
|
49
|
+
".dshh-wrap td{white-space:normal}",
|
|
50
|
+
".dshh-input{font:inherit;width:100%;box-sizing:border-box;padding:5px 8px;border:.5px solid var(--dsw-alias-border-l2);border-radius:6px;background:0 0;color:var(--dsw-alias-label-primary)}",
|
|
51
|
+
".dshh-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:2px}",
|
|
52
|
+
".dshh-textarea{font:inherit;width:100%;box-sizing:border-box;padding:6px 8px;border:.5px solid var(--dsw-alias-border-l2);border-radius:6px;background:0 0;color:var(--dsw-alias-label-primary);resize:vertical}",
|
|
53
|
+
".dshh-code{font-family:ui-monospace,Consolas,monospace;font-size:12px}",
|
|
54
|
+
].join("");
|
|
55
|
+
|
|
56
|
+
/** 注入样式(按 tagId 去重,插件重复激活时不会堆叠)。 */
|
|
57
|
+
function installCss() {
|
|
58
|
+
if (typeof document === "undefined") return;
|
|
59
|
+
if (document.querySelector('style[data-plugin-css="' + CSS_TAG_ID + '"]') !== null) return;
|
|
60
|
+
const tag = document.createElement("style");
|
|
61
|
+
tag.dataset.plugin = "@mingmaple/dsh-handover";
|
|
62
|
+
tag.dataset.pluginCss = CSS_TAG_ID;
|
|
63
|
+
tag.textContent = CSS;
|
|
64
|
+
document.head.appendChild(tag);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 依次尝试候选地址取状态;全失败时抛出带全部原因的错。 */
|
|
68
|
+
/** 带超时的 fetch(旧通道回退用;没有超时就会永远「读取中」)。 */
|
|
69
|
+
async function fetchWithTimeout(url, opts, ms) {
|
|
70
|
+
const ac = new AbortController();
|
|
71
|
+
const timer = setTimeout(() => ac.abort(), ms || 8000);
|
|
72
|
+
try {
|
|
73
|
+
return await fetch(url, Object.assign({ signal: ac.signal }, opts || {}));
|
|
74
|
+
} finally {
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function fetchStatus() {
|
|
80
|
+
const reasons = [];
|
|
81
|
+
for (const url of ENDPOINTS) {
|
|
82
|
+
try {
|
|
83
|
+
const res = await fetchWithTimeout(url, { cache: "no-store", headers: { accept: "application/json" } }, 30000);
|
|
84
|
+
if (!res.ok) { reasons.push(url + " → HTTP " + res.status); continue; }
|
|
85
|
+
return await res.json();
|
|
86
|
+
} catch (e) {
|
|
87
|
+
reasons.push(url + " → " + (e && e.message ? e.message : String(e)));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
throw new Error(reasons.join(";") || "无法取得状态");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 通道诊断(显示于页面;出现异常时可直接定位,无需推测)。 */
|
|
94
|
+
const diag = { lastError: "" };
|
|
95
|
+
|
|
96
|
+
/** 页面级快照缓存:设置页切走再切回时先渲染旧读数,再后台刷新(不出现「正在读取」)。 */
|
|
97
|
+
const state = { last: null };
|
|
98
|
+
|
|
99
|
+
/** 读数时间保留完整值(含年份与时区),与页头一致。 */
|
|
100
|
+
function shortTime(s) {
|
|
101
|
+
return String(s || "");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 会话短号:去掉 session- 前缀,留前 8 位。 */
|
|
105
|
+
function shortSid(s) {
|
|
106
|
+
return String(s || "").replace(/^session-/, "").slice(0, 8);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 可折叠表格:先显示 pageSize 行,点「再展开」继续加 pageSize 行。
|
|
111
|
+
* 列宽由 widths 百分比给定(表头固定、不横向滚动)。
|
|
112
|
+
*/
|
|
113
|
+
function Folded(props) {
|
|
114
|
+
const rows = props.rows || [];
|
|
115
|
+
const size = props.pageSize || 5;
|
|
116
|
+
const [shown, setShown] = React.useState(size);
|
|
117
|
+
const visible = rows.slice(0, shown);
|
|
118
|
+
const rest = rows.length - visible.length;
|
|
119
|
+
const more = rest > 0
|
|
120
|
+
? h("button", { className: "dshh-btn", type: "button", onClick: () => setShown(shown + size) }, "再展开 " + Math.min(size, rest) + " 个")
|
|
121
|
+
: null;
|
|
122
|
+
const fold = shown > size
|
|
123
|
+
? h("button", { className: "dshh-btn", type: "button", onClick: () => setShown(size) }, "收起")
|
|
124
|
+
: null;
|
|
125
|
+
return h(React.Fragment, null,
|
|
126
|
+
h("table", { className: "dshh-table" },
|
|
127
|
+
h("colgroup", null, (props.widths || []).map((w, i) => h("col", { key: i, style: { width: w } }))),
|
|
128
|
+
h("thead", null, h("tr", null, (props.cols || []).map((c, i) => h("th", { key: i }, c)))),
|
|
129
|
+
h("tbody", null, visible.map((r, i) => props.render(r, i)))),
|
|
130
|
+
rows.length > size
|
|
131
|
+
? h("div", { className: "dshh-foot" },
|
|
132
|
+
h("span", { className: "dshh-muted" }, "共 " + rows.length + " 个,已显示 " + visible.length + " 个"),
|
|
133
|
+
more, fold)
|
|
134
|
+
: null);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function carrierKind() {
|
|
138
|
+
const conn = connection;
|
|
139
|
+
if (!conn) return "无 connection";
|
|
140
|
+
if (!conn.rpc) return "无 connection.rpc";
|
|
141
|
+
if (typeof conn.rpc.call !== "function") return "无 rpc.call";
|
|
142
|
+
return "可用(参数 " + (conn.rpc.call.length || "?") + ")";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 与超时赛跑:宿主 rpc.call 不理会 AbortSignal,光 abort 会让页面永远「读取中」。 */
|
|
146
|
+
function raceTimeout(promise, ms, label) {
|
|
147
|
+
return new Promise((resolve, reject) => {
|
|
148
|
+
const timer = setTimeout(() => reject(new Error(label + " 超时 " + ms + " ms")), ms);
|
|
149
|
+
Promise.resolve(promise).then(
|
|
150
|
+
(v) => { clearTimeout(timer); resolve(v); },
|
|
151
|
+
(e) => { clearTimeout(timer); reject(e); },
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** 主通道:DSH 认证的 /api 载体(宿主 registerRpcCarrier 注册)。 */
|
|
157
|
+
async function rpc(method, payload, ms) {
|
|
158
|
+
const conn = connection;
|
|
159
|
+
// 必须保留 this=connection.rpc:宿主用 this 拼目标,换成别的对象会得到
|
|
160
|
+
// "[object Object]/api" 并报 invalid RPC target。
|
|
161
|
+
const rpcObj = conn && conn.rpc && typeof conn.rpc.call === "function" ? conn.rpc : null;
|
|
162
|
+
if (!rpcObj) throw new Error("宿主没有 connection.rpc(载体=" + carrierKind() + ")");
|
|
163
|
+
// 不用 AbortController 主动中止:宿主处理可能超过 8 秒,只做与超时赛跑,
|
|
164
|
+
// 让请求在宿主侧自然完成,慢一点也能拿到结果。
|
|
165
|
+
const ac = new AbortController();
|
|
166
|
+
const limit = ms || 30000;
|
|
167
|
+
const t0 = Date.now();
|
|
168
|
+
try {
|
|
169
|
+
// 签名(与 dsh-im 一致):connection.rpc.call(target, endpoint, call, signal),且 this 必须是 connection.rpc。
|
|
170
|
+
let r;
|
|
171
|
+
try {
|
|
172
|
+
r = await raceTimeout(
|
|
173
|
+
rpcObj.call("/api", "dsh-handover", { method: method, payload: payload || {} }, ac.signal),
|
|
174
|
+
limit + 500,
|
|
175
|
+
"载体",
|
|
176
|
+
);
|
|
177
|
+
} catch (e) {
|
|
178
|
+
if (!/invalid RPC target/i.test(String(e && e.message ? e.message : e))) throw e;
|
|
179
|
+
// 退一步:有的宿主把 target 预置在 rpc 上,只收 (endpoint, call, signal)
|
|
180
|
+
r = await raceTimeout(
|
|
181
|
+
rpcObj.call("dsh-handover", { method: method, payload: payload || {} }, ac.signal),
|
|
182
|
+
limit + 500,
|
|
183
|
+
"载体",
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
if (!r || r.ok !== true) {
|
|
187
|
+
const err = (r && r.error) || {};
|
|
188
|
+
throw new Error(String(err.message || "载体返回失败"));
|
|
189
|
+
}
|
|
190
|
+
return r.value;
|
|
191
|
+
} catch (e) {
|
|
192
|
+
diag.lastError = "载体:" + String(e && e.message ? e.message : e) + "(耗时 " + (Date.now() - t0) + " ms)";
|
|
193
|
+
throw e;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** 写操作:先载体,失败再退 HTTP;成功统一补 ok:true,失败抛错。 */
|
|
198
|
+
async function post(method, payload, ms) {
|
|
199
|
+
try {
|
|
200
|
+
const v = await rpc(method, payload, ms || 20000);
|
|
201
|
+
return Object.assign({ ok: true }, v && typeof v === "object" ? v : { value: v });
|
|
202
|
+
} catch (e1) {
|
|
203
|
+
try {
|
|
204
|
+
const b = await postJson("/dsh-handover/" + method, payload);
|
|
205
|
+
return Object.assign({ ok: true }, b && typeof b === "object" ? b : { value: b });
|
|
206
|
+
} catch (e2) {
|
|
207
|
+
throw new Error(String(e1 && e1.message ? e1.message : e1) + "(HTTP 回退也失败:" + String(e2 && e2.message ? e2.message : e2) + ")");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** POST JSON:同样先绝对路径再相对路径。 */
|
|
213
|
+
async function postJson(pathname, payload) {
|
|
214
|
+
const reasons = [];
|
|
215
|
+
for (const base of ["", "."]) {
|
|
216
|
+
const url = base + pathname;
|
|
217
|
+
try {
|
|
218
|
+
const res = await fetchWithTimeout(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload || {}) }, 15000);
|
|
219
|
+
let body = null;
|
|
220
|
+
try { body = await res.json(); } catch (e) { body = null; }
|
|
221
|
+
if (body) return body;
|
|
222
|
+
reasons.push(url + " → HTTP " + res.status);
|
|
223
|
+
} catch (e) {
|
|
224
|
+
reasons.push(url + " → " + (e && e.message ? e.message : String(e)));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
throw new Error(reasons.join(";"));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function card(title, children) {
|
|
231
|
+
return h("section", { className: "dshh-card" }, h("h3", { className: "dshh-cardTitle" }, title), children);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function kv(rows) {
|
|
235
|
+
const kids = [];
|
|
236
|
+
rows.forEach((r, i) => {
|
|
237
|
+
kids.push(h("div", { className: "dshh-k", key: "k" + i }, r[0]));
|
|
238
|
+
kids.push(h("div", { key: "v" + i }, r[1]));
|
|
239
|
+
});
|
|
240
|
+
return h("div", { className: "dshh-grid" }, kids);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function verdict(el, ok) {
|
|
244
|
+
return h("span", { className: ok ? "dshh-ok" : "dshh-bad" }, el);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function fmtBytes(n) {
|
|
248
|
+
const v = Number(n) || 0;
|
|
249
|
+
if (v >= 1024 * 1024 * 1024) return (v / (1024 * 1024 * 1024)).toFixed(2) + " GB";
|
|
250
|
+
if (v >= 1024 * 1024) return (v / (1024 * 1024)).toFixed(1) + " MB";
|
|
251
|
+
if (v >= 1024) return (v / 1024).toFixed(1) + " KB";
|
|
252
|
+
return v + " B";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** 设置页正身:只读,一个刷新按钮。 */
|
|
256
|
+
function HandoverSection() {
|
|
257
|
+
// 首屏直接用上一次的读数(切走再切回不再出现「正在读取」),后台再刷新。
|
|
258
|
+
const [data, setData] = React.useState(state.last);
|
|
259
|
+
const [err, setErr] = React.useState("");
|
|
260
|
+
const [busy, setBusy] = React.useState(false);
|
|
261
|
+
// 首次安装向导
|
|
262
|
+
const [parent, setParent] = React.useState("");
|
|
263
|
+
const [created, setCreated] = React.useState("");
|
|
264
|
+
const [setupMsg, setSetupMsg] = React.useState("");
|
|
265
|
+
const [setupBad, setSetupBad] = React.useState(false);
|
|
266
|
+
const [working, setWorking] = React.useState(false);
|
|
267
|
+
// 追加系统提示词
|
|
268
|
+
const [extra, setExtra] = React.useState("");
|
|
269
|
+
const [extraMsg, setExtraMsg] = React.useState("");
|
|
270
|
+
const [extraBad, setExtraBad] = React.useState(false);
|
|
271
|
+
const [channel, setChannel] = React.useState("");
|
|
272
|
+
// 搬迁:探针 / 复制 / 状态
|
|
273
|
+
const [mvTo, setMvTo] = React.useState("");
|
|
274
|
+
const [mvBusy, setMvBusy] = React.useState(false);
|
|
275
|
+
const [mvMsg, setMvMsg] = React.useState("");
|
|
276
|
+
const [mvBad, setMvBad] = React.useState(false);
|
|
277
|
+
const [mvProbe, setMvProbe] = React.useState(null);
|
|
278
|
+
const [mvStatus, setMvStatus] = React.useState(null);
|
|
279
|
+
// 删除旧副本的确认词。
|
|
280
|
+
const [delWord, setDelWord] = React.useState("");
|
|
281
|
+
|
|
282
|
+
const load = React.useCallback(() => {
|
|
283
|
+
if (state.last) setData(state.last);
|
|
284
|
+
setBusy(true);
|
|
285
|
+
setErr("");
|
|
286
|
+
(async () => {
|
|
287
|
+
let d = null;
|
|
288
|
+
let via = "";
|
|
289
|
+
try {
|
|
290
|
+
d = await rpc("status", {}, 30000);
|
|
291
|
+
via = "载体(/api)";
|
|
292
|
+
} catch (e1) {
|
|
293
|
+
try {
|
|
294
|
+
d = await fetchStatus();
|
|
295
|
+
via = "HTTP 回退";
|
|
296
|
+
} catch (e2) {
|
|
297
|
+
const msg = "载体(" + carrierKind() + "):" + String(e1 && e1.message ? e1.message : e1)
|
|
298
|
+
+ ";HTTP 回退:" + String(e2 && e2.message ? e2.message : e2);
|
|
299
|
+
diag.lastError = msg;
|
|
300
|
+
setErr(msg);
|
|
301
|
+
setBusy(false);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
state.last = d;
|
|
306
|
+
setData(d);
|
|
307
|
+
setChannel(via);
|
|
308
|
+
setExtra(typeof d.extraPrompt === "string" ? d.extraPrompt : "");
|
|
309
|
+
try { setMvStatus(await rpc("move.status", {}, 8000)); } catch (e) { setMvStatus(null); }
|
|
310
|
+
setBusy(false);
|
|
311
|
+
})();
|
|
312
|
+
}, []);
|
|
313
|
+
|
|
314
|
+
const buildTree = React.useCallback(() => {
|
|
315
|
+
setWorking(true); setSetupBad(false); setSetupMsg("");
|
|
316
|
+
post("setup", { parent: parent }, 120000)
|
|
317
|
+
.then((r) => {
|
|
318
|
+
if (r && r.ok) {
|
|
319
|
+
setCreated(r.root || "");
|
|
320
|
+
setSetupMsg("已建立:目录 " + (r.created || []).length + " 个、文件 " + (r.written || []).length + " 个。下一步:注册为工作区。");
|
|
321
|
+
} else {
|
|
322
|
+
setSetupBad(true);
|
|
323
|
+
setSetupMsg((r && r.error) || "建立失败");
|
|
324
|
+
}
|
|
325
|
+
})
|
|
326
|
+
.catch((e) => { setSetupBad(true); setSetupMsg("请求未完成:" + (e && e.message ? e.message : String(e))); })
|
|
327
|
+
.then(() => setWorking(false));
|
|
328
|
+
}, [parent]);
|
|
329
|
+
|
|
330
|
+
const registerWs = React.useCallback(() => {
|
|
331
|
+
if (!created) return;
|
|
332
|
+
setWorking(true); setSetupBad(false); setSetupMsg("");
|
|
333
|
+
post("register", { path: created, title: "全局" }, 20000)
|
|
334
|
+
.then((r) => {
|
|
335
|
+
if (r && r.ok) setSetupMsg("已注册工作区:" + (r.title || "全局") + "(" + (r.id || "?") + ")。新建一个标题含「全局」的会话即可接入。");
|
|
336
|
+
else { setSetupBad(true); setSetupMsg(((r && r.error) || "注册失败") + (r && r.hint ? ";" + r.hint : "")); }
|
|
337
|
+
})
|
|
338
|
+
.catch((e) => { setSetupBad(true); setSetupMsg("请求未完成:" + (e && e.message ? e.message : String(e))); })
|
|
339
|
+
.then(() => setWorking(false));
|
|
340
|
+
}, [created]);
|
|
341
|
+
|
|
342
|
+
const saveExtra = React.useCallback(() => {
|
|
343
|
+
setWorking(true); setExtraBad(false); setExtraMsg("");
|
|
344
|
+
post("extra-prompt", { text: extra }, 20000)
|
|
345
|
+
.then((r) => {
|
|
346
|
+
if (r && r.ok) {
|
|
347
|
+
if (r.unchanged) setExtraMsg(r.note || "内容未变,已跳过写入。");
|
|
348
|
+
else setExtraMsg((r.chars ? "已保存,共 " + r.chars + " 字符,位置在工具说明之前。" : "已清空,不再注册提示段。") + (r.cacheNote ? " " + r.cacheNote : ""));
|
|
349
|
+
}
|
|
350
|
+
else { setExtraBad(true); setExtraMsg((r && r.error) || "保存失败"); }
|
|
351
|
+
})
|
|
352
|
+
.catch((e) => { setExtraBad(true); setExtraMsg("请求未完成:" + (e && e.message ? e.message : String(e))); })
|
|
353
|
+
.then(() => setWorking(false));
|
|
354
|
+
}, [extra]);
|
|
355
|
+
|
|
356
|
+
// 会话角色
|
|
357
|
+
const [roleSel, setRoleSel] = React.useState({});
|
|
358
|
+
const [roleStage, setRoleStage] = React.useState({});
|
|
359
|
+
const [roleMsg, setRoleMsg] = React.useState("");
|
|
360
|
+
const [roleBad, setRoleBad] = React.useState(false);
|
|
361
|
+
|
|
362
|
+
const setRoleValue = React.useCallback((sid, patch) => {
|
|
363
|
+
setRoleSel((prev) => {
|
|
364
|
+
const next = Object.assign({}, prev || {});
|
|
365
|
+
next[sid] = Object.assign({}, next[sid] || {}, patch);
|
|
366
|
+
return next;
|
|
367
|
+
});
|
|
368
|
+
// 改了角色就回到第一步,按钮重新显示「设置角色」。
|
|
369
|
+
setRoleStage((prev) => {
|
|
370
|
+
const next = Object.assign({}, prev || {});
|
|
371
|
+
next[sid] = "";
|
|
372
|
+
return next;
|
|
373
|
+
});
|
|
374
|
+
}, []);
|
|
375
|
+
|
|
376
|
+
const submitRole = React.useCallback((sid, role, project, task, wake) => {
|
|
377
|
+
setWorking(true); setRoleBad(false); setRoleMsg("");
|
|
378
|
+
post("role", { session_id: sid, role: role, project: project, task: task, wake: wake }, 30000)
|
|
379
|
+
.then((r) => {
|
|
380
|
+
if (r && r.ok) {
|
|
381
|
+
const liveText = r.live ? "已在当前会话生效" : "自下一轮起生效";
|
|
382
|
+
const sendText = wake
|
|
383
|
+
? (r.delivered ? "整理指令已送达,该会话将被唤醒一轮" : ("整理指令未送达" + (r.error ? ":" + r.error : "")))
|
|
384
|
+
: "";
|
|
385
|
+
setRoleMsg("角色已设为" + (r.role || role) + "," + liveText + (sendText ? ";" + sendText : "。"));
|
|
386
|
+
setRoleStage((prev) => {
|
|
387
|
+
const next = Object.assign({}, prev || {});
|
|
388
|
+
next[sid] = wake ? "notified" : "set";
|
|
389
|
+
return next;
|
|
390
|
+
});
|
|
391
|
+
load();
|
|
392
|
+
} else {
|
|
393
|
+
setRoleBad(true);
|
|
394
|
+
setRoleMsg((r && r.error) || "设置失败");
|
|
395
|
+
}
|
|
396
|
+
})
|
|
397
|
+
.catch((e) => { setRoleBad(true); setRoleMsg("请求未完成:" + (e && e.message ? e.message : String(e))); })
|
|
398
|
+
.then(() => setWorking(false));
|
|
399
|
+
}, [load]);
|
|
400
|
+
|
|
401
|
+
// 搬迁:探针 → 复制并校验 → 状态 → 清旧副本
|
|
402
|
+
const refreshMove = React.useCallback(() => {
|
|
403
|
+
rpc("move.status", {}, 10000).then((st) => setMvStatus(st)).catch(() => {});
|
|
404
|
+
}, []);
|
|
405
|
+
|
|
406
|
+
const callMove = React.useCallback((method, payload, ms) => {
|
|
407
|
+
setMvBusy(true); setMvBad(false); setMvMsg("");
|
|
408
|
+
return rpc(method, payload, ms || 20000)
|
|
409
|
+
.then((v) => v)
|
|
410
|
+
.catch((e) => { setMvBad(true); setMvMsg(String(e && e.message ? e.message : e)); throw e; })
|
|
411
|
+
.then((v) => v, () => null)
|
|
412
|
+
.then((v) => { setMvBusy(false); return v; });
|
|
413
|
+
}, []);
|
|
414
|
+
|
|
415
|
+
const doProbe = React.useCallback(() => {
|
|
416
|
+
callMove("move.probe", { to: mvTo }, 90000).then((p) => {
|
|
417
|
+
if (!p) return;
|
|
418
|
+
setMvProbe(p);
|
|
419
|
+
setMvBad(!p.ok);
|
|
420
|
+
setMvMsg(p.ok ? "预检通过,可以开始复制。" : "预检未通过,原因见下。");
|
|
421
|
+
});
|
|
422
|
+
}, [mvTo, callMove]);
|
|
423
|
+
|
|
424
|
+
const doCopy = React.useCallback(() => {
|
|
425
|
+
callMove("move.start", { to: mvTo }, 900000).then((r) => {
|
|
426
|
+
if (!r) return;
|
|
427
|
+
setMvBad(false);
|
|
428
|
+
setMvMsg("复制与校验通过:共 " + r.report.files + " 个文件、" + fmtBytes(r.report.bytes) + "。切换脚本已生成。");
|
|
429
|
+
refreshMove();
|
|
430
|
+
});
|
|
431
|
+
}, [mvTo, callMove, refreshMove]);
|
|
432
|
+
|
|
433
|
+
const doSwitch = React.useCallback(() => {
|
|
434
|
+
callMove("move.switch", {}, 30000).then((r) => {
|
|
435
|
+
if (!r) return;
|
|
436
|
+
setMvBad(false);
|
|
437
|
+
setMvMsg(r.already
|
|
438
|
+
? "当前已处于切换完成状态(原路径为链接),无需再次切换。"
|
|
439
|
+
: "已就绪:请完全退出 DSH,退出后约 1 秒完成切换,等待 10 秒后再打开。日志:" + (r.log || ""));
|
|
440
|
+
refreshMove();
|
|
441
|
+
});
|
|
442
|
+
}, [callMove, refreshMove]);
|
|
443
|
+
|
|
444
|
+
// 压缩授权:只有用户在这里点一下,压缩才排得进去(AI 不能自己批)
|
|
445
|
+
const [csSid, setCsSid] = React.useState("");
|
|
446
|
+
const [csMsg, setCsMsg] = React.useState("");
|
|
447
|
+
const doGrant = React.useCallback(() => {
|
|
448
|
+
setWorking(true); setCsMsg("");
|
|
449
|
+
post("consent.grant", { session_id: csSid }, 15000)
|
|
450
|
+
.then((r) => {
|
|
451
|
+
if (r && r.ok) { setCsMsg("已授权会话 " + r.sessionId + ",5 分钟内可压缩一次,用后失效。"); load(); }
|
|
452
|
+
else setCsMsg("授权未完成:" + ((r && r.error) || "原因未知"));
|
|
453
|
+
})
|
|
454
|
+
.catch((e) => setCsMsg("请求未完成:" + (e && e.message ? e.message : String(e))))
|
|
455
|
+
.then(() => setWorking(false));
|
|
456
|
+
}, [csSid, load]);
|
|
457
|
+
const doGrantSid = React.useCallback((sid) => {
|
|
458
|
+
setWorking(true); setCsMsg("");
|
|
459
|
+
post("consent.grant", { session_id: sid }, 15000)
|
|
460
|
+
.then((r) => {
|
|
461
|
+
if (r && r.ok) { setCsMsg("已授权会话 " + r.sessionId + ",5 分钟内可压缩一次,用后失效。"); load(); }
|
|
462
|
+
else setCsMsg("授权未完成:" + ((r && r.error) || "原因未知"));
|
|
463
|
+
})
|
|
464
|
+
.catch((e) => setCsMsg("请求未完成:" + (e && e.message ? e.message : String(e))))
|
|
465
|
+
.then(() => setWorking(false));
|
|
466
|
+
}, [load]);
|
|
467
|
+
|
|
468
|
+
const doRevoke = React.useCallback((sid) => {
|
|
469
|
+
setWorking(true); setCsMsg("");
|
|
470
|
+
post("consent.revoke", { session_id: sid }, 15000)
|
|
471
|
+
.then(() => { setCsMsg("已撤销 " + sid + " 的压缩授权。"); load(); })
|
|
472
|
+
.catch(() => { /* 撤销失败不影响别的 */ })
|
|
473
|
+
.then(() => setWorking(false));
|
|
474
|
+
}, [load]);
|
|
475
|
+
|
|
476
|
+
const doRetarget = React.useCallback(() => {
|
|
477
|
+
callMove("move.retarget-stage", {}, 20000).then((r) => {
|
|
478
|
+
if (!r) return;
|
|
479
|
+
if (r.already) { setMvMsg("DSH 登记已在新位置,无需修改登记。"); return; }
|
|
480
|
+
setMvBad(false);
|
|
481
|
+
setMvMsg("已就绪:DSH 退出后改写登记,每份先备份,重启后生效。日志:" + ((r.script && r.script.log) || ""));
|
|
482
|
+
refreshMove();
|
|
483
|
+
});
|
|
484
|
+
}, [callMove, refreshMove]);
|
|
485
|
+
|
|
486
|
+
const doRepair = React.useCallback(() => {
|
|
487
|
+
callMove("move.repair", {}, 20000).then((r) => {
|
|
488
|
+
if (!r) return;
|
|
489
|
+
setMvBad(false);
|
|
490
|
+
setMvMsg(r.already ? "原路径已经是链接,无需重建。" : "已重建链接:" + r.src + " 现指向 " + r.dst + "(未改动任何文件)");
|
|
491
|
+
refreshMove();
|
|
492
|
+
});
|
|
493
|
+
}, [callMove, refreshMove]);
|
|
494
|
+
|
|
495
|
+
const doCleanup = React.useCallback(() => {
|
|
496
|
+
if (String(delWord || "").trim() !== "删除") {
|
|
497
|
+
setMvBad(true);
|
|
498
|
+
setMvMsg("确认词不正确。请输入「删除」两个字后再执行。");
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
callMove("move.cleanup", { confirm: true }, 180000).then((r) => {
|
|
502
|
+
if (!r) return;
|
|
503
|
+
setMvBad(false);
|
|
504
|
+
setDelWord("");
|
|
505
|
+
setMvMsg(r.note + ((r.removed || []).length ? ":" + r.removed.join("、") : ""));
|
|
506
|
+
refreshMove();
|
|
507
|
+
});
|
|
508
|
+
}, [delWord, callMove, refreshMove]);
|
|
509
|
+
|
|
510
|
+
// 有缓存时先出一帧旧读数,再拉新数据;没有缓存才等一次。
|
|
511
|
+
React.useEffect(() => { installCss(); load(); }, []);
|
|
512
|
+
|
|
513
|
+
const head = h("div", { className: "dshh-head" },
|
|
514
|
+
h("h2", { className: "dshh-title" }, "dsh-handover"),
|
|
515
|
+
h("button", { className: "dshh-btn", onClick: load, disabled: busy, type: "button" }, busy ? (data ? "正在刷新" : "正在读取") : "刷新"),
|
|
516
|
+
h("span", { className: "dshh-muted" }, "数据通道 " + (channel || carrierKind()) + (diag.lastError ? ";上次失败 " + diag.lastError.slice(0, 90) : "")),
|
|
517
|
+
data ? h("span", { className: "dshh-muted" }, "读数时间 " + data.generatedAt) : null);
|
|
518
|
+
|
|
519
|
+
if (err && !data) {
|
|
520
|
+
return h("div", { className: "dshh-root" }, head,
|
|
521
|
+
h("div", { className: "dshh-bad" }, "无法取得状态:" + err),
|
|
522
|
+
h("div", { className: "dshh-muted" }, "若插件刚安装,请确认已在配置中启用本插件,并已重启 DSH。"));
|
|
523
|
+
}
|
|
524
|
+
if (!data) {
|
|
525
|
+
return h("div", { className: "dshh-root" }, head, h("div", { className: "dshh-muted" }, "正在读取状态数据。"));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const p = data.plugin || {};
|
|
529
|
+
const ws = data.workspace || {};
|
|
530
|
+
const gate = data.gate;
|
|
531
|
+
const ho = data.handover;
|
|
532
|
+
const ss = data.sessions;
|
|
533
|
+
const allSessions = (data.sessions && Array.isArray(data.sessions.all)) ? data.sessions.all : [];
|
|
534
|
+
const byActivity = allSessions.slice().sort((a, b) => String(b.seen || "").localeCompare(String(a.seen || "")));
|
|
535
|
+
|
|
536
|
+
// 排序原则:进来大概率要做的事在前,只读数据居中,说明文字最后。
|
|
537
|
+
const sections = [];
|
|
538
|
+
|
|
539
|
+
// 一、首次安装:仅在尚未接入工作区时出现,此时它是唯一可做的事。
|
|
540
|
+
const setup = data.setup;
|
|
541
|
+
const notReady = !ws || ws.found === false;
|
|
542
|
+
if (setup && notReady && (setup.steps || []).length) {
|
|
543
|
+
const msg = setupMsg ? h("div", { className: setupBad ? "dshh-bad" : "dshh-ok" }, setupMsg) : null;
|
|
544
|
+
sections.push(card("首次安装",
|
|
545
|
+
h("div", null,
|
|
546
|
+
h("div", { className: "dshh-muted" }, "插件已加载,工具与本页可用;闸门、交接体检与会话水位在接入工作区后显示。"),
|
|
547
|
+
h("ol", { className: "dshh-list" }, setup.steps.map((s, i) => h("li", { key: i }, s))),
|
|
548
|
+
h("input", {
|
|
549
|
+
className: "dshh-input",
|
|
550
|
+
type: "text",
|
|
551
|
+
value: parent,
|
|
552
|
+
placeholder: "已存在目录的绝对路径,_Workspace 将建在其内",
|
|
553
|
+
onChange: (e) => setParent(e.target.value),
|
|
554
|
+
}),
|
|
555
|
+
h("div", { className: "dshh-row" },
|
|
556
|
+
h("button", { className: "dshh-btn", type: "button", disabled: working || !parent, onClick: buildTree }, working ? "正在建立" : "建立全局文件体系"),
|
|
557
|
+
created ? h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: registerWs }, "注册为工作区") : null),
|
|
558
|
+
created ? h("div", { className: "dshh-code" }, created) : null,
|
|
559
|
+
msg)));
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// 二、压缩授权:唯一合法的「用户同意」入口,置顶。
|
|
563
|
+
const consentRows = Array.isArray(data.consent) ? data.consent : [];
|
|
564
|
+
sections.push(card("压缩授权",
|
|
565
|
+
h("div", null,
|
|
566
|
+
h("div", { className: "dshh-muted" }, "压缩须由你授权:点「允许」后,该会话可在 5 分钟内压缩一次,用后失效。也可在对话中发送 /allow-compact。"),
|
|
567
|
+
h("div", { className: "dshh-pick" },
|
|
568
|
+
byActivity.slice(0, 10).map((r, i) => h("button", {
|
|
569
|
+
className: "dshh-btn",
|
|
570
|
+
type: "button",
|
|
571
|
+
key: i,
|
|
572
|
+
disabled: working,
|
|
573
|
+
title: r.title || r.sid,
|
|
574
|
+
onClick: () => doGrantSid(r.sid),
|
|
575
|
+
}, "允许 " + shortSid(r.sid)))),
|
|
576
|
+
h("div", { className: "dshh-row" },
|
|
577
|
+
h("input", {
|
|
578
|
+
className: "dshh-inputSm",
|
|
579
|
+
type: "text",
|
|
580
|
+
value: csSid,
|
|
581
|
+
placeholder: "或输入会话 id 或 8 位短号",
|
|
582
|
+
onChange: (e) => setCsSid(e.target.value),
|
|
583
|
+
}),
|
|
584
|
+
h("button", { className: "dshh-btn", type: "button", disabled: working || !csSid, onClick: doGrant }, working ? "正在授权" : "授权")),
|
|
585
|
+
consentRows.length
|
|
586
|
+
? h("table", { className: "dshh-table" },
|
|
587
|
+
h("colgroup", null, h("col", { style: { width: "62%" } }), h("col", { style: { width: "18%" } }), h("col", { style: { width: "20%" } })),
|
|
588
|
+
h("thead", null, h("tr", null, h("th", null, "已授权会话"), h("th", null, "剩余"), h("th", null, "操作"))),
|
|
589
|
+
h("tbody", null, consentRows.map((r, i) => h("tr", { key: i },
|
|
590
|
+
h("td", { className: "dshh-code", title: r.sessionId }, shortSid(r.sessionId)),
|
|
591
|
+
h("td", { className: "dshh-num" }, Math.round((r.leftMs || 0) / 1000) + " 秒"),
|
|
592
|
+
h("td", null, h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: () => doRevoke(r.sessionId) }, "撤销"))))))
|
|
593
|
+
: h("div", { className: "dshh-muted" }, "当前无有效授权。"),
|
|
594
|
+
csMsg ? h("div", { className: "dshh-ok" }, csMsg) : null)));
|
|
595
|
+
|
|
596
|
+
// 三、移动全局根目录:常态折叠为一行状态。
|
|
597
|
+
{
|
|
598
|
+
const st = mvStatus;
|
|
599
|
+
const phaseLabel = {
|
|
600
|
+
none: "尚无迁移记录",
|
|
601
|
+
verified: "已复制并校验,等待切换",
|
|
602
|
+
switched: "已切换,原路径为链接",
|
|
603
|
+
"switched+cleaned": "已切换,旧副本已删除",
|
|
604
|
+
partial: "校验有差异,需重新复制",
|
|
605
|
+
};
|
|
606
|
+
const attention = !!(st && (st.linkBroken || st.srcExists === false || st.phase === "verified" || st.phase === "partial" || (st.retarget && st.retarget.needed) || (st.phase === "switched" && (st.olds || []).length)));
|
|
607
|
+
const stateText = st ? "状态:" + (phaseLabel[st.phase] || st.phase) + (st.dst ? "(" + st.dst + ")" : "") : "正在读取迁移状态";
|
|
608
|
+
const kids = [];
|
|
609
|
+
kids.push(h("div", { className: "dshh-muted" }, "复制并校验通过后才能切换;切换只改名与建立链接,不改动文件内容。"));
|
|
610
|
+
if (st && st.phase && st.phase.indexOf("switched") === 0) {
|
|
611
|
+
kids.push(h("div", { className: "dshh-muted" }, "原路径现为链接,请勿删除;若已删除,用「重建链接」恢复。"));
|
|
612
|
+
}
|
|
613
|
+
if (st && (st.srcExists === false || st.linkBroken)) {
|
|
614
|
+
kids.push(h("div", { className: "dshh-bad" }, st.linkBroken
|
|
615
|
+
? "链接指向的新位置不存在:" + (st.dst || "") + "。请先确认真实数据位置;本页不改动文件。"
|
|
616
|
+
: "原路径不存在(可能已被删除):" + (st.src || "") + "。数据仍在 " + (st.dst || "") + "。"));
|
|
617
|
+
if (st.canRepair) {
|
|
618
|
+
kids.push(h("div", { className: "dshh-row" },
|
|
619
|
+
h("button", { className: "dshh-btn", type: "button", disabled: mvBusy, onClick: doRepair }, "重建链接")));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (st && st.phase === "verified" && st.script) {
|
|
623
|
+
kids.push(h("div", { className: "dshh-sub" }, "切换步骤"));
|
|
624
|
+
kids.push(h("ol", { className: "dshh-list" },
|
|
625
|
+
h("li", null, "点击「准备切换」,助手进入待命状态。"),
|
|
626
|
+
h("li", null, "完全退出 DSH,含托盘与后台进程。"),
|
|
627
|
+
h("li", null, "退出后自动执行,约 50 毫秒。"),
|
|
628
|
+
h("li", null, "等待 10 秒后再打开 DSH。"),
|
|
629
|
+
h("li", null, "回到本页,状态应为「已切换,原路径为链接」。")));
|
|
630
|
+
kids.push(h("div", { className: "dshh-row" },
|
|
631
|
+
h("button", { className: "dshh-btn", type: "button", disabled: mvBusy, onClick: doSwitch }, mvBusy ? "正在准备" : "准备切换(退出 DSH 后自动完成)")));
|
|
632
|
+
kids.push(h("div", { className: "dshh-muted" }, "日志末行出现「切换完成」即为成功;失败会写明原因且不改动文件,可重试。助手未运行时手工执行:" + ((st.script && st.script.helper) || "")));
|
|
633
|
+
}
|
|
634
|
+
if (st && st.retarget && st.retarget.needed) {
|
|
635
|
+
kids.push(h("div", { className: "dshh-warn" }, "DSH 登记仍指向旧路径:" + st.retarget.total + " 处,涉及 " + st.retarget.files.length + " 个文件。"));
|
|
636
|
+
kids.push(h("div", { className: "dshh-muted" }, "退出 DSH 时改写,每份先备份;重启后生效。"));
|
|
637
|
+
kids.push(h("div", { className: "dshh-row" },
|
|
638
|
+
h("button", { className: "dshh-btn", type: "button", disabled: mvBusy, onClick: doRetarget }, "把登记迁到新位置(退出 DSH 后生效)")));
|
|
639
|
+
for (const rf of (st.retarget.files || [])) {
|
|
640
|
+
kids.push(h("div", { className: "dshh-code dshh-muted" }, rf.path + " ×" + rf.count));
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (st && st.retarget && st.retarget.needed === false) {
|
|
644
|
+
kids.push(h("div", { className: "dshh-ok" }, "DSH 登记已指向新位置:" + (st.dst || "") + "。"));
|
|
645
|
+
}
|
|
646
|
+
if (st && st.phase === "switched" && (st.olds || []).length) {
|
|
647
|
+
kids.push(h("div", { className: "dshh-muted" }, "旧副本:" + st.olds.map((o) => o.path + "(" + fmtBytes(o.bytes) + ")").join("、") + "。删除后无法恢复。"));
|
|
648
|
+
kids.push(h("div", { className: "dshh-row" },
|
|
649
|
+
h("input", {
|
|
650
|
+
className: "dshh-inputSm",
|
|
651
|
+
type: "text",
|
|
652
|
+
value: delWord,
|
|
653
|
+
placeholder: "输入「删除」以确认",
|
|
654
|
+
onChange: (e) => setDelWord(e.target.value),
|
|
655
|
+
}),
|
|
656
|
+
h("button", { className: "dshh-btn", type: "button", disabled: mvBusy || delWord.trim() !== "删除", onClick: doCleanup }, "删除旧副本(不可撤销)")));
|
|
657
|
+
}
|
|
658
|
+
kids.push(h("div", { className: "dshh-row" },
|
|
659
|
+
h("input", {
|
|
660
|
+
className: "dshh-input",
|
|
661
|
+
type: "text",
|
|
662
|
+
value: mvTo,
|
|
663
|
+
placeholder: "新根目录的绝对路径",
|
|
664
|
+
onChange: (e) => setMvTo(e.target.value),
|
|
665
|
+
})));
|
|
666
|
+
kids.push(h("div", { className: "dshh-row" },
|
|
667
|
+
h("button", { className: "dshh-btn", type: "button", disabled: mvBusy || !mvTo, onClick: doProbe }, mvBusy ? "正在预检" : "预检"),
|
|
668
|
+
h("button", { className: "dshh-btn", type: "button", disabled: mvBusy || !mvTo, onClick: doCopy }, "复制并校验"),
|
|
669
|
+
h("button", { className: "dshh-btn", type: "button", disabled: mvBusy, onClick: refreshMove }, "刷新状态")));
|
|
670
|
+
if (mvProbe) {
|
|
671
|
+
kids.push(kv([
|
|
672
|
+
["源", h("span", { className: "dshh-code" }, mvProbe.src)],
|
|
673
|
+
["目标", h("span", { className: "dshh-code" }, mvProbe.dst)],
|
|
674
|
+
["总大小", mvProbe.files + " 文件 · " + fmtBytes(mvProbe.bytes)],
|
|
675
|
+
["目标盘可用", mvProbe.freeBytes === null || mvProbe.freeBytes === undefined ? "?" : fmtBytes(mvProbe.freeBytes)],
|
|
676
|
+
["结论", mvProbe.ok ? verdict("通过", true) : verdict("不通过", false)],
|
|
677
|
+
]));
|
|
678
|
+
for (const w of (mvProbe.warnings || [])) kids.push(h("div", { className: "dshh-warn" }, "注意:" + w));
|
|
679
|
+
for (const x of (mvProbe.problems || [])) kids.push(h("div", { className: "dshh-bad" }, "未通过:" + x));
|
|
680
|
+
}
|
|
681
|
+
if (mvMsg) kids.push(h("div", { className: mvBad ? "dshh-bad" : "dshh-ok" }, mvMsg));
|
|
682
|
+
sections.push(card("移动全局根目录",
|
|
683
|
+
h("div", null,
|
|
684
|
+
h("details", null,
|
|
685
|
+
h("summary", { className: "dshh-sum" }, (attention ? "需要处理:" : "") + stateText),
|
|
686
|
+
h("div", { className: "dshh-wrap" }, kids)))));
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// 四、会话角色:角色列即当前值,设置成功后按钮切换为「通知该会话」。
|
|
690
|
+
const roleRow = (r, i) => {
|
|
691
|
+
const draft = (roleSel && roleSel[r.sid]) || {};
|
|
692
|
+
const role = draft.role !== undefined ? draft.role : (r.role || "plain");
|
|
693
|
+
const project = draft.project !== undefined ? draft.project : (r.project || "");
|
|
694
|
+
const task = draft.task !== undefined ? draft.task : (r.task || "");
|
|
695
|
+
const wide = role === "project" || role === "misc";
|
|
696
|
+
const stage = (roleStage && roleStage[r.sid]) || "";
|
|
697
|
+
const action = stage === "notified"
|
|
698
|
+
? h("button", { className: "dshh-btn", type: "button", disabled: true }, "已通知")
|
|
699
|
+
: stage === "set"
|
|
700
|
+
? h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: () => submitRole(r.sid, role, project, task, true) }, "通知该会话")
|
|
701
|
+
: h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: () => submitRole(r.sid, role, project, task, false) }, "设置角色");
|
|
702
|
+
return h("tr", { key: i },
|
|
703
|
+
h("td", null, r.title || "(无标题)"),
|
|
704
|
+
h("td", { className: "dshh-code dshh-num" }, shortSid(r.sid)),
|
|
705
|
+
h("td", { className: "dshh-num" }, r.pct === null || r.pct === undefined ? "?" : r.pct.toFixed(1) + "%"),
|
|
706
|
+
h("td", null, h("select", {
|
|
707
|
+
className: "dshh-inputSm",
|
|
708
|
+
value: role,
|
|
709
|
+
onChange: (e) => setRoleValue(r.sid, { role: e.target.value }),
|
|
710
|
+
},
|
|
711
|
+
h("option", { value: "plain" }, "普通"),
|
|
712
|
+
h("option", { value: "global" }, "全局"),
|
|
713
|
+
h("option", { value: "project" }, "项目"),
|
|
714
|
+
h("option", { value: "misc" }, "杂项"))),
|
|
715
|
+
h("td", null, wide ? h("input", {
|
|
716
|
+
className: "dshh-inputSm",
|
|
717
|
+
type: "text",
|
|
718
|
+
value: role === "project" ? project : task,
|
|
719
|
+
placeholder: role === "project" ? "填写项目名" : "填写任务名",
|
|
720
|
+
onChange: (e) => setRoleValue(r.sid, role === "project" ? { project: e.target.value } : { task: e.target.value }),
|
|
721
|
+
}) : h("span", { className: "dshh-muted" }, "不需填写")),
|
|
722
|
+
h("td", null, h("div", { className: "dshh-pick" }, action)));
|
|
723
|
+
};
|
|
724
|
+
sections.push(card("会话角色",
|
|
725
|
+
h("div", null,
|
|
726
|
+
h("div", { className: "dshh-muted" }, "选定角色后点「设置角色」,当场生效;按钮随即变为「通知该会话」,再点一次即向它发送整理指令并唤醒一轮。"),
|
|
727
|
+
byActivity.length
|
|
728
|
+
? h(Folded, {
|
|
729
|
+
rows: byActivity,
|
|
730
|
+
pageSize: 5,
|
|
731
|
+
cols: ["会话", "短号", "水位", "角色", "项目或任务名", "操作"],
|
|
732
|
+
widths: ["30%", "9%", "7%", "12%", "20%", "22%"],
|
|
733
|
+
render: roleRow,
|
|
734
|
+
})
|
|
735
|
+
: h("div", { className: "dshh-muted" }, "暂无可设置角色的会话。空白会话不在此列出。"),
|
|
736
|
+
roleMsg ? h("div", { className: roleBad ? "dshh-bad" : "dshh-ok" }, roleMsg) : null)));
|
|
737
|
+
|
|
738
|
+
// 五、全局前置提示词:默认折叠,只显示已保存字数。
|
|
739
|
+
sections.push(card("全局前置提示词",
|
|
740
|
+
h("div", null,
|
|
741
|
+
h("div", { className: "dshh-muted" }, "只注入你填写的内容,不含插件约束;对全部会话生效,位置在工具说明之前。留空保存即不注册提示段。"),
|
|
742
|
+
h("details", null,
|
|
743
|
+
h("summary", { className: "dshh-sum" }, "已保存 " + (extra ? extra.length : 0) + " 字符,点击编辑"),
|
|
744
|
+
h("textarea", {
|
|
745
|
+
className: "dshh-textarea",
|
|
746
|
+
rows: 6,
|
|
747
|
+
value: extra,
|
|
748
|
+
placeholder: "例如:一律用中文;改动前说明改动范围;简单任务不必拆分。",
|
|
749
|
+
onChange: (e) => setExtra(e.target.value),
|
|
750
|
+
}),
|
|
751
|
+
h("div", { className: "dshh-row" },
|
|
752
|
+
h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: saveExtra }, working ? "正在保存" : "保存"),
|
|
753
|
+
h("span", { className: "dshh-muted" }, extra.length + " 字符")),
|
|
754
|
+
extraMsg ? h("div", { className: extraBad ? "dshh-bad" : "dshh-ok" }, extraMsg) : null))));
|
|
755
|
+
|
|
756
|
+
// 六、会话水位:只读数据区从这里开始。
|
|
757
|
+
if (ss) {
|
|
758
|
+
const byRecent = (ss.rows || []).slice().sort((a, b) => String(b.seen || "").localeCompare(String(a.seen || "")));
|
|
759
|
+
const waterRow = (r, i) => h("tr", { key: i },
|
|
760
|
+
h("td", null, r.project),
|
|
761
|
+
h("td", { className: "dshh-code dshh-num" }, shortSid(r.sid)),
|
|
762
|
+
h("td", { className: "dshh-num" }, r.pct === null || r.pct === undefined ? "?" : r.pct.toFixed(1) + "%"),
|
|
763
|
+
h("td", null, r.state),
|
|
764
|
+
h("td", null, r.active ? "活跃" : "沉默"),
|
|
765
|
+
h("td", { className: "dshh-muted dshh-num" }, shortTime(r.seen)));
|
|
766
|
+
sections.push(card("会话水位",
|
|
767
|
+
h("div", null,
|
|
768
|
+
h("div", { className: "dshh-muted" }, ss.verdict + "。活跃线 " + ss.cutoff + ",回溯 " + ss.silentHours + " 小时。"),
|
|
769
|
+
(ss.notify || []).length
|
|
770
|
+
? h("div", null, "本轮需通知:" + ss.notify.map((r) => r.project + "(" + shortSid(r.sid) + ")").join("、"))
|
|
771
|
+
: h("div", { className: "dshh-muted" }, "本轮无需通知。"),
|
|
772
|
+
byRecent.length
|
|
773
|
+
? h(Folded, {
|
|
774
|
+
rows: byRecent,
|
|
775
|
+
pageSize: 5,
|
|
776
|
+
cols: ["项目", "会话", "水位", "状态", "活跃", "最后活动"],
|
|
777
|
+
widths: ["22%", "13%", "9%", "9%", "9%", "38%"],
|
|
778
|
+
render: waterRow,
|
|
779
|
+
})
|
|
780
|
+
: h("div", { className: "dshh-muted" }, "暂无可显示的项目会话。"))));
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// 七、通用约束条闸门
|
|
784
|
+
if (gate) {
|
|
785
|
+
const rows = (gate.libraries || []).map((r, i) => h("tr", { key: i },
|
|
786
|
+
h("td", null, r.name),
|
|
787
|
+
h("td", null, r.titleOk ? "通过" : "缺"),
|
|
788
|
+
h("td", null, (r.lines - r.missing) + "/" + r.lines),
|
|
789
|
+
h("td", null, r.waterOk ? "通过" : "缺"),
|
|
790
|
+
h("td", null, verdict(r.ok ? "通过" : "未通过", r.ok))));
|
|
791
|
+
sections.push(card("通用约束条闸门",
|
|
792
|
+
gate.error
|
|
793
|
+
? h("div", { className: "dshh-bad" }, gate.error)
|
|
794
|
+
: h("div", null,
|
|
795
|
+
h("div", { className: "dshh-muted" }, gate.total + " 个库,正文 " + gate.bodyLines + " 行;" + gate.verdict),
|
|
796
|
+
h("table", { className: "dshh-table" },
|
|
797
|
+
h("thead", null, h("tr", null,
|
|
798
|
+
h("th", null, "库"), h("th", null, "标题"), h("th", null, "正文"), h("th", null, "水位自查"), h("th", null, "结论"))),
|
|
799
|
+
h("tbody", null, rows)))));
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// 八、交接文档体检:问题列只留问题本身,去掉括注与时间要求。
|
|
803
|
+
if (ho) {
|
|
804
|
+
const cleanReason = (x) => String(x || "").replace(/([^)]*)/g, "").replace(/\s+/g, " ").trim();
|
|
805
|
+
const prows = (ho.projects || []).map((r, i) => h("tr", { key: i },
|
|
806
|
+
h("td", null, r.name),
|
|
807
|
+
h("td", null, r.verdict === "通过" ? verdict("通过", true) : r.verdict === "缺节" ? h("span", { className: "dshh-warn" }, "缺节") : verdict(r.verdict, false)),
|
|
808
|
+
h("td", null, (r.reasons || []).map(cleanReason).filter(Boolean).join(";"))));
|
|
809
|
+
sections.push(card("交接文档体检",
|
|
810
|
+
h("div", null,
|
|
811
|
+
h("div", null, "全局 Global_Handover.md:", ho.global.verdict === "通过" ? verdict("通过", true) : verdict(ho.global.verdict, false),
|
|
812
|
+
(ho.global.reasons || []).length ? h("span", { className: "dshh-muted" }, (ho.global.reasons || []).map(cleanReason).filter(Boolean).join(";")) : null),
|
|
813
|
+
h("table", { className: "dshh-table" },
|
|
814
|
+
h("thead", null, h("tr", null, h("th", null, "项目"), h("th", null, "结论"), h("th", null, "问题"))),
|
|
815
|
+
h("tbody", null, prows)))));
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// 九、插件信息
|
|
819
|
+
sections.push(card("插件", kv([
|
|
820
|
+
["名称", h("span", { className: "dshh-code" }, p.name + "@" + p.version)],
|
|
821
|
+
["工具数", String(p.tools)],
|
|
822
|
+
["压缩调度器", p.scheduler],
|
|
823
|
+
["注入项", (p.inject || []).join("、")],
|
|
824
|
+
["初始化", (p.initErrors && p.initErrors.length) ? h("span", { className: "dshh-bad" }, p.initErrors.join(";")) : verdict("无错误", true)],
|
|
825
|
+
["工作区", ws.found ? h("span", { className: "dshh-code" }, ws.root) : verdict("未接入(未找到 _Control_Center)", false)],
|
|
826
|
+
])));
|
|
827
|
+
|
|
828
|
+
// 十、唤醒权守卫
|
|
829
|
+
const g = data.guardLast;
|
|
830
|
+
sections.push(card("唤醒权守卫",
|
|
831
|
+
g
|
|
832
|
+
? kv([
|
|
833
|
+
["结论", g.allow ? verdict("允许", true) : verdict("拒绝", false)],
|
|
834
|
+
["依据", g.source + (g.title ? ";会话标题 " + g.title : "")],
|
|
835
|
+
["会话", h("span", { className: "dshh-code" }, g.sid || "-")],
|
|
836
|
+
])
|
|
837
|
+
: h("div", { className: "dshh-muted" }, "本进程尚未收到唤醒请求,暂无判定。")));
|
|
838
|
+
|
|
839
|
+
if ((data.notes || []).length) {
|
|
840
|
+
sections.push(card("待处理", h("ul", { className: "dshh-list" }, data.notes.map((n, i) => h("li", { key: i }, n)))));
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// 末、使用说明:参考性质,放最后,默认折叠。
|
|
844
|
+
const gd = data.guide;
|
|
845
|
+
if (gd) {
|
|
846
|
+
const sit = (gd.situations || []).map((r, i) => h("tr", { key: i },
|
|
847
|
+
h("td", null, r.when),
|
|
848
|
+
h("td", null, r.say),
|
|
849
|
+
h("td", { className: "dshh-muted" }, r.what)));
|
|
850
|
+
const groups = (gd.groups || []).map((gr, i) => h("div", { key: i },
|
|
851
|
+
h("div", { className: "dshh-sub" }, gr.title),
|
|
852
|
+
h("table", { className: "dshh-table" }, h("tbody", null, (gr.tools || []).map((t, j) => h("tr", { key: j },
|
|
853
|
+
h("td", { className: "dshh-code" }, t[0]),
|
|
854
|
+
h("td", { className: "dshh-muted" }, t[1])))))));
|
|
855
|
+
sections.push(card("使用说明",
|
|
856
|
+
h("details", null,
|
|
857
|
+
h("summary", { className: "dshh-sum" }, "交互方式、常见请求、工具清单与约定"),
|
|
858
|
+
h("div", { className: "dshh-wrap" },
|
|
859
|
+
(gd.intro || []).map((t, i) => h("div", { key: i }, t)),
|
|
860
|
+
h("div", { className: "dshh-sub" }, "常见请求与对应动作"),
|
|
861
|
+
h("table", { className: "dshh-table" },
|
|
862
|
+
h("thead", null, h("tr", null, h("th", null, "时机"), h("th", null, "可以这样说"), h("th", null, "插件会执行的动作"))),
|
|
863
|
+
h("tbody", null, sit)),
|
|
864
|
+
groups,
|
|
865
|
+
h("div", { className: "dshh-sub" }, "约定"),
|
|
866
|
+
h("ul", { className: "dshh-list" }, (gd.rules || []).map((t, i) => h("li", { key: i }, t)))))));
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
return h("div", { className: "dshh-root" }, head, sections);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/** 把只读状态页注册进设置导航。 */
|
|
873
|
+
function apply(ctx) {
|
|
874
|
+
try { connection = (ctx && ctx.connection) || null; } catch (e) { connection = null; }
|
|
875
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
876
|
+
name: "settings.section",
|
|
877
|
+
id: "handover",
|
|
878
|
+
order: 100,
|
|
879
|
+
label: "dsh-handover",
|
|
880
|
+
}, HandoverSection));
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
exports.apply = apply;
|
|
884
|
+
exports.inject = inject;
|
|
885
|
+
return module.exports;
|
|
886
|
+
},
|
|
887
|
+
});
|