dsh-kingdom 2.0.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ASSETS_NOTICE.md +22 -22
- package/README.md +246 -243
- package/THIRD_PARTY_NOTICES.md +92 -92
- package/docs/DSH-KINGDOM-3.0-ANNOUNCEMENT.md +13 -0
- package/docs/V2.0-RELEASE-NOTES.md +30 -30
- package/docs/V2.0-USER-GUIDE.md +74 -74
- package/docs/V3.0-RELEASE-NOTES.md +21 -0
- package/docs/V3.0-USER-GUIDE.md +33 -0
- package/docs/V3.1-RELEASE-NOTES.md +76 -0
- package/docs/V3.2-RELEASE-NOTES.md +103 -0
- package/lib/core/binding.js +167 -60
- package/lib/core/binding.js.map +1 -1
- package/lib/core/db.js +582 -546
- package/lib/core/db.js.map +1 -1
- package/lib/core/delivery-ack.js +1356 -0
- package/lib/core/delivery-ack.js.map +1 -0
- package/lib/core/delivery-change.js +1237 -0
- package/lib/core/delivery-change.js.map +1 -0
- package/lib/core/owner-binding-intent.js +14 -25
- package/lib/core/owner-binding-intent.js.map +1 -1
- package/lib/core/owner-window.js +698 -23
- package/lib/core/owner-window.js.map +1 -1
- package/lib/core/task-service.js +144 -5
- package/lib/core/task-service.js.map +1 -1
- package/lib/core/territory.js +43 -9
- package/lib/core/territory.js.map +1 -1
- package/lib/gui/assets/characters/chancellor-idle.svg +67 -67
- package/lib/gui/assets/characters/chancellor-sleeping.svg +67 -67
- package/lib/gui/assets/characters/chancellor-thinking.svg +67 -67
- package/lib/gui/assets/characters/chancellor-working.svg +67 -67
- package/lib/gui/assets/characters/knight-redraw-r1-idle.svg +67 -67
- package/lib/gui/assets/characters/knight-redraw-r1-sleeping.svg +67 -67
- package/lib/gui/assets/characters/knight-redraw-r1-thinking.svg +67 -67
- package/lib/gui/assets/characters/knight-redraw-r1-working.svg +67 -67
- package/lib/gui/assets/characters/supervisor-idle.svg +67 -67
- package/lib/gui/assets/characters/supervisor-sleeping.svg +67 -67
- package/lib/gui/assets/characters/supervisor-thinking.svg +67 -67
- package/lib/gui/assets/characters/supervisor-working.svg +67 -67
- package/lib/gui/console-app.js +7 -1
- package/lib/gui/console-app.js.map +1 -1
- package/lib/gui/contract.js +6 -0
- package/lib/gui/contract.js.map +1 -1
- package/lib/gui/control-contract.js +3 -2
- package/lib/gui/control-contract.js.map +1 -1
- package/lib/gui/owner-control.js +114 -4
- package/lib/gui/owner-control.js.map +1 -1
- package/lib/gui/owner-ui.js +645 -183
- package/lib/gui/owner-ui.js.map +1 -1
- package/lib/gui/server.js +109 -109
- package/lib/gui/snapshot.js +127 -4
- package/lib/gui/snapshot.js.map +1 -1
- package/lib/gui/visual-assets.js +37 -21
- package/lib/gui/visual-assets.js.map +1 -1
- package/lib/gui/visual-system.js +269 -269
- package/lib/gui/workbench-ui.js +549 -136
- package/lib/gui/workbench-ui.js.map +1 -1
- package/lib/gui/workbench.js +133 -0
- package/lib/gui/workbench.js.map +1 -1
- package/lib/index.js +392 -40
- package/lib/index.js.map +1 -1
- package/lib/types/core/binding.d.ts +29 -0
- package/lib/types/core/db.d.ts +14 -0
- package/lib/types/core/delivery-ack.d.ts +688 -0
- package/lib/types/core/delivery-change.d.ts +381 -0
- package/lib/types/core/owner-binding-intent.d.ts +9 -1
- package/lib/types/core/owner-window.d.ts +237 -3
- package/lib/types/core/task-service.d.ts +22 -0
- package/lib/types/core/territory.d.ts +18 -3
- package/lib/types/gui/contract.d.ts +162 -1
- package/lib/types/gui/owner-control.d.ts +61 -1
- package/lib/types/gui/snapshot.d.ts +8 -0
- package/lib/types/gui/visual-assets.d.ts +16 -0
- package/lib/types/gui/workbench.d.ts +47 -1
- package/lib/types/index.d.ts +60 -1
- package/lib/types/worker/governed-executor.d.ts +15 -0
- package/lib/worker/governed-executor.js +21 -0
- package/lib/worker/governed-executor.js.map +1 -1
- package/package.json +80 -75
package/lib/gui/owner-ui.js
CHANGED
|
@@ -4,188 +4,650 @@ export function renderOwnerApp(nonce) {
|
|
|
4
4
|
throw new Error('invalid owner page nonce');
|
|
5
5
|
return OWNER_APP_HTML.replaceAll('__OWNER_NONCE__', nonce);
|
|
6
6
|
}
|
|
7
|
-
const OWNER_APP_HTML = String.raw `<!doctype html>
|
|
8
|
-
<html lang="zh-CN" data-theme="forest"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>王国管理 · dsh-Kingdom</title>
|
|
9
|
-
<style nonce="__OWNER_NONCE__">
|
|
10
|
-
:root{color-scheme:dark;--bg:#132920;--panel:#1d372b;--soft:#243f31;--ink:#f4ecda;--muted:#bec5b4;--line:#526346;--accent:#e4c483;--button:#f0d49a;--button-ink:#213123;font-family:system-ui,"Microsoft YaHei",sans-serif}
|
|
11
|
-
:root[data-theme=parchment]{color-scheme:light;--bg:#efe4cb;--panel:#faf3e3;--soft:#eee1c5;--ink:#392d21;--muted:#6e624d;--line:#b7a47d;--accent:#745326;--button:#674c28;--button-ink:#fff7e6}
|
|
12
|
-
:root[data-theme=night]{--bg:#101f35;--panel:#1b2c45;--soft:#223750;--ink:#edf1fa;--muted:#bac8dd;--line:#526a8a;--accent:#bbd2ee;--button:#c7dcef;--button-ink:#142239}
|
|
13
|
-
:root[data-theme=wine]{--bg:#301e29;--panel:#402a36;--soft:#4f3342;--ink:#f9e8e9;--muted:#d6b9c6;--line:#866573;--accent:#efc4b6;--button:#f0cbb6;--button-ink:#3b2430}
|
|
14
|
-
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);line-height:1.55}main{width:min(100% - 40px,1020px);margin:auto;padding:26px 0 64px}header{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:28px}a{color:var(--accent);text-underline-offset:4px}h1{font-size:clamp(1.7rem,4vw,2.5rem);font-weight:650;letter-spacing:.02em;margin:0 0 8px}h2{font-size:1.12rem;margin:0 0 12px}p{margin:8px 0}.eyebrow{letter-spacing:.15em;font-size:.76rem;color:var(--accent)}.muted,small{color:var(--muted)}.panel{padding:24px;border:1px solid var(--line);background:var(--panel);border-radius:14px;margin:18px 0;overflow-wrap:anywhere}.bar{display:flex;gap:16px;justify-content:space-between;align-items:start}.stack{display:grid;gap:15px}.fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}.field{display:grid;gap:7px;min-width:0}.field.wide{grid-column:1/-1}label{font-weight:600}input,select,textarea,button{font:inherit}input,select,textarea{width:100%;min-width:0;color:var(--ink);background:var(--bg);border:1px solid var(--line);border-radius:7px;padding:10px 12px}textarea{min-height:90px;resize:vertical}button{border:1px solid var(--line);padding:10px 18px;border-radius:8px;cursor:pointer;background:var(--soft);color:var(--ink)}button.primary{background:var(--button);color:var(--button-ink);border-color:var(--button);font-weight:700}button:disabled{opacity:.5;cursor:default}:focus-visible{outline:3px solid var(--accent);outline-offset:3px}fieldset{border:0;margin:0;padding:0;min-width:0}.actions{display:flex;gap:12px;flex-wrap:wrap;margin-top:20px}.status{padding:12px 0;min-height:30px;white-space:pre-wrap}.tag{display:inline-block;border:1px solid var(--line);border-radius:20px;padding:3px 10px;margin:4px 7px 0 0;font-size:.84rem}.changes{margin:0}.change{border-top:1px solid var(--line);padding:12px 0}.change dt{font-weight:700}.change dd{margin:6px 0 0;white-space:pre-wrap}.before{color:var(--muted)}.after{color:var(--accent)}code{display:block;white-space:pre-wrap;overflow-wrap:anywhere;background:var(--bg);padding:14px;border-radius:8px;font-size:.85rem}.ceiling-row{display:grid;grid-template-columns:minmax(0,1fr) 100px auto;gap:10px;align-items:end}.receipt-meta{font-size:.8rem;color:var(--muted);overflow-wrap:anywhere}[hidden]{display:none!important}#theme{width:auto}.title-note{max-width:70ch}.scope-summary{white-space:pre-line}.danger{border-color:var(--accent)}
|
|
15
|
-
@media(max-width:600px){main{width:calc(100% - 24px);padding-top:16px}header{align-items:start}.panel{padding:17px}.fields{grid-template-columns:minmax(0,1fr)}.bar{display:grid;gap:12px}.bar button{justify-self:start}.ceiling-row{grid-template-columns:minmax(0,1fr) 92px}.ceiling-row button{grid-column:1/-1;justify-self:start}.actions button{flex:1 1 auto}h1{font-size:1.85rem}}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<
|
|
26
|
-
</
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
7
|
+
const OWNER_APP_HTML = String.raw `<!doctype html>
|
|
8
|
+
<html lang="zh-CN" data-theme="forest"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>王国管理 · dsh-Kingdom</title>
|
|
9
|
+
<style nonce="__OWNER_NONCE__">
|
|
10
|
+
:root{color-scheme:dark;--bg:#132920;--panel:#1d372b;--soft:#243f31;--ink:#f4ecda;--muted:#bec5b4;--line:#526346;--accent:#e4c483;--button:#f0d49a;--button-ink:#213123;font-family:system-ui,"Microsoft YaHei",sans-serif}
|
|
11
|
+
:root[data-theme=parchment]{color-scheme:light;--bg:#efe4cb;--panel:#faf3e3;--soft:#eee1c5;--ink:#392d21;--muted:#6e624d;--line:#b7a47d;--accent:#745326;--button:#674c28;--button-ink:#fff7e6}
|
|
12
|
+
:root[data-theme=night]{--bg:#101f35;--panel:#1b2c45;--soft:#223750;--ink:#edf1fa;--muted:#bac8dd;--line:#526a8a;--accent:#bbd2ee;--button:#c7dcef;--button-ink:#142239}
|
|
13
|
+
:root[data-theme=wine]{--bg:#301e29;--panel:#402a36;--soft:#4f3342;--ink:#f9e8e9;--muted:#d6b9c6;--line:#866573;--accent:#efc4b6;--button:#f0cbb6;--button-ink:#3b2430}
|
|
14
|
+
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);line-height:1.55}main{width:min(100% - 40px,1020px);margin:auto;padding:26px 0 64px}header{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:28px}a{color:var(--accent);text-underline-offset:4px}h1{font-size:clamp(1.7rem,4vw,2.5rem);font-weight:650;letter-spacing:.02em;margin:0 0 8px}h2{font-size:1.12rem;margin:0 0 12px}p{margin:8px 0}.eyebrow{letter-spacing:.15em;font-size:.76rem;color:var(--accent)}.muted,small{color:var(--muted)}.panel{padding:24px;border:1px solid var(--line);background:var(--panel);border-radius:14px;margin:18px 0;overflow-wrap:anywhere}.bar{display:flex;gap:16px;justify-content:space-between;align-items:start}.stack{display:grid;gap:15px}.fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}.field{display:grid;gap:7px;min-width:0}.field.wide{grid-column:1/-1}label{font-weight:600}input,select,textarea,button{font:inherit}input,select,textarea{width:100%;min-width:0;color:var(--ink);background:var(--bg);border:1px solid var(--line);border-radius:7px;padding:10px 12px}textarea{min-height:90px;resize:vertical}button{border:1px solid var(--line);padding:10px 18px;border-radius:8px;cursor:pointer;background:var(--soft);color:var(--ink);min-height:44px}button.primary{background:var(--button);color:var(--button-ink);border-color:var(--button);font-weight:700}button:disabled{opacity:.5;cursor:default}:focus-visible{outline:3px solid var(--accent);outline-offset:3px}fieldset{border:0;margin:0;padding:0;min-width:0}.actions{display:flex;gap:12px;flex-wrap:wrap;margin-top:20px}.status{padding:12px 0;min-height:30px;white-space:pre-wrap}.tag{display:inline-block;border:1px solid var(--line);border-radius:20px;padding:3px 10px;margin:4px 7px 0 0;font-size:.84rem}.changes{margin:0}.change{border-top:1px solid var(--line);padding:12px 0}.change dt{font-weight:700}.change dd{margin:6px 0 0;white-space:pre-wrap}.before{color:var(--muted)}.after{color:var(--accent)}code{display:block;white-space:pre-wrap;overflow-wrap:anywhere;background:var(--bg);padding:14px;border-radius:8px;font-size:.85rem}.ceiling-row{display:grid;grid-template-columns:minmax(0,1fr) 100px auto;gap:10px;align-items:end}.receipt-meta{font-size:.8rem;color:var(--muted);overflow-wrap:anywhere}[hidden]{display:none!important}#theme{width:auto}.title-note{max-width:70ch}.scope-summary{white-space:pre-line}.danger{border-color:var(--accent)}
|
|
15
|
+
@media(max-width:600px){main{width:calc(100% - 24px);padding-top:16px}header{align-items:start}.panel{padding:17px}.fields{grid-template-columns:minmax(0,1fr)}.bar{display:grid;gap:12px}.bar button{justify-self:start}.ceiling-row{grid-template-columns:minmax(0,1fr) 92px}.ceiling-row button{grid-column:1/-1;justify-self:start}.actions button{flex:1 1 auto}h1{font-size:1.85rem}}
|
|
16
|
+
.evidence-weak{padding:10px 12px;border:1px solid var(--line);border-left:3px solid var(--accent);border-radius:8px;background:var(--soft);overflow-wrap:anywhere}
|
|
17
|
+
.evidence-weak strong{margin-right:4px}
|
|
18
|
+
.change-body{display:grid;gap:8px}
|
|
19
|
+
.change-body code{font-size:.82rem}
|
|
20
|
+
.change-body code[data-hunk=ADDED]{border-left:3px solid var(--accent)}
|
|
21
|
+
.change-body code[data-hunk=REMOVED]{opacity:.85;border-left:3px solid var(--line)}
|
|
22
|
+
.question-history-entry{text-align:left;width:100%;justify-self:start}
|
|
23
|
+
@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}}
|
|
24
|
+
</style></head><body><main>
|
|
25
|
+
<header><a href="/console#settings">← 返回工作台设置</a><label class="field" for="theme"><span class="muted">外观</span><select id="theme"><option value="forest">森林墨绿</option><option value="parchment">羊皮纸</option><option value="night">夜蓝星图</option><option value="wine">酒红议会</option></select></label></header>
|
|
26
|
+
<div class="eyebrow">KINGDOM / OWNER</div><h1>王国管理</h1><p class="title-note muted">在已授权范围内管理领地、角色与执行配置。每次变更先查看影响,再确认应用。</p>
|
|
27
|
+
<p id="status" class="status" role="status" aria-live="polite">正在检查管理窗口…</p>
|
|
28
|
+
<section id="activation" class="panel" hidden><h2>从本地打开管理窗口</h2><p>当前没有可用的管理授权。请由人类在 DSH 的直接命令入口打开一次管理窗口,再使用返回的短期链接。</p><p class="muted">普通工作台会话不会自动获得管理权;每个窗口都需要明确操作范围,并在十分钟内到期。</p><details><summary>首次初始化的本地命令</summary><p>只用于尚未建立王国的空库;初始化后需要按实际范围重新激活。</p><code>/kingdom owner.gui {"kingdomId":null,"actions":["init"],"scope":{"kingdomWide":false,"territoryIds":[],"bindingIds":[],"roleTypes":[],"targetSessionIds":[],"workspaceRoots":[]},"ttlMs":600000}</code></details></section>
|
|
29
|
+
<section id="window" class="panel" hidden><div class="bar"><div><h2>本次管理范围</h2><p id="scope" class="scope-summary"></p><p id="expiry" class="muted"></p></div><button id="revoke" class="danger" type="button">立即撤销管理窗口</button></div></section>
|
|
30
|
+
<section id="editor" class="panel" hidden><h2>准备变更</h2><form id="operation-form"><fieldset id="edit-fields"><div class="field"><label for="action">要进行的操作</label><select id="action" required></select></div><p id="action-note" class="muted"></p><div id="fields" class="fields"></div><div class="actions"><button id="prepare" class="primary" type="submit" title="先核对本次变更的准确影响,再决定是否提交。">查看变更预览</button></div></fieldset></form></section>
|
|
31
|
+
<section id="preview-panel" class="panel" hidden><h2>确认本次变更</h2><p id="preview-summary"></p><dl id="changes" class="changes"></dl><p id="preview-expiry" class="muted"></p><div class="actions"><button id="commit" class="primary" type="button" title="提交上面列出的准确变更;提交前会再次核对当前状态。">确认应用此变更</button><button id="edit-again" type="button" title="返回修改内容;修改后需要重新生成预览。">返回修改</button></div></section>
|
|
32
|
+
<section id="result-panel" class="panel" hidden><h2 id="result-title">操作结果</h2><p id="result-message"></p><p id="result-meta" class="receipt-meta"></p><div class="actions"><button id="lookup" type="button" hidden>查询本次操作结果</button><button id="next" type="button" hidden>准备下一项变更</button></div></section>
|
|
33
|
+
<section id="change-panel" class="panel" hidden><h2>主管确认的改动证据</h2><p class="muted">只读查看某条已由主管确认的改动。查看不写入任何事实,也不自动记为已知悉;它不是 Git 作者证据,也不改变任务或 Claim。</p><div id="change-body" class="change-body" aria-live="polite"></div></section>
|
|
34
|
+
<section id="question-thread" class="panel" hidden><h2>条目提问与主管回复</h2><p class="muted">只读查看这一条交付条目的提问与回复。读取不写入任何事实、不自动知悉、也不替你回复;未回复的问题只显示「待领取」,不表示主管已通知或已阅读。问答正文不进入工作台或任何通用事件投影。</p><div id="question-thread-body" class="change-body" aria-live="polite"></div></section>
|
|
35
|
+
<section id="question-history" class="panel" hidden><h2>问答历史(只读)</h2><p class="muted">这里列出本窗口授权范围内已有提问记录的条目,包括已离开当前交付目录、目前无法重验的旧问答。只读回看:不写入任何事实、不自动知悉,也不能作为新提问目标;新提问只能针对当前已确认交付条目。</p><div id="question-history-list" class="stack"></div></section>
|
|
36
|
+
</main><script nonce="__OWNER_NONCE__">
|
|
37
|
+
(() => {
|
|
38
|
+
'use strict';
|
|
39
|
+
const el = id => document.getElementById(id);
|
|
40
|
+
const state = { control: null, decisionId: null, preview: null, busy: false, uncertain: false, ackTarget: null, ackHintInvalid: false, askTarget: null, askHintInvalid: false, askThread: null, changeTarget: null, changeHintInvalid: false, questionLimit: 50, questionSeq: 0 };
|
|
41
|
+
/**
|
|
42
|
+
* 工作台知悉图标带入的准确条目引用(task/item/contentHash 三元组)。
|
|
43
|
+
* 这里只做格式校验与预选;它不构成授权,也不写入任何事实。
|
|
44
|
+
*
|
|
45
|
+
* 必须区分两种情形:完全没有 ack_* 参数时保持目录默认选择;一旦出现 ack_*
|
|
46
|
+
* 参数却非法(缺失、重复、超长或含非法字符),就按「目标非法」处理——
|
|
47
|
+
* 清空选择并禁用准备,绝不退回目录第一条充当兜底成功路径。
|
|
48
|
+
*/
|
|
49
|
+
const ackTargetFromLocation = () => {
|
|
50
|
+
try {
|
|
51
|
+
if (typeof location === 'undefined' || typeof location.search !== 'string') return { state: 'ABSENT' };
|
|
52
|
+
const query = location.search.startsWith('?') ? location.search.slice(1) : location.search;
|
|
53
|
+
if (!query) return { state: 'ABSENT' };
|
|
54
|
+
const values = {};
|
|
55
|
+
let present = false;
|
|
56
|
+
for (const part of query.split('&')) {
|
|
57
|
+
if (!part) continue;
|
|
58
|
+
const at = part.indexOf('=');
|
|
59
|
+
const key = decodeURIComponent(at < 0 ? part : part.slice(0, at));
|
|
60
|
+
if (!['ack_task','ack_item','ack_hash'].includes(key)) continue;
|
|
61
|
+
present = true;
|
|
62
|
+
if (Object.hasOwn(values,key)) return { state: 'INVALID' };
|
|
63
|
+
values[key] = at < 0 ? '' : decodeURIComponent(part.slice(at + 1));
|
|
64
|
+
}
|
|
65
|
+
if (!present) return { state: 'ABSENT' };
|
|
66
|
+
const safe = value => typeof value === 'string' && /^[A-Za-z0-9_.:@-]{1,96}$/u.test(value) ? value : null;
|
|
67
|
+
const taskId = safe(values.ack_task); const itemId = safe(values.ack_item); const contentHash = safe(values.ack_hash);
|
|
68
|
+
return taskId && itemId && contentHash ? { state: 'TARGET', target: { taskId, itemId, contentHash } } : { state: 'INVALID' };
|
|
69
|
+
} catch { return { state: 'INVALID' }; }
|
|
70
|
+
};
|
|
71
|
+
const ackHint = ackTargetFromLocation();
|
|
72
|
+
state.ackTarget = ackHint.state === 'TARGET' ? ackHint.target : null;
|
|
73
|
+
state.ackHintInvalid = ackHint.state === 'INVALID';
|
|
74
|
+
/**
|
|
75
|
+
* 工作台提问图标带入的准确条目引用(与知悉同款三元组,只是目标动作不同)。
|
|
76
|
+
*
|
|
77
|
+
* 纪律完全一致:没有 ask_* 参数时保持目录默认;一旦出现却缺失、重复或非法,
|
|
78
|
+
* 就清空选择并禁用准备,绝不退回目录第一条。提问与知悉互不触发。
|
|
79
|
+
*/
|
|
80
|
+
const askTargetFromLocation = () => {
|
|
81
|
+
try {
|
|
82
|
+
if (typeof location === 'undefined' || typeof location.search !== 'string') return { state: 'ABSENT' };
|
|
83
|
+
const query = location.search.startsWith('?') ? location.search.slice(1) : location.search;
|
|
84
|
+
if (!query) return { state: 'ABSENT' };
|
|
85
|
+
const values = {};
|
|
86
|
+
let present = false;
|
|
87
|
+
for (const part of query.split('&')) {
|
|
88
|
+
if (!part) continue;
|
|
89
|
+
const at = part.indexOf('=');
|
|
90
|
+
const key = decodeURIComponent(at < 0 ? part : part.slice(0, at));
|
|
91
|
+
if (!['ask_task','ask_item','ask_hash'].includes(key)) continue;
|
|
92
|
+
present = true;
|
|
93
|
+
if (Object.hasOwn(values,key)) return { state: 'INVALID' };
|
|
94
|
+
values[key] = at < 0 ? '' : decodeURIComponent(part.slice(at + 1));
|
|
95
|
+
}
|
|
96
|
+
if (!present) return { state: 'ABSENT' };
|
|
97
|
+
const safe = value => typeof value === 'string' && /^[A-Za-z0-9_.:@-]{1,96}$/u.test(value) ? value : null;
|
|
98
|
+
const taskId = safe(values.ask_task); const itemId = safe(values.ask_item); const contentHash = safe(values.ask_hash);
|
|
99
|
+
return taskId && itemId && contentHash ? { state: 'TARGET', target: { taskId, itemId, contentHash } } : { state: 'INVALID' };
|
|
100
|
+
} catch { return { state: 'INVALID' }; }
|
|
101
|
+
};
|
|
102
|
+
const askHint = askTargetFromLocation();
|
|
103
|
+
state.askTarget = askHint.state === 'TARGET' ? askHint.target : null;
|
|
104
|
+
state.askHintInvalid = askHint.state === 'INVALID';
|
|
105
|
+
// 只读线程面板的目标:预选提示或本窗口刚提交的条目;不参与任何授权判断。
|
|
106
|
+
state.askThread = state.askTarget ? { taskId: state.askTarget.taskId, itemId: state.askTarget.itemId } : null;
|
|
107
|
+
/**
|
|
108
|
+
* 工作台「查看改动」图标带入的精确引用(task/evidence/entry 三元组)。
|
|
109
|
+
*
|
|
110
|
+
* 与知悉提示同款纪律:完全没有 change_* 参数时不显示;一旦出现却缺失、重复或
|
|
111
|
+
* 非法,就按「目标非法」处理并明确不做替代选择,绝不退回目录里其他条目。
|
|
112
|
+
* 这些参数不构成授权:读取仍由服务端按当前 ACTIVE 窗口与 scope 判定。
|
|
113
|
+
*/
|
|
114
|
+
const changeTargetFromLocation = () => {
|
|
115
|
+
try {
|
|
116
|
+
if (typeof location === 'undefined' || typeof location.search !== 'string') return { state: 'ABSENT' };
|
|
117
|
+
const query = location.search.startsWith('?') ? location.search.slice(1) : location.search;
|
|
118
|
+
if (!query) return { state: 'ABSENT' };
|
|
119
|
+
const values = {};
|
|
120
|
+
let present = false;
|
|
121
|
+
for (const part of query.split('&')) {
|
|
122
|
+
if (!part) continue;
|
|
123
|
+
const at = part.indexOf('=');
|
|
124
|
+
const key = decodeURIComponent(at < 0 ? part : part.slice(0, at));
|
|
125
|
+
if (!['change_task','change_evidence','change_item'].includes(key)) continue;
|
|
126
|
+
present = true;
|
|
127
|
+
if (Object.hasOwn(values,key)) return { state: 'INVALID' };
|
|
128
|
+
values[key] = at < 0 ? '' : decodeURIComponent(part.slice(at + 1));
|
|
129
|
+
}
|
|
130
|
+
if (!present) return { state: 'ABSENT' };
|
|
131
|
+
const safeTask = value => typeof value === 'string' && /^[A-Za-z0-9_.:@-]{1,96}$/u.test(value) ? value : null;
|
|
132
|
+
const safeDigest = value => typeof value === 'string' && /^[0-9a-f]{64}$/u.test(value) ? value : null;
|
|
133
|
+
const taskId = safeTask(values.change_task);
|
|
134
|
+
const evidenceId = safeDigest(values.change_evidence);
|
|
135
|
+
const entryId = safeDigest(values.change_item);
|
|
136
|
+
return taskId && evidenceId && entryId ? { state: 'TARGET', target: { taskId, evidenceId, entryId } } : { state: 'INVALID' };
|
|
137
|
+
} catch { return { state: 'INVALID' }; }
|
|
138
|
+
};
|
|
139
|
+
const changeHint = changeTargetFromLocation();
|
|
140
|
+
state.changeTarget = changeHint.state === 'TARGET' ? changeHint.target : null;
|
|
141
|
+
state.changeHintInvalid = changeHint.state === 'INVALID';
|
|
142
|
+
const pendingKey = 'dsh-kingdom.owner.pending.v1';
|
|
143
|
+
let pendingInvalid = false;
|
|
144
|
+
try { const raw=sessionStorage.getItem(pendingKey); if(raw) { const value=JSON.parse(raw);
|
|
145
|
+
if(!value || !['decisionId','prepareId','operationId'].every(key=>typeof value[key]==='string' && value[key].length>0 && value[key].length<=768)) throw new Error('invalid pending reference');
|
|
146
|
+
state.decisionId=value.decisionId; state.preview={prepareId:value.prepareId,operationId:value.operationId}; state.uncertain=true;
|
|
147
|
+
} } catch { pendingInvalid=true; state.uncertain=true; }
|
|
148
|
+
function rememberPending() { const raw=JSON.stringify({decisionId:state.decisionId,prepareId:state.preview.prepareId,operationId:state.preview.operationId});
|
|
149
|
+
try { sessionStorage.setItem(pendingKey,raw); if(sessionStorage.getItem(pendingKey)!==raw) throw new Error(); }
|
|
150
|
+
catch { const error=new Error('浏览器无法保留操作编号,本次尚未发送。请允许此页面使用会话存储后重新预览。'); error.confirmedRejection=true; throw error; }
|
|
151
|
+
}
|
|
152
|
+
function clearPending() { try { sessionStorage.removeItem(pendingKey); } catch {} }
|
|
153
|
+
const names = { init:'初始化王国', 'territory.create':'创建领地', 'territory.update':'修改领地名称与说明', 'territory.supervisor':'任命领地主管', 'role.bind':'登记新角色', 'role.session':'调整角色会话', ceiling:'调整王国权限上限', 'execution-profile':'设置执行模型', 'budget.policy':'调整软预算政策', 'plan.adopt':'采纳一份协作计划', 'delivery.item.ack':'逐条记下已知悉交付' };
|
|
154
|
+
const roleNames = { CHANCELLOR:'宰相', SUPERVISOR:'主管', WORKER:'执行者' };
|
|
155
|
+
const notes = { init:'初始化只创建王国与人类所有者,完成后本次授权即结束。', 'territory.create':'选择已存在且在授权根目录内的工作目录;这里不会创建目录。', 'territory.update':'保留工作目录和已有会话关系。', 'territory.supervisor':'仍有未决执行或工作占用时,变更将被拒绝。', 'role.bind':'主管与宰相需要真实运行会话;执行者会话由正常执行流程建立。', 'role.session':'仅调整主管或宰相;不会迁移执行者的持久会话。', ceiling:'此映射将替换王国上限。请完整列出本次需要配置的能力;它不会改变已在途的运行限制。', 'execution-profile':'保存的是请求配置;只有后续真实运行才能确认实际使用的模型。', 'budget.policy':'软预算按 Token 管理新增执行,不是费用硬上限。每次启动的预留是估计;关闭不重置统计起点,调整不会终止在途工作或释放恢复中的占用。' };
|
|
156
|
+
notes['plan.adopt']='一次采纳完整计划及其固定版本。采纳只创建子任务;主管仍须合法分配、启动和审查,主执行者仍须实际整合。不会自动授权、派发或接受产物。';
|
|
157
|
+
notes['delivery.item.ack']='只对已被主管 ACCEPT 确认的交付条目生效,一次一条、只记当前内容版本,不采集理由或理解程度。它不改变任务状态、主管审查、正式 Owner acceptance 与发布状态。重复知悉同一版本不会新增记录。';
|
|
158
|
+
names['delivery.item.question']='就一条交付条目提问';
|
|
159
|
+
notes['delivery.item.question']='只对已被主管 ACCEPT 确认的交付条目生效,一次一条、只记当前内容版本。问题交给**接受该交付的主管**;如果原主管已退任、换了 session 或该领地已改绑,问题保持可见但明确不可达,不会自动转给继任者。提问不是知悉,也不改变任务状态、主管审查、Owner acceptance 与发布状态;重复提交同一条目的同一问题不会新增记录,回复由该主管在其 session-bound Agent Tool 中完成。';
|
|
160
|
+
const text = (id, value) => { el(id).textContent = String(value ?? ''); };
|
|
161
|
+
const status = value => text('status', value);
|
|
162
|
+
const show = (id, visible) => { el(id).hidden = !visible; };
|
|
163
|
+
const active = () => state.control?.decision?.state === 'ACTIVE' && Date.parse(state.control.decision.expiresAt) > Date.now();
|
|
164
|
+
function applyTheme(value) { const theme = ['forest','parchment','night','wine'].includes(value) ? value : 'forest'; document.documentElement.dataset.theme = theme; el('theme').value = theme; try { localStorage.setItem('dsh-kingdom.console.theme', theme); } catch {} }
|
|
165
|
+
let stored = 'forest'; try { stored = localStorage.getItem('dsh-kingdom.console.theme'); } catch {} applyTheme(stored);
|
|
166
|
+
el('theme').addEventListener('change', () => applyTheme(el('theme').value));
|
|
167
|
+
const make = (tag, value, className) => { const node = document.createElement(tag); if (value !== undefined) node.textContent = value; if (className) node.className = className; return node; };
|
|
168
|
+
const options = (select, values) => { select.replaceChildren(); for (const value of values) { const option = make('option', value.label); option.value = value.id; select.append(option); } };
|
|
169
|
+
function field(key, label, kind = 'text', choices, required = true) {
|
|
170
|
+
const box = make('div', undefined, 'field' + (kind === 'textarea' ? ' wide' : ''));
|
|
171
|
+
const input = make(kind === 'textarea' ? 'textarea' : kind === 'select' ? 'select' : 'input'); input.id = 'param-' + key; input.name = key; input.required = required;
|
|
172
|
+
if (input.tagName === 'INPUT') input.type = kind;
|
|
173
|
+
if (kind === 'select') options(input, choices || []);
|
|
174
|
+
if (kind === 'text' || kind === 'textarea') input.maxLength = kind === 'textarea' ? 4000 : 2000;
|
|
175
|
+
const title = make('label', label); title.htmlFor = input.id; box.append(title, input); el('fields').append(box); return input;
|
|
176
|
+
}
|
|
177
|
+
function allowedBindings(filter) { return (state.control.catalog?.bindings || []).filter(filter || (() => true)).map(item => ({id:item.id,label:(roleNames[item.roleType] || item.roleType) + ' · ' + item.roleName + ' · ' + item.id})); }
|
|
178
|
+
// v3.2.0:解除领地主理用的勾选框。它刻意不带 name(不进入通用字段收集),
|
|
179
|
+
// 只由 payload() 显式转成 supervisor_binding_id: null。
|
|
180
|
+
const releaseSupervisorChecked = () => { const box = el('fields').querySelector('[data-release-supervisor]'); return !!box && box.checked; };
|
|
181
|
+
function ceilingRow() {
|
|
182
|
+
const row = make('div', undefined, 'ceiling-row');
|
|
183
|
+
const box = make('label', '能力名称', 'field'); const input = make('input'); input.required = true; input.maxLength = 200; input.setAttribute('aria-label', '能力名称'); box.append(input);
|
|
184
|
+
const selectBox = make('label', '是否允许', 'field'); const select = make('select'); select.setAttribute('aria-label', '是否允许'); options(select,[{id:'true',label:'允许'},{id:'false',label:'禁止'}]); selectBox.append(select);
|
|
185
|
+
const remove = make('button','移除此项'); remove.type='button'; remove.addEventListener('click',()=>row.remove()); row.append(box,selectBox,remove); return row;
|
|
186
|
+
}
|
|
187
|
+
function renderFields() {
|
|
188
|
+
state.preview = null; state.uncertain = false; show('preview-panel',false); show('result-panel',false); el('fields').replaceChildren();
|
|
189
|
+
const action = el('action').value; const catalog = state.control.catalog || {}; text('action-note', notes[action]);
|
|
190
|
+
const territories = (catalog.territories || []).map(item => ({id:item.id,label:item.name + ' · ' + item.id}));
|
|
191
|
+
const sessions = (catalog.runtimeSessions || []).map(item => ({id:item.id,label:item.label + ' · ' + item.id}));
|
|
192
|
+
if (action === 'init') { field('kingdom_name','王国名称'); field('owner_name','所有者显示名'); }
|
|
193
|
+
if (action === 'territory.create') { field('name','领地名称'); field('workspace_path','已存在的工作目录'); field('summary','领地说明','textarea',undefined,false); const roots = make('p','允许的根目录:' + (catalog.workspaceRoots || []).join(';'),'muted'); roots.classList.add('wide'); el('fields').append(roots); }
|
|
194
|
+
if (action === 'territory.update') { field('territory_id','领地','select',territories); field('name','新名称(不改请留空)','text',undefined,false); field('summary','新说明(不改请留空)','textarea',undefined,false); }
|
|
195
|
+
if (action === 'territory.supervisor') {
|
|
196
|
+
field('territory_id','领地','select',territories);
|
|
197
|
+
const supervisor = field('supervisor_binding_id','主管','select',allowedBindings(item=>item.roleType==='SUPERVISOR'));
|
|
198
|
+
// v3.2.0:管理窗口此前无法表达“解除”,而 1:1 之下“换领地”必须先解除。
|
|
199
|
+
const releaseBox = make('label', undefined, 'field wide');
|
|
200
|
+
const release = make('input'); release.type='checkbox'; release.id='param-release-supervisor'; release.dataset.releaseSupervisor='true';
|
|
201
|
+
const releaseText = make('span','解除现任主理(不指派新主管)'); releaseBox.append(release, releaseText); el('fields').append(releaseBox);
|
|
202
|
+
const releaseHint = make('p','解除后该领地无主理:任何 Supervisor 都不能治理它(fail-closed),直到指派新的。1:1 之下“把主理换到另一个领地”就是“先在这里解除、再去那边指派”。','muted');
|
|
203
|
+
releaseHint.classList.add('wide'); el('fields').append(releaseHint);
|
|
204
|
+
const supervisorChoices = allowedBindings(item=>item.roleType==='SUPERVISOR');
|
|
205
|
+
const update = () => {
|
|
206
|
+
const releasing = release.checked;
|
|
207
|
+
supervisor.required = !releasing; supervisor.disabled = releasing; supervisor.parentElement.hidden = releasing;
|
|
208
|
+
releaseHint.hidden = false;
|
|
209
|
+
el('prepare').disabled = !releasing && !supervisorChoices.length;
|
|
210
|
+
};
|
|
211
|
+
release.addEventListener('change', update); update();
|
|
212
|
+
}
|
|
213
|
+
if (action === 'role.bind') {
|
|
214
|
+
const role = field('role_type','角色','select',(state.control.decision.scope.roleTypes || []).map(id=>({id,label:roleNames[id]}))); field('role_name','角色名称');
|
|
215
|
+
const session = field('session_id','运行会话','select',sessions);
|
|
216
|
+
// v3.2.0:主管必须同时隶属一个领地(席位与主理同一事务写入),因此这里强制选择领地;
|
|
217
|
+
// 其它角色不隶属领地,字段隐藏并禁用,不进入 payload。
|
|
218
|
+
const territory = field('territory_id','主理领地(主管必选)','select',territories);
|
|
219
|
+
const territoryHint = make('p', territories.length
|
|
220
|
+
? '领地和主管完全绑定:任命主管时会同时把该领地的主理设为此席位;领地当前已有主理时会被拒绝,请先解除现有主理再指派。'
|
|
221
|
+
: '本次授权范围内没有任何可用领地:无法任命主管。请先创建领地,或重新激活本包含目标领地的管理窗口。','muted');
|
|
222
|
+
territoryHint.classList.add('wide'); el('fields').append(territoryHint);
|
|
223
|
+
const update = () => {
|
|
224
|
+
const needed = role.value !== 'WORKER'; session.required = needed; session.disabled = !needed; session.parentElement.hidden = !needed;
|
|
225
|
+
const supervisor = role.value === 'SUPERVISOR';
|
|
226
|
+
territory.required = supervisor; territory.disabled = !supervisor; territory.parentElement.hidden = !supervisor;
|
|
227
|
+
territoryHint.hidden = !supervisor;
|
|
228
|
+
el('prepare').disabled = supervisor && !territories.length;
|
|
229
|
+
};
|
|
230
|
+
role.addEventListener('change',update); update();
|
|
231
|
+
}
|
|
232
|
+
if (action === 'role.session') { field('binding_id','要调整的角色','select',allowedBindings(item=>['CHANCELLOR','SUPERVISOR'].includes(item.roleType))); field('session_id','新的运行会话','select',sessions); }
|
|
233
|
+
if (action === 'execution-profile') {
|
|
234
|
+
field('binding_id','要配置的角色','select',allowedBindings(item=>['CHANCELLOR','SUPERVISOR','WORKER'].includes(item.roleType)));
|
|
235
|
+
field('provider','模型服务商(可选,留空使用现有配置默认值)','text',undefined,false); field('model','模型名称');
|
|
236
|
+
}
|
|
237
|
+
if (action === 'ceiling') { const rows=make('div',undefined,'stack field wide'); rows.id='ceiling-rows'; rows.append(ceilingRow()); const add=make('button','添加能力'); add.type='button'; add.addEventListener('click',()=>rows.append(ceilingRow())); el('fields').append(rows,add); }
|
|
238
|
+
if (action === 'budget.policy') {
|
|
239
|
+
field('enabled','预算拦截','select',[{id:'true',label:'启用,按政策检查新增执行'},{id:'false',label:'关闭,保留统计起点与已有记录'}]);
|
|
240
|
+
const limit=field('limit_tokens','周期 Token 额度','number'); limit.min='1'; limit.step='1';
|
|
241
|
+
const reserve=field('reserve_tokens','每次新执行预留 Token(估计)','number'); reserve.min='1'; reserve.step='1';
|
|
242
|
+
field('unknown_policy','存在未知用量时','select',[{id:'BLOCK',label:'阻止新增执行,先核对未知用量'},{id:'WARN',label:'提示后继续,可能超过估计额度'}]);
|
|
243
|
+
const warning=field('warning_percent','提醒阈值(百分比)','number'); warning.min='1'; warning.max='100'; warning.step='1'; warning.value='80';
|
|
244
|
+
}
|
|
245
|
+
if (action === 'plan.adopt') {
|
|
246
|
+
const plans=(Array.isArray(catalog.plans)?catalog.plans:[]).filter(plan=>plan.state==='PROPOSED');
|
|
247
|
+
const selection=field('plan_id','本次采纳的完整计划','select',plans.map(plan=>({id:plan.planId,label:plan.parentTaskId+' · 第 '+plan.version+' 版 · '+(plan.mode==='EXPERT'?'只读专家':'独立小团队')})));
|
|
248
|
+
const version=field('version','固定版本','number'); version.readOnly=true;
|
|
249
|
+
const digest=field('digest','内容指纹(固定)','textarea'); digest.readOnly=true; digest.rows=3; digest.style.overflowWrap='anywhere'; digest.style.wordBreak='break-all';
|
|
250
|
+
const review=make('section',undefined,'field wide'); review.id='plan-adoption-review'; review.setAttribute('aria-live','polite'); el('fields').append(review);
|
|
251
|
+
const update=()=>{ review.replaceChildren(); const plan=plans.find(item=>item.planId===selection.value); version.value=plan?String(plan.version):''; digest.value=plan?plan.digest:'';
|
|
252
|
+
if(!plan){review.append(make('p',plans.length?'请选择一份计划,再核对完整分工。':'当前授权范围内没有可采纳计划;请先核对计划状态及本窗口范围。','muted'));return;}
|
|
253
|
+
const binding=id=>(catalog.bindings || []).find(item=>item.id===id)?.roleName || id;
|
|
254
|
+
const territory=id=>(catalog.territories || []).find(item=>item.id===id)?.name || id;
|
|
255
|
+
review.append(make('h3','整份计划 · '+plan.parentTaskId),make('p','拆分理由:'+plan.reason),make('p','主整合者:'+binding(plan.integratorBindingId)),make('p','整项软额度 '+plan.budgetTokens+' Tokens;每次估计预留 '+plan.reserveTokens+' Tokens。金额未知。'));
|
|
256
|
+
plan.items.forEach(item=>{const row=make('article',undefined,'change');row.append(make('h4',item.title),make('p','负责人:'+binding(item.workerBindingId)+' · 领地:'+territory(item.territoryId)+' · '+(item.access==='READ_ONLY'?'只读':'范围内写入')),make('p','范围:'+item.description),make('p','验收:'+item.acceptanceCriteria),make('p','前置工作:'+(item.dependsOn.length?item.dependsOn.map(key=>plan.items.find(other=>other.key===key)?.title || key).join('、'):'无')),make('p','预期产物:'+item.expectedArtifact));review.append(row);});
|
|
257
|
+
review.append(make('p','所有子项接受后,由指定主执行者实际整合,再由主管正常审查父任务。此采纳不代表产物验收。','muted'));
|
|
258
|
+
}; selection.addEventListener('change',update); update();
|
|
259
|
+
}
|
|
260
|
+
if (action === 'delivery.item.ack') {
|
|
261
|
+
const all=(Array.isArray(catalog.deliveryItems)?catalog.deliveryItems:[]);
|
|
262
|
+
const stateLabels={ACKNOWLEDGED:'该条当前版本已知悉',PENDING:'待知悉',PENDING_REVISION:'已有旧版本知悉,当前版本待知悉'};
|
|
263
|
+
const selection=field('item_key','要记下已知悉的条目','select',all.map(item=>({id:item.itemId,label:item.taskTitle+' · '+(item.layer==='SUMMARY'?'成果摘要':'证据')+' · '+item.label+' · '+(stateLabels[item.acknowledgementState]||item.acknowledgementState)})));
|
|
264
|
+
const detail=make('section',undefined,'field wide'); detail.id='delivery-ack-review'; detail.setAttribute('aria-live','polite'); el('fields').append(detail);
|
|
265
|
+
// 工作台带入的三元组必须与当前授权范围目录完全一致才预选;否则不做任何替代选择。
|
|
266
|
+
const target=state.ackTarget; state.ackTarget=null;
|
|
267
|
+
// 「有但非法」与「没有」必须区分:前者清空选择并禁用准备,绝不默认其他条目。
|
|
268
|
+
const invalidHint=state.ackHintInvalid;
|
|
269
|
+
const matched=target?all.find(candidate=>candidate.taskId===target.taskId && candidate.itemId===target.itemId && candidate.contentHash===target.contentHash):null;
|
|
270
|
+
if(target) selection.value=matched?matched.itemId:'';
|
|
271
|
+
else if(invalidHint){ selection.value=''; el('prepare').disabled=true; }
|
|
272
|
+
const update=()=>{
|
|
273
|
+
detail.replaceChildren();
|
|
274
|
+
if(invalidHint) detail.append(make('p','地址中的条目提示缺失、重复或非法,无法定位任何一条交付:已清空选择并禁用准备,不会退回目录中的其他条目。请重新读取清单后自行选择,或从工作台重新复制激活命令。','muted'));
|
|
275
|
+
if(target) detail.append(make('p',matched
|
|
276
|
+
? '已按工作台提供的准确条目预选:任务、条目与内容版本三者一致。仍需由你查看变更预览并确认提交;知悉不会自动写入。'
|
|
277
|
+
: '无法在当前授权范围定位该条交付:任务、条目与内容版本必须同时匹配。不会提供替代或回退选择;请重新读取清单后自行选择,或按实际范围重新激活管理窗口。','muted'));
|
|
278
|
+
const item=all.find(candidate=>candidate.itemId===selection.value);
|
|
279
|
+
if(!item){ detail.append(make('p',invalidHint?'当前没有可提交的条目;非法提示不会被当作有效目标。':all.length?'请选择一条交付条目后再核对。':'本次授权范围内没有已确认交付条目。只有已被主管 ACCEPT 确认的交付才会出现在这里;执行者自述不算交付。','muted')); el('prepare').disabled=invalidHint||all.length===0; return; }
|
|
280
|
+
detail.append(make('h3','条目内容 · '+(item.layer==='SUMMARY'?'成果摘要':'证据/改动')));
|
|
281
|
+
detail.append(make('p','任务:'+item.taskTitle+'('+item.taskId+') · 已接受尝试第 '+item.attemptNo+' 次'));
|
|
282
|
+
detail.append(make('p','条目:'+item.label));
|
|
283
|
+
detail.append(make('p','内容:'+item.detail));
|
|
284
|
+
detail.append(make('p','改动定位:'+(item.changeKind==='REPO_RELATIVE_VERIFIED'?'可验证的仓库相对路径与固定版本':'不可定位')+'。'+item.changeNote));
|
|
285
|
+
detail.append(make('p','内容版本:'+item.contentHash));
|
|
286
|
+
// 接受证据强度必须在 Owner 窗口可见:legacy v1.0.0 的 TASK_ACCEPTED 缺少被审查
|
|
287
|
+
// 结果 ID 与内容摘要,允许知悉但不得冒充 exact result-bound。
|
|
288
|
+
if(item.acceptanceEvidenceKind==='LEGACY_ATTEMPT_ONLY') {
|
|
289
|
+
const weak=make('p',undefined,'evidence-weak'); weak.setAttribute('data-acceptance-evidence','LEGACY_ATTEMPT_ONLY'); weak.setAttribute('role','note');
|
|
290
|
+
weak.append(make('strong','历史接受证据较弱:'),make('span',item.acceptanceEvidenceNote || '该 Task/attempt 的 TASK_ACCEPTED 是 v1.0.0 旧格式,只有尝试编号,缺少被审查结果 ID 与内容摘要;本条按真实事件字段与同 Task/attempt 的唯一 WorkerResult 判定,不构成 exact result-bound 证据。')); detail.append(weak);
|
|
291
|
+
} else if(item.acceptanceEvidenceKind==='EXACT_RESULT_BOUND') {
|
|
292
|
+
detail.append(make('p','接受证据强度:exact result-bound(TASK_ACCEPTED 锁定了本次结果 ID 与内容摘要,且两者都与当前呈报一致)。','muted'));
|
|
293
|
+
} else {
|
|
294
|
+
detail.append(make('p','接受证据强度未在本窗口中给出;按最弱解释处理,不要据此推断 exact result-bound。','muted'));
|
|
295
|
+
}
|
|
296
|
+
detail.append(make('p','当前知悉状态:'+(stateLabels[item.acknowledgementState]||item.acknowledgementState)+(item.acknowledgedAt?' · '+new Date(item.acknowledgedAt).toLocaleString():'')));
|
|
297
|
+
detail.append(make('p','知悉只表示已知悉该条当前版本,不代表理解、质量认可、Task DONE、正式验收或发布授权;外层摘要知悉不覆盖子条。','muted'));
|
|
298
|
+
el('prepare').disabled=item.acknowledgementState==='ACKNOWLEDGED';
|
|
299
|
+
if(item.acknowledgementState==='ACKNOWLEDGED') detail.append(make('p','该条当前版本已记录 Owner 知悉;重复知悉不会新增记录。','muted'));
|
|
300
|
+
};
|
|
301
|
+
selection.addEventListener('change',update); update();
|
|
302
|
+
}
|
|
303
|
+
if (action === 'delivery.item.question') {
|
|
304
|
+
const all=(Array.isArray(catalog.deliveryItems)?catalog.deliveryItems:[]);
|
|
305
|
+
const selection=field('item_key','要提问的条目','select',all.map(item=>({id:item.itemId,label:item.taskTitle+' · '+(item.layer==='SUMMARY'?'成果摘要':'证据')+' · '+item.label+' · '+(item.questions?('已有 '+item.questions.totalCount+' 条提问'):'尚无提问')})));
|
|
306
|
+
const question=field('question_text','要问主管的问题(一次一条,提交前可预览)','textarea');
|
|
307
|
+
const detail=make('section',undefined,'field wide'); detail.id='delivery-question-review'; detail.setAttribute('aria-live','polite'); el('fields').append(detail);
|
|
308
|
+
// 与知悉同款:工作台带入的三元组必须与当前授权目录完全一致才预选。
|
|
309
|
+
const target=state.askTarget; state.askTarget=null;
|
|
310
|
+
const invalidHint=state.askHintInvalid;
|
|
311
|
+
const matched=target?all.find(candidate=>candidate.taskId===target.taskId && candidate.itemId===target.itemId && candidate.contentHash===target.contentHash):null;
|
|
312
|
+
if(target) selection.value=matched?matched.itemId:'';
|
|
313
|
+
else if(invalidHint){ selection.value=''; el('prepare').disabled=true; }
|
|
314
|
+
const update=()=>{
|
|
315
|
+
detail.replaceChildren();
|
|
316
|
+
if(invalidHint) detail.append(make('p','地址中的条目提示缺失、重复或非法,无法定位任何一条交付:已清空选择并禁用准备,不会退回目录中的其他条目。请重新读取清单后自行选择,或从工作台重新复制激活命令。','muted'));
|
|
317
|
+
if(target) detail.append(make('p',matched
|
|
318
|
+
? '已按工作台提供的准确条目预选:任务、条目与内容版本三者一致。仍需由你填写问题、查看变更预览并确认提交;提问不会自动写入。'
|
|
319
|
+
: '无法在当前授权范围定位该条交付:任务、条目与内容版本必须同时匹配。不会提供替代或回退选择;请重新读取清单后自行选择,或按实际范围重新激活管理窗口。','muted'));
|
|
320
|
+
const item=all.find(candidate=>candidate.itemId===selection.value);
|
|
321
|
+
if(!item){ detail.append(make('p',invalidHint?'当前没有可提交的条目;非法提示不会被当作有效目标。':all.length?'请选择一条交付条目后再核对。':'本次授权范围内没有已确认交付条目。只有已被主管 ACCEPT 确认的交付才会出现在这里;执行者自述不算交付。','muted')); el('prepare').disabled=invalidHint||all.length===0; return; }
|
|
322
|
+
detail.append(make('h3','条目的准确引用'));
|
|
323
|
+
detail.append(make('p','任务:'+item.taskTitle+'('+item.taskId+') · 已接受尝试第 '+item.attemptNo+' 次'));
|
|
324
|
+
detail.append(make('p','条目:'+item.label));
|
|
325
|
+
detail.append(make('p','内容版本:'+item.contentHash));
|
|
326
|
+
detail.append(make('p','接收者:接受该交付的主管;若其已退任、换 session 或领地已改绑,本条不可达,也不会自动改投继任者。','muted'));
|
|
327
|
+
const questions=item.questions;
|
|
328
|
+
detail.append(make('p', questions
|
|
329
|
+
? '该条已有 '+questions.totalCount+' 条提问('+questions.pendingCount+' 待主管回复 / '+questions.answeredCount+' 已回复)。未回复的问题在主管实际读取前只显示「待领取」。'
|
|
330
|
+
: '该条还没有提问记录。','muted'));
|
|
331
|
+
detail.append(make('p','提问不是知悉:它不改变任务状态、主管审查、Owner acceptance 与发布状态,也不自动派发或唤醒任何 Agent。','muted'));
|
|
332
|
+
el('prepare').disabled=invalidHint||!item.contentHash||!item.itemId;
|
|
333
|
+
// 选条目即可只读回看:不要求先发新问题、也不要求带 ask_* 提示重开窗口。
|
|
334
|
+
// 这里只触发既有精确只读 GET,不发送任何写请求、不自动知悉。
|
|
335
|
+
if(item.itemId){ state.askThread={ taskId:item.taskId, itemId:item.itemId }; state.questionLimit=50; void renderQuestions(); }
|
|
336
|
+
};
|
|
337
|
+
selection.addEventListener('change',update); update();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function payload() {
|
|
341
|
+
const action=el('action').value; const parameters={};
|
|
342
|
+
for (const field of el('fields').querySelectorAll('[name]')) if (!field.disabled && field.value.trim() !== '') parameters[field.name]=field.value.trim();
|
|
343
|
+
// v3.2.0:解除现任主理必须**显式**发送 JSON null。空 select 会被上面的收集逻辑跳过,
|
|
344
|
+
// 无法表达“解除”,因此这里读专用勾选框(它不参与 [name] 收集,避免向服务端多送字段)。
|
|
345
|
+
if (action==='territory.supervisor' && releaseSupervisorChecked()) {
|
|
346
|
+
if (!parameters.territory_id) throw new Error('请选择要解除主理的领地。');
|
|
347
|
+
return {action,parameters:{territory_id:parameters.territory_id,supervisor_binding_id:null}};
|
|
348
|
+
}
|
|
349
|
+
if (action==='execution-profile') { const binding_id=parameters.binding_id; if (!parameters.model) throw new Error('请填写明确的模型名称。'); const profile={...(parameters.provider?{provider:parameters.provider}:{}),model:parameters.model}; return {action,parameters:{binding_id,profile}}; }
|
|
350
|
+
if (action==='ceiling') { const ceiling=Object.create(null); for (const row of el('ceiling-rows').children) { const name=row.querySelector('input').value.trim(); if (!name || Object.hasOwn(ceiling,name)) throw new Error('能力名称不能为空或重复。'); ceiling[name]=row.querySelector('select').value==='true'; } if (!Object.keys(ceiling).length) throw new Error('请至少配置一项能力。'); return {action,parameters:{ceiling}}; }
|
|
351
|
+
if (action==='budget.policy') { const numeric={}; for (const key of ['limit_tokens','reserve_tokens','warning_percent']) { const value=Number(parameters[key]); if (!Number.isSafeInteger(value) || value<1 || key==='warning_percent' && value>100) throw new Error('预算额度、预留和阈值必须是范围内的正整数。'); numeric[key]=value; } if (numeric.reserve_tokens>numeric.limit_tokens) throw new Error('每次预留不能超过周期额度。'); return {action,parameters:{enabled:parameters.enabled==='true',...numeric,unknown_policy:parameters.unknown_policy}}; }
|
|
352
|
+
if (action==='plan.adopt') { const plan=(state.control.catalog?.plans || []).find(item=>item.planId===parameters.plan_id && item.state==='PROPOSED'); const version=Number(parameters.version); if(!plan || !Number.isSafeInteger(version) || version<1 || version!==plan.version || parameters.digest!==plan.digest) throw new Error('请选择当前范围内的完整计划;版本或内容指纹已变化时请重新读取。'); return {action,parameters:{plan_id:plan.planId,version:plan.version,digest:plan.digest}}; }
|
|
353
|
+
if (action==='delivery.item.ack') { const item=(state.control.catalog?.deliveryItems || []).find(candidate=>candidate.itemId===parameters.item_key); if(!item) throw new Error('请选择当前范围内的一条已确认交付条目。'); return {action,parameters:{task_id:item.taskId,delivery_id:item.deliveryId,item_id:item.itemId,content_hash:item.contentHash,attempt_no:item.attemptNo,result_id:item.resultId}}; }
|
|
354
|
+
if (action==='delivery.item.question') {
|
|
355
|
+
const item=(state.control.catalog?.deliveryItems || []).find(candidate=>candidate.itemId===parameters.item_key);
|
|
356
|
+
if(!item) throw new Error('请选择当前范围内的一条已确认交付条目。');
|
|
357
|
+
const questionText=String(parameters.question_text || '').trim();
|
|
358
|
+
if(!questionText) throw new Error('请填写要问主管的具体问题;提交前会先给出预览。');
|
|
359
|
+
return {action,parameters:{task_id:item.taskId,delivery_id:item.deliveryId,item_id:item.itemId,content_hash:item.contentHash,attempt_no:item.attemptNo,result_id:item.resultId,question_text:questionText}};
|
|
360
|
+
}
|
|
361
|
+
if (action==='territory.update' && !parameters.name && !parameters.summary) throw new Error('请填写要修改的名称或说明。');
|
|
362
|
+
return {action,parameters};
|
|
363
|
+
}
|
|
364
|
+
async function request(path, body) {
|
|
365
|
+
const config={credentials:'same-origin',cache:'no-store',headers:{Accept:'application/json'}};
|
|
366
|
+
if (path.startsWith('receipts/')) config.headers['X-Kingdom-Decision-Id']=state.decisionId;
|
|
367
|
+
if (body !== undefined) { config.method='POST'; config.headers['Content-Type']='application/json'; config.headers['X-Kingdom-Client']='owner-gui'; config.headers['X-Kingdom-CSRF']=state.control.csrfToken; config.headers['X-Kingdom-Request-Id']=crypto.randomUUID(); config.body=JSON.stringify(body); }
|
|
368
|
+
const response=await fetch('/api/owner/'+path,config); let result;
|
|
369
|
+
try { result=await response.json(); } catch { throw new Error('未收到可核对的服务端结果。'); }
|
|
370
|
+
if (!response.ok || result.ok===false) { const error=new Error(result.message || '请求未成功。'); error.confirmedRejection=response.status===409 && ['PREVIEW_STALE','PREVIEW_EXPIRED','MUTATION_NOT_APPLIED'].includes(result.errorCode); throw error; }
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
async function requestChange(target) {
|
|
374
|
+
const query = 'task=' + encodeURIComponent(target.taskId) + '&evidence=' + encodeURIComponent(target.evidenceId) + '&item=' + encodeURIComponent(target.entryId);
|
|
375
|
+
const response = await fetch('/api/owner/delivery-change?' + query, { credentials:'same-origin', cache:'no-store', headers:{ Accept:'application/json' } });
|
|
376
|
+
let result;
|
|
377
|
+
try { result = await response.json(); } catch { throw new Error('未收到可核对的服务端结果。'); }
|
|
378
|
+
if (!response.ok || result.ok === false) throw new Error(result.message || '读取改动证据未成功。');
|
|
379
|
+
return result;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* 只读渲染某条交付条目的问答线程。
|
|
383
|
+
*
|
|
384
|
+
* 与改动读取同款边界:缺窗口、未授权该动作、超范围或条目不存在都如实说明,
|
|
385
|
+
* 不做替代选择;这里不发送任何写请求,也不自动知悉。
|
|
386
|
+
*/
|
|
387
|
+
async function requestQuestions(target) {
|
|
388
|
+
const query = 'task=' + encodeURIComponent(target.taskId) + '&item=' + encodeURIComponent(target.itemId);
|
|
389
|
+
const response = await fetch('/api/owner/delivery-questions?' + query, { credentials:'same-origin', cache:'no-store', headers:{ Accept:'application/json' } });
|
|
390
|
+
let result;
|
|
391
|
+
try { result = await response.json(); } catch { throw new Error('未收到可核对的服务端结果。'); }
|
|
392
|
+
if (!response.ok || result.ok === false) throw new Error(result.message || '读取问答记录未成功。');
|
|
393
|
+
return result;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* 逐条卡片:先标出该问题相对当前交付目录的版本关系,再谈可达性。
|
|
397
|
+
*
|
|
398
|
+
* 历史版与无法重验的问题仍可读、仍显示原文与回复,但绝不能被读成「当前可回复」:
|
|
399
|
+
* 它们不计当前待办,界面也要明说。这不是可写状态,只是本窗口的展示判定;写入端
|
|
400
|
+
* 另有自己的版本门。
|
|
401
|
+
*/
|
|
402
|
+
function questionCard(question) {
|
|
403
|
+
const version = question.itemVersion === 'CURRENT' ? 'CURRENT' : question.itemVersion === 'UNVERIFIABLE' ? 'UNVERIFIABLE' : 'HISTORICAL';
|
|
404
|
+
const card = make('article',undefined,'change');
|
|
405
|
+
card.setAttribute('data-item-version',version);
|
|
406
|
+
card.append(make('h4','提问 · ' + new Date(question.askedAt).toLocaleString()));
|
|
407
|
+
card.append(make('p',version === 'CURRENT' ? '版本:已验证当前版(内容版本与提问时一致)'
|
|
408
|
+
: version === 'UNVERIFIABLE' ? '版本:无法重验(当前交付目录无法重新派生该条目版本;不计当前待办,也不可回复)'
|
|
409
|
+
: '版本:历史版(可确认的旧内容版本,仅留历史;不是当前待办,也不可回复)','muted'));
|
|
410
|
+
card.append(make('p',question.questionText));
|
|
411
|
+
card.append(make('p','接收者:' + question.reviewerBindingId,'muted'));
|
|
412
|
+
card.append(make('p',version === 'CURRENT'
|
|
413
|
+
? question.replyStateNote
|
|
414
|
+
// 不进当前回复的卡片不展示可达性判定:Core 只按此刻的 binding 与领地关系计算,
|
|
415
|
+
// 既没有当时证据,也不能暗示旧问题现在仍可回复。因此这里只说明版本事实。
|
|
416
|
+
: version === 'UNVERIFIABLE'
|
|
417
|
+
? '该问题不参与当前回复:当前交付无法重验它所属条目的内容版本,因此既不能当作当前版,也不能断言它是旧版;它不会作为当前待办,也不能回复。'
|
|
418
|
+
: '该问题不参与当前回复:它只对提问时的那一内容版本有效;它不会作为当前待办,也不能在现在回复。',
|
|
419
|
+
version === 'CURRENT' ? 'muted' : 'evidence-weak'));
|
|
420
|
+
if (question.reply) {
|
|
421
|
+
card.append(make('h4','主管回复 · ' + new Date(question.reply.repliedAt).toLocaleString()));
|
|
422
|
+
card.append(make('p',question.reply.replyText));
|
|
423
|
+
card.append(make('p','回复者:' + question.reply.responderBindingId,'muted'));
|
|
424
|
+
} else if (version === 'HISTORICAL') {
|
|
425
|
+
card.append(make('p','该历史版问题没有回复记录;它不会被算作当前待领取。','muted'));
|
|
426
|
+
} else if (version === 'UNVERIFIABLE') {
|
|
427
|
+
card.append(make('p','该问题没有回复记录,且当前无法重验其条目版本;它不会被算作当前待领取,也不能回复。','muted'));
|
|
428
|
+
} else {
|
|
429
|
+
card.append(make('p','待领取(尚未确认主管已读取);这不表示已通知或已阅读。','muted'));
|
|
430
|
+
}
|
|
431
|
+
return card;
|
|
432
|
+
}
|
|
433
|
+
function renderQuestions(focusAfterRender) {
|
|
434
|
+
const target = state.askThread;
|
|
435
|
+
if (!target) return;
|
|
436
|
+
show('question-thread', true);
|
|
437
|
+
const body = el('question-thread-body'); if (!body) return;
|
|
438
|
+
// 防串:快速改选条目后,慢的旧响应不得覆盖新面板。序号在每次发起读取时递增,
|
|
439
|
+
// 响应回来时若已不是最新一次请求(或目标已换)就丢弃,不写任何 DOM。
|
|
440
|
+
const seq = ++state.questionSeq;
|
|
441
|
+
// 焦点意图按**本次调用**捕获,不放共享 state:这样它天然受上面的防串守卫约束,
|
|
442
|
+
// 过期的 A 响应在守卫处就已返回,不会把焦点或视口跳到旧线程。
|
|
443
|
+
// 'MORE' 是键盘连续翻页(点「更多」会重建整段 DOM,原按钮随即消失,渲染完成后
|
|
444
|
+
// 把焦点交给新的「更多」按钮,或全部显示完时最后揭示的那张卡片);'THREAD' 是
|
|
445
|
+
// 从只读历史条目点入后,把焦点与视口送回上方真实线程标题。
|
|
446
|
+
const limit = Math.max(1, state.questionLimit);
|
|
447
|
+
body.replaceChildren(make('p','正在读取该条目的问答记录…','muted'));
|
|
448
|
+
return (async () => {
|
|
449
|
+
try {
|
|
450
|
+
const thread = (await requestQuestions(target)).questions;
|
|
451
|
+
if (seq !== state.questionSeq || state.askThread !== target) return;
|
|
452
|
+
body.replaceChildren();
|
|
453
|
+
const heading = make('h3', thread.taskTitle + ' · ' + thread.itemLabel);
|
|
454
|
+
heading.id = 'question-thread-heading';
|
|
455
|
+
body.append(heading);
|
|
456
|
+
body.append(make('p','任务 '+thread.taskId+' · 条目 '+thread.itemId+' · '
|
|
457
|
+
+ (thread.contentHash ? '当前内容版本 '+thread.contentHash : '当前交付无法重验该条目版本'),'muted'));
|
|
458
|
+
body.append(make('p','共 '+thread.questions.length+' 条提问('+thread.pendingCount+' 条当前版待领取 / '+thread.answeredCount+' 条已回复'
|
|
459
|
+
+(thread.historyCount?(';'+thread.historyCount+' 条属于旧内容版本,仅留历史,不计当前待办'):'')
|
|
460
|
+
+(thread.unverifiableCount?(';'+thread.unverifiableCount+' 条当前无法重验,不计当前待办,也不可回复'):'')+')。','muted'));
|
|
461
|
+
const shown=Math.min(limit, thread.questions.length);
|
|
462
|
+
let lastCard=null;
|
|
463
|
+
for (const question of thread.questions.slice(0,shown)) { lastCard=questionCard(question); body.append(lastCard); }
|
|
464
|
+
let more=null;
|
|
465
|
+
if (thread.questions.length > shown) {
|
|
466
|
+
// 文案必须覆盖整份列表:它展开的既有当前版,也有历史版和无法重验的记录。
|
|
467
|
+
more = make('button','显示更多提问记录(已显示 '+shown+' / 共 '+thread.questions.length+' 条)');
|
|
468
|
+
more.type='button'; more.id='question-more';
|
|
469
|
+
more.title='继续显示余下的提问与回复;仍然只读,不写入任何事实。';
|
|
470
|
+
more.addEventListener('click',()=>{ state.questionLimit=shown+50; void renderQuestions('MORE'); });
|
|
471
|
+
body.append(more);
|
|
472
|
+
}
|
|
473
|
+
if(focusAfterRender==='MORE') {
|
|
474
|
+
if(more) more.focus();
|
|
475
|
+
else if(lastCard){ lastCard.tabIndex=-1; lastCard.focus(); }
|
|
476
|
+
} else if(focusAfterRender==='THREAD') {
|
|
477
|
+
// 只读历史列表在页面靠下,而线程详情在它上方:点完靠下的条目后,更新的内容
|
|
478
|
+
// 落在当前视口之外。标题本身不可聚焦,这里临时给 tabIndex=-1 让程序化焦点成立,
|
|
479
|
+
// 再把视口带到真实线程开头;焦点只在上面防串守卫通过后才动。
|
|
480
|
+
heading.tabIndex=-1; heading.focus();
|
|
481
|
+
if(typeof heading.scrollIntoView === 'function') heading.scrollIntoView({ block: 'start' });
|
|
482
|
+
}
|
|
483
|
+
body.append(make('p',thread.note,'muted'));
|
|
484
|
+
body.append(make('p','只读查看:不写入任何事实,也不自动记为已知悉;回复只由接受该交付的主管在其 session-bound Agent Tool 中完成。','muted'));
|
|
485
|
+
} catch (error) {
|
|
486
|
+
if (seq !== state.questionSeq || state.askThread !== target) return;
|
|
487
|
+
const failure = make('p','读取失败:'+error.message+' 不会退回其他条目;请确认管理窗口仍然有效、已授权该动作并在授权范围内。','muted');
|
|
488
|
+
body.replaceChildren(failure);
|
|
489
|
+
// 与新鲜成功同款:线程详情在历史列表上方,失败若只写进上方面板,停在下方列表的
|
|
490
|
+
// Owner 看不到。这里仍只在**本次**失败通过上面防串守卫后,才把焦点与视口落到这条
|
|
491
|
+
// 失败消息本身;自动预选/改选触发的读取不动焦点,避免页面加载时抢走键盘位置。
|
|
492
|
+
// 过期 A 失败在守卫处已返回,既不会覆盖 B 的面板,也不会把焦点或视口跳回旧线程。
|
|
493
|
+
if (focusAfterRender === 'THREAD') {
|
|
494
|
+
failure.tabIndex=-1; failure.focus();
|
|
495
|
+
if(typeof failure.scrollIntoView === 'function') failure.scrollIntoView({ block: 'start' });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
})();
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* 只读问答历史入口。
|
|
502
|
+
*
|
|
503
|
+
* 只列出 Core 已按本窗口 scope 与 delivery.item.question 动作过滤后的既有提问条目
|
|
504
|
+
* (含已离开当前交付目录、目前无法重验的旧问答)。按钮只切换只读线程目标:
|
|
505
|
+
* 它不在表单内、也不进入 prepare 的目录来源,因此不能作为新提问目标,且不写任何事实。
|
|
506
|
+
* 线程详情在列表上方,点击后由 renderQuestions('THREAD') 在真实响应就绪时才把
|
|
507
|
+
* 焦点与视口送到线程标题,靠下的条目点完也有当前视口反馈。
|
|
508
|
+
*/
|
|
509
|
+
function renderQuestionHistory() {
|
|
510
|
+
const panel = el('question-history'); const list = el('question-history-list');
|
|
511
|
+
if (!panel || !list) return;
|
|
512
|
+
list.replaceChildren();
|
|
513
|
+
const entries = Array.isArray(state.control?.catalog?.deliveryQuestionHistory) ? state.control.catalog.deliveryQuestionHistory : [];
|
|
514
|
+
if (!entries.length) { panel.hidden = true; return; }
|
|
515
|
+
panel.hidden = false;
|
|
516
|
+
for (const entry of entries) {
|
|
517
|
+
const version = entry.latestItemVersion === 'CURRENT' ? '当前版' : entry.latestItemVersion === 'UNVERIFIABLE' ? '当前无法重验' : '旧内容版本';
|
|
518
|
+
const button = make('button', entry.taskTitle + ' · ' + entry.itemLabel + ' · ' + version
|
|
519
|
+
+ '(共 ' + entry.questionCount + ' 条提问,' + entry.pendingCount + ' 条当前版待领取)· 最后提问 ' + new Date(entry.lastAskedAt).toLocaleString());
|
|
520
|
+
button.type='button'; button.className='question-history-entry';
|
|
521
|
+
button.setAttribute('data-question-history','READ_ONLY');
|
|
522
|
+
button.title='只读回看这一条的提问与回复;不会用作新提问目标,也不写入任何事实。';
|
|
523
|
+
button.addEventListener('click',()=>{ state.askThread={ taskId:entry.taskId, itemId:entry.itemId }; state.questionLimit=50; void renderQuestions('THREAD'); });
|
|
524
|
+
list.append(button);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* 只读渲染已确认改动。缺窗口、超范围、证据漂移或条目不存在都如实说明,
|
|
529
|
+
* 且不做替代选择;这里不发送任何写请求。
|
|
530
|
+
*/
|
|
531
|
+
async function renderChange() {
|
|
532
|
+
if (!state.changeTarget && !state.changeHintInvalid) return;
|
|
533
|
+
show('change-panel', true);
|
|
534
|
+
const body = el('change-body');
|
|
535
|
+
body.replaceChildren();
|
|
536
|
+
if (state.changeHintInvalid) {
|
|
537
|
+
body.append(make('p','地址中的改动提示缺失、重复或非法:无法定位任何一条改动,也不会退回其他条目。请从工作台重新打开该条改动。','muted'));
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
body.append(make('p','正在读取该条改动证据…','muted'));
|
|
541
|
+
try {
|
|
542
|
+
const result = await requestChange(state.changeTarget);
|
|
543
|
+
const change = result.change;
|
|
544
|
+
body.replaceChildren();
|
|
545
|
+
body.append(make('h3','主管确认的改动证据 · ' + change.status));
|
|
546
|
+
body.append(make('p','任务:' + change.taskTitle + '(' + change.taskId + ') · 第 ' + change.attemptNo + ' 次尝试'));
|
|
547
|
+
body.append(make('p','仓库相对路径:' + change.repoPath));
|
|
548
|
+
body.append(make('p','固定版本:' + change.revision + ' · VCS:' + change.repoVcs + (change.repoHead ? '(基准 ' + String(change.repoHead).slice(0,12) + ')' : '')));
|
|
549
|
+
body.append(make('p','证据级别:有界窗口观测 + 主管确认;不证明 Git 作者身份,也不证明由哪个执行者写入。','muted'));
|
|
550
|
+
body.append(make('p', change.coverageComplete
|
|
551
|
+
? '覆盖:完整。'
|
|
552
|
+
: '覆盖:部分(' + ((change.coverageReasons || []).join('、') || '存在未判定项') + ');不得据此判断为完整覆盖。', 'muted'));
|
|
553
|
+
body.append(make('p', change.note, 'muted'));
|
|
554
|
+
const hunks = Array.isArray(change.hunks) ? change.hunks : [];
|
|
555
|
+
if (!hunks.length) body.append(make('p','本条目没有可展示的行级片段(二进制、超限或正文未保留)。','muted'));
|
|
556
|
+
for (const hunk of hunks.slice(0,200)) {
|
|
557
|
+
const line = make('code', (hunk.kind === 'ADDED' ? '+ ' : '- ') + String(hunk.text));
|
|
558
|
+
line.setAttribute('data-hunk', hunk.kind === 'ADDED' ? 'ADDED' : 'REMOVED');
|
|
559
|
+
body.append(line);
|
|
560
|
+
}
|
|
561
|
+
body.append(make('p','只读查看:不写入任何事实,也不自动记为已知悉。逐条知悉仍须在管理窗口中另行准备、预览并提交。','muted'));
|
|
562
|
+
} catch (error) {
|
|
563
|
+
body.replaceChildren(make('p','读取失败:' + error.message + ' 不会退回其他条目;请确认管理窗口仍然有效并在授权范围内。','muted'));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function lock(value) { state.busy=value; el('edit-fields').disabled=value || !!state.preview || state.uncertain || !active(); el('commit').disabled=value || !state.preview || !active() || state.uncertain; el('edit-again').disabled=value || state.uncertain; el('next').disabled=value || state.uncertain; el('commit').title=el('commit').disabled?'当前不能提交:需要先查看变更预览,且管理窗口必须有效。':'确认应用上面列出的准确变更。'; } function scopeText(view) {
|
|
567
|
+
const scope=view.scope; const catalog=state.control.catalog || {};
|
|
568
|
+
const lines=[view.kingdomId?'王国:'+view.kingdomId:'仅允许初始化空王国', '操作:'+view.actions.map(action=>names[action] || action).join('、')];
|
|
569
|
+
if (scope.kingdomWide) lines.push('包含明确授权的王国级配置');
|
|
570
|
+
if (scope.territoryIds.length) lines.push('领地:'+(catalog.territories || []).map(item=>item.name).join('、'));
|
|
571
|
+
if (scope.bindingIds.length) lines.push('角色:'+(catalog.bindings || []).map(item=>item.roleName).join('、'));
|
|
572
|
+
return lines.join('\n');
|
|
573
|
+
}
|
|
574
|
+
async function load() {
|
|
575
|
+
try {
|
|
576
|
+
if(pendingInvalid) throw new Error('浏览器中的待确认操作引用无法读取。请保留当前页面并核对原操作记录后再继续。');
|
|
577
|
+
if(state.uncertain && state.preview) { show('result-panel',true); show('lookup',true); show('next',false); text('result-title','正在恢复待确认操作'); text('result-meta','决定 '+state.decisionId+' · 操作 '+state.preview.operationId); }
|
|
578
|
+
const control=await request('control');
|
|
579
|
+
if (state.decisionId !== null && control.decision.decisionId !== state.decisionId) throw new Error('管理窗口已被替换。请保留原决定与操作编号核对结果,不要重新提交原变更。');
|
|
580
|
+
if (state.decisionId === null) state.decisionId=control.decision.decisionId;
|
|
581
|
+
state.control=control;
|
|
582
|
+
if(state.uncertain && state.preview) { show('editor',false); show('preview-panel',false); lock(false); await lookupReceipt(); tick(); return; }
|
|
583
|
+
if (!active()) { tick(); return; }
|
|
584
|
+
show('activation',false); show('window',true); show('editor',true); text('scope',scopeText(control.decision));
|
|
585
|
+
const available=control.decision.actions.filter(action=>Object.hasOwn(names,action));
|
|
586
|
+
options(el('action'),available.map(id=>({id,label:names[id]})));
|
|
587
|
+
let openNotice='';
|
|
588
|
+
// 预选只发生在本次窗口确实授权了对应动作时;否则提示按实际范围重新激活。
|
|
589
|
+
if(!available.includes('delivery.item.ack') && (state.ackTarget || state.ackHintInvalid)) {
|
|
590
|
+
state.ackTarget=null; state.ackHintInvalid=false;
|
|
591
|
+
openNotice='当前管理窗口的授权范围不包含「逐条记下已知悉交付」,无法预选该条目;请按实际范围重新激活管理窗口。';
|
|
592
|
+
}
|
|
593
|
+
if(!available.includes('delivery.item.question') && (state.askTarget || state.askHintInvalid)) {
|
|
594
|
+
state.askTarget=null; state.askHintInvalid=false; state.askThread=null;
|
|
595
|
+
openNotice='当前管理窗口的授权范围不包含「就一条交付条目提问」,无法预选该条目;请按实际范围重新激活管理窗口。';
|
|
596
|
+
}
|
|
597
|
+
if(state.ackTarget) el('action').value='delivery.item.ack';
|
|
598
|
+
if(state.askTarget) el('action').value='delivery.item.question';
|
|
599
|
+
renderFields(); lock(false); status(openNotice || '管理窗口已就绪。填写变更后先查看预览。'); tick();
|
|
600
|
+
// 只读问答线程与历史入口在窗口就绪后单独渲染:它们不参与授权,也不写任何事实。
|
|
601
|
+
renderQuestionHistory();
|
|
602
|
+
void renderQuestions();
|
|
603
|
+
} catch(error) { show('activation',true); show('editor',false); show('window',false); status(error.message); }
|
|
604
|
+
}
|
|
605
|
+
function expire(message) { show('activation',true); show('editor',false); el('commit').disabled=true; el('revoke').disabled=true; status(message); }
|
|
606
|
+
function tick() { if (!state.control) return; const seconds=Math.max(0,Math.ceil((Date.parse(state.control.decision.expiresAt)-Date.now())/1000)); text('expiry','授权剩余 '+Math.floor(seconds/60)+' 分 '+seconds%60+' 秒'); if (!active()) { const receiptUntil=Date.parse(state.control.receiptExpiresAt || ''); expire(Number.isFinite(receiptUntil) && receiptUntil>Date.now()?'写窗口已到期或撤销,仍可在五分钟宽限期内查询已提交结果;新变更需要重新激活。':'管理窗口及结果查询宽限期已结束。请保留操作编号并重新核对。'); } }
|
|
607
|
+
setInterval(tick,1000);
|
|
608
|
+
el('action').addEventListener('change',renderFields);
|
|
609
|
+
el('operation-form').addEventListener('submit',async event=>{
|
|
610
|
+
event.preventDefault(); if (state.busy || state.uncertain || !active()) return; lock(true); status('正在核对范围与当前状态…');
|
|
611
|
+
try { const result=await request('prepare',payload()); state.preview=result.preview; show('preview-panel',true); text('preview-summary',result.preview.summary); el('changes').replaceChildren();
|
|
612
|
+
for (const change of result.preview.changes) { const row=make('div',undefined,'change'); row.append(make('dt',change.label),make('dd','当前:'+(change.before ?? '无'),'before'),make('dd','应用后:'+(change.after ?? '无'),'after')); el('changes').append(row); }
|
|
613
|
+
text('preview-expiry','预览有效至 '+new Date(result.preview.expiresAt).toLocaleTimeString()+'。提交前会再次检查当前状态。'); status('请确认下面的准确变更。'); el('commit').focus();
|
|
614
|
+
} catch(error) { status(error.message); } finally { lock(false); }
|
|
615
|
+
});
|
|
616
|
+
el('edit-again').addEventListener('click',()=>{ if (state.busy || state.uncertain) return; state.preview=null; show('preview-panel',false); lock(false); el('action').focus(); status('可修改内容;修改后需要重新生成预览。'); });
|
|
617
|
+
/** 当前表单里选中的条目 ID;只用于提交后回读同一条目的问答线程。 */
|
|
618
|
+
function selectedItemKey() { const node=el('param-item_key'); return node ? node.value : ''; }
|
|
619
|
+
function receiptView(receipt) { clearPending(); state.uncertain=false; if (receipt.action==='init' && state.control) { state.control.decision.state='CONSUMED'; show('editor',false); show('activation',true); el('revoke').disabled=true; } show('result-panel',true); show('lookup',false); show('next',active()); text('result-title','变更已应用'); text('result-message',receipt.message); text('result-meta','操作 '+receipt.operationId+' · 记录 '+receipt.receiptSeq+' · '+new Date(receipt.appliedAt).toLocaleString()); show('preview-panel',false);
|
|
620
|
+
// 提问提交成功后只读回读该条目的问答线程:新问题在主管实际读取前仍是「待领取」。
|
|
621
|
+
if (receipt.action==='delivery.item.question') {
|
|
622
|
+
const item=(state.control.catalog?.deliveryItems || []).find(candidate=>candidate.itemId===selectedItemKey());
|
|
623
|
+
if(item) { state.askThread={ taskId:item.taskId, itemId:item.itemId }; void renderQuestions(); }
|
|
624
|
+
}
|
|
625
|
+
status(receipt.action==='init'?'初始化已完成。本次单次授权已用完,日常管理需要按实际范围重新激活。'
|
|
626
|
+
:receipt.action==='delivery.item.question'?'已提交提问。它不会自动通知主管,也不改变任务状态;主管回复后可在此回读(未回复前只显示「待领取」)。'
|
|
627
|
+
:'已收到本次变更的服务端记录。'); }
|
|
628
|
+
el('commit').addEventListener('click',async()=>{
|
|
629
|
+
if (state.busy || !state.preview || state.uncertain || !active()) return; lock(true); status('正在提交已确认的变更…');
|
|
630
|
+
try { rememberPending(); receiptView((await request('commit',{prepareId:state.preview.prepareId,operationId:state.preview.operationId})).receipt); }
|
|
631
|
+
catch(error) { state.uncertain=!error.confirmedRejection; show('result-panel',true); show('lookup',state.uncertain); show('next',false); text('result-title',error.confirmedRejection?'本次提交未应用':'结果待确认'); text('result-meta','决定 '+state.decisionId+' · 操作 '+state.preview.operationId);
|
|
632
|
+
if(error.confirmedRejection) { clearPending(); state.preview=null; show('preview-panel',false); text('result-message',error.message+' 原输入已保留,请重新查看变更预览。'); status('本次未应用,可以修改内容并重新预览。'); }
|
|
633
|
+
else { text('result-message',error.message+' 请查询同一操作的记录。页面不会自动重发变更;刷新页面后也会恢复此操作。'); status('请先查询本次结果,再决定下一步。'); }
|
|
634
|
+
}
|
|
635
|
+
finally { lock(false); }
|
|
636
|
+
});
|
|
637
|
+
async function lookupReceipt() {
|
|
638
|
+
if (!state.preview || state.busy) return; lock(true);
|
|
639
|
+
try { const result=await request('receipts/'+encodeURIComponent(state.preview.operationId)); if (result.receipt) receiptView(result.receipt); else { text('result-message','暂未发现本次操作的完成记录。这不等于确认没有副作用;请保留操作编号,待核对后再创建新变更。'); status('尚无完成记录;原操作不会被自动重试。'); } }
|
|
640
|
+
catch(error) { status(error.message+' 原决定 '+state.decisionId+';原操作 '+state.preview.operationId+'。'); } finally { lock(false); }
|
|
641
|
+
}
|
|
642
|
+
el('lookup').addEventListener('click',lookupReceipt);
|
|
643
|
+
el('next').addEventListener('click',()=>{ if(state.busy || state.uncertain) return; state.preview=null; void load(); });
|
|
644
|
+
el('revoke').addEventListener('click',async()=>{
|
|
645
|
+
if (!state.control) return; el('revoke').disabled=true;
|
|
646
|
+
try { const result=await request('revoke',{}); state.control.decision.state='REVOKED'; state.control.receiptExpiresAt=result.receiptExpiresAt; expire('管理窗口已撤销。五分钟内仍可查询已提交结果;未提交预览不能继续应用。'); }
|
|
647
|
+
catch(error) { status(error.message+' 撤销尚未确认。'); el('revoke').disabled=false; }
|
|
648
|
+
});
|
|
649
|
+
void load();
|
|
650
|
+
void renderChange();
|
|
651
|
+
})();
|
|
190
652
|
</script></body></html>`;
|
|
191
653
|
//# sourceMappingURL=owner-ui.js.map
|