nx-kv 0.2.1 → 0.3.1

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.
@@ -0,0 +1,277 @@
1
+ // 阻塞 wait 时由内嵌服务器直出的独立弹窗页面(不依赖 vite 构建,npx 也能开)。
2
+ //
3
+ // 注意:本文件返回的是一整段 HTML 字符串,内联 <script> 刻意只用单/双引号与
4
+ // 字符串拼接(不用模板字符串、不用 ${}),以免与外层模板串冲突;内联脚本里
5
+ // 出现的反斜杠在此外层模板中需双写(如换行正则写成 /\\n/g)。
6
+ import { basename } from 'node:path';
7
+
8
+ export function renderPopupPage({ key, cwd, summary, timeoutMs }) {
9
+ const name = (cwd && basename(cwd)) || '';
10
+ return `<!doctype html>
11
+ <html lang="zh-CN">
12
+ <head>
13
+ <meta charset="utf-8" />
14
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
15
+ <title>nx-kv · 实时任务</title>
16
+ <style>
17
+ :root {
18
+ --ink: #14161a;
19
+ --paper: #fff;
20
+ --soft: #f5f6f7;
21
+ --soft-2: #ebedf0;
22
+ --mid: #8b9096;
23
+ --radius: 12px;
24
+ --radius-sm: 8px;
25
+ --shadow: 0 1px 2px rgba(20,22,26,.06), 0 6px 16px rgba(20,22,26,.05);
26
+ }
27
+ * { box-sizing: border-box; }
28
+ body {
29
+ margin: 0;
30
+ font-family: system-ui, "Segoe UI", "Microsoft YaHei", sans-serif;
31
+ font-size: 13.5px; line-height: 1.55;
32
+ color: var(--ink); background: #fbfbfc;
33
+ -webkit-font-smoothing: antialiased;
34
+ }
35
+ .wrap { max-width: 880px; margin: 0 auto; padding: 20px 18px 32px; }
36
+ .top { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
37
+ .brand { font-weight: 800; letter-spacing: .3px; }
38
+ .brand .sub { font-weight: 400; color: var(--mid); margin-left: 8px; }
39
+ .meta { margin-left: auto; color: var(--mid); font-family: ui-monospace, Consolas, monospace; font-size: 11.5px; }
40
+ .meta .pill { background: var(--soft-2); border-radius: 5px; padding: 1px 7px; color: var(--ink); }
41
+
42
+ .topic {
43
+ background: var(--paper); border-radius: var(--radius); box-shadow: var(--shadow);
44
+ padding: 10px 14px; margin-bottom: 10px;
45
+ }
46
+ .topic .topic-label { font-size: 11px; color: var(--mid); letter-spacing: 1px; margin-bottom: 3px; }
47
+ .topic .topic-name { font-size: 14px; font-weight: 800; }
48
+ .topic .topic-cwd { font-family: ui-monospace, Consolas, monospace; font-size: 11.5px; color: var(--mid); word-break: break-all; margin-top: 2px; }
49
+
50
+ .state {
51
+ background: var(--paper); border-radius: var(--radius); box-shadow: var(--shadow);
52
+ border-left: 3px solid var(--ink); padding: 11px 14px; margin-bottom: 12px;
53
+ }
54
+ .state .state-label { font-size: 11px; color: var(--mid); letter-spacing: 1px; margin-bottom: 3px; }
55
+ .state .state-text { font-size: 15px; font-weight: 700; white-space: pre-wrap; }
56
+
57
+ .statusbar {
58
+ display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
59
+ background: var(--paper); border-radius: var(--radius); box-shadow: var(--shadow);
60
+ padding: 10px 14px; margin-bottom: 12px;
61
+ }
62
+ .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ink); animation: pulse 1.4s ease-in-out infinite; }
63
+ @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .25; } }
64
+ #clock { font-family: ui-monospace, Consolas, monospace; font-weight: 700; }
65
+ .statusbar .hint { color: var(--mid); font-size: 11.5px; }
66
+
67
+ .composer { background: var(--paper); border-radius: var(--radius); box-shadow: var(--shadow); padding: 12px 14px; margin-bottom: 12px; transition: opacity .2s; }
68
+ textarea {
69
+ width: 100%; min-height: 76px; resize: vertical;
70
+ font: inherit; color: var(--ink); background: var(--paper);
71
+ border: 1px solid var(--soft-2); border-radius: var(--radius-sm);
72
+ padding: 9px 11px; transition: border-color .15s, box-shadow .15s;
73
+ }
74
+ textarea::placeholder { color: #b3b8bd; }
75
+ textarea:focus { outline: none; border-color: var(--ink); box-shadow: 0 0 0 3px rgba(20,22,26,.07); }
76
+ .composer .row { display: flex; align-items: center; gap: 10px; margin-top: 9px; }
77
+ .composer .hint { color: var(--mid); font-size: 11.5px; margin-left: auto; }
78
+
79
+ .btn { font: inherit; cursor: pointer; padding: 6px 14px; border: 0; border-radius: var(--radius-sm); background: var(--ink); color: var(--paper); transition: opacity .15s; }
80
+ .btn:hover { opacity: .86; }
81
+ .btn.ghost { background: var(--soft); color: var(--ink); }
82
+ .btn.ghost:hover { background: var(--soft-2); opacity: 1; }
83
+ .btn.small { padding: 3px 9px; font-size: 12px; border-radius: 7px; }
84
+
85
+ .banner { display: none; border-radius: var(--radius-sm); padding: 9px 13px; margin-bottom: 12px; font-size: 12.5px; }
86
+ .banner.show { display: block; }
87
+ .banner.good { background: var(--soft); color: var(--ink); }
88
+ .banner.bad { background: #f3e3e2; color: #b3261e; }
89
+
90
+ .card { background: var(--paper); border-radius: var(--radius); box-shadow: var(--shadow); padding: 6px 12px 10px; }
91
+ .colhead { display: flex; align-items: baseline; gap: 8px; margin: 8px 2px; }
92
+ h3 { font-size: 13px; margin: 0; font-weight: 700; }
93
+ table { width: 100%; border-collapse: collapse; }
94
+ th, td { text-align: left; padding: 9px 10px; vertical-align: top; }
95
+ th { color: var(--mid); font-weight: 600; font-size: 11.5px; }
96
+ tbody tr:hover { background: var(--soft); }
97
+ tbody tr + tr td { box-shadow: inset 0 1px 0 var(--soft); }
98
+ .mono { font-family: ui-monospace, Consolas, monospace; font-size: 12px; word-break: break-all; }
99
+ td.text { min-width: 200px; }
100
+ td.time { white-space: nowrap; color: var(--mid); }
101
+ td.ops { white-space: nowrap; }
102
+ .muted { color: var(--mid); }
103
+ td.empty { text-align: center; padding: 22px 10px; }
104
+ .tag { font-size: 11px; padding: 2px 8px; border-radius: 999px; background: var(--soft); color: var(--mid); white-space: nowrap; }
105
+ .tag.strong { background: var(--ink); color: var(--paper); }
106
+ </style>
107
+ </head>
108
+ <body>
109
+ <div class="wrap">
110
+ <div class="top">
111
+ <div class="brand">nx-kv<span class="sub">实时任务输入</span></div>
112
+ <div class="meta"><span class="pill">回填后立即返回 agent</span></div>
113
+ </div>
114
+
115
+ <div class="topic">
116
+ <div class="topic-label">主题(工作目录)</div>
117
+ <div class="topic-name" id="topicName"></div>
118
+ <div class="topic-cwd" id="topicCwd"></div>
119
+ </div>
120
+
121
+ <div class="state">
122
+ <div class="state-label">当前现状</div>
123
+ <div class="state-text" id="summary"></div>
124
+ </div>
125
+
126
+ <div class="statusbar">
127
+ <span class="dot"></span>
128
+ <span>agent 正在等待你针对上述现状回填 · 剩余</span>
129
+ <span id="clock">--:--</span>
130
+ <span class="hint">超时未回填则本轮自动结束</span>
131
+ </div>
132
+
133
+ <div class="composer" id="composer">
134
+ <textarea id="input" autofocus spellcheck="false"
135
+ placeholder="输入要交给 agent 的下一条任务(Enter 提交,Shift+Enter 换行)"></textarea>
136
+ <div class="row">
137
+ <button class="btn" id="submit">回填任务</button>
138
+ <span class="hint">Enter 提交 · Shift+Enter 换行</span>
139
+ </div>
140
+ </div>
141
+
142
+ <div class="banner" id="banner"></div>
143
+
144
+ <div class="card">
145
+ <div class="colhead"><h3>本主题的任务队列</h3><span class="muted" id="count"></span></div>
146
+ <table>
147
+ <thead><tr><th style="width:54px">#</th><th>任务</th><th style="width:78px">状态</th><th style="width:150px">时间</th><th style="width:70px">操作</th></tr></thead>
148
+ <tbody id="list"></tbody>
149
+ </table>
150
+ </div>
151
+ </div>
152
+
153
+ <script>
154
+ var KEY = ${JSON.stringify(key)};
155
+ var CWD = ${JSON.stringify(cwd || '')};
156
+ var TOPIC_NAME = ${JSON.stringify(name || '')};
157
+ var SUMMARY = ${JSON.stringify(summary)};
158
+ var TIMEOUT_MS = ${JSON.stringify(timeoutMs)};
159
+ var delivered = false;
160
+
161
+ function el(id) { return document.getElementById(id); }
162
+ function esc(s) {
163
+ return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
164
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
165
+ });
166
+ }
167
+ function fmtClock(ms) {
168
+ var s = Math.max(0, Math.ceil(ms / 1000));
169
+ var m = Math.floor(s / 60);
170
+ s = s % 60;
171
+ return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
172
+ }
173
+
174
+ el('topicName').textContent = TOPIC_NAME || '(未命名)';
175
+ el('topicCwd').textContent = CWD || '(旧数据,无目录)';
176
+ el('summary').textContent = SUMMARY;
177
+ var deadline = Date.now() + TIMEOUT_MS;
178
+ function tickClock() {
179
+ var left = deadline - Date.now();
180
+ el('clock').textContent = fmtClock(left);
181
+ if (left <= 0 && !delivered) showBanner('等待已结束(超时未回填),本窗口可关闭。', true);
182
+ }
183
+ setInterval(tickClock, 250);
184
+ tickClock();
185
+
186
+ function showBanner(msg, bad) {
187
+ var b = el('banner');
188
+ b.textContent = msg;
189
+ b.className = 'banner show ' + (bad ? 'bad' : 'good');
190
+ }
191
+
192
+ function render(topic) {
193
+ el('count').textContent = '待领取 ' + topic.pending + ' · 共 ' + topic.tasks.length;
194
+ var tasks = topic.tasks.slice().reverse();
195
+ var rows = '';
196
+ if (!tasks.length) {
197
+ rows = '<tr><td colspan="5" class="muted empty">(暂无任务,在上方回填第一条)</td></tr>';
198
+ }
199
+ for (var i = 0; i < tasks.length; i++) {
200
+ var t = tasks[i];
201
+ var status = t.status === 'consumed'
202
+ ? '<span class="tag strong">已领取</span>'
203
+ : '<span class="tag">待领取</span>';
204
+ var body = esc(t.text).replace(/\\n/g, '<br>');
205
+ rows += '<tr><td class="mono">#' + t.id + '</td><td class="text">' + body + '<\/td><td>' + status +
206
+ '</td><td class="mono time">' + esc(t.consumedAt || t.createdAt || '') +
207
+ '</td><td class="ops"><button class="btn small ghost" data-id="' + t.id + '">删除</button></td></tr>';
208
+ }
209
+ el('list').innerHTML = rows;
210
+ }
211
+
212
+ function refresh() {
213
+ if (delivered) return;
214
+ fetch('/api/sessiontasks', { cache: 'no-store' })
215
+ .then(function (r) { return r.json(); })
216
+ .then(function (j) {
217
+ if (!j.ok) return;
218
+ var topic = null;
219
+ for (var i = 0; i < j.data.topics.length; i++) {
220
+ if (j.data.topics[i].key === KEY) { topic = j.data.topics[i]; break; }
221
+ }
222
+ if (topic) render(topic);
223
+ })
224
+ .catch(function () { /* 交付后服务器关闭,静默 */ });
225
+ }
226
+
227
+ function submit() {
228
+ var input = el('input');
229
+ var text = input.value.trim();
230
+ if (!text) { input.focus(); return; }
231
+ fetch('/api/sessiontasks', {
232
+ method: 'POST',
233
+ headers: { 'content-type': 'application/json' },
234
+ body: JSON.stringify({ text: text })
235
+ })
236
+ .then(function (r) { return r.json(); })
237
+ .then(function (j) {
238
+ if (!j.ok) { showBanner(j.error || '提交失败', true); return; }
239
+ input.value = '';
240
+ input.focus();
241
+ if (j.data.delivered) {
242
+ delivered = true;
243
+ showBanner('已回填给 agent,正在返回,本窗口可关闭。', false);
244
+ el('composer').style.opacity = '0.5';
245
+ refresh();
246
+ } else {
247
+ showBanner('已排入主题队列,agent 下一次 wait 会取走。', false);
248
+ refresh();
249
+ }
250
+ })
251
+ .catch(function (e) { showBanner('提交失败:' + e, true); });
252
+ }
253
+
254
+ el('submit').addEventListener('click', submit);
255
+ el('input').addEventListener('keydown', function (e) {
256
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); submit(); }
257
+ });
258
+ el('list').addEventListener('click', function (e) {
259
+ var b = e.target;
260
+ if (b && b.dataset && b.dataset.id) {
261
+ fetch('/api/sessiontasks/item', {
262
+ method: 'DELETE',
263
+ headers: { 'content-type': 'application/json' },
264
+ body: JSON.stringify({ id: Number(b.dataset.id) })
265
+ })
266
+ .then(function (r) { return r.json(); })
267
+ .then(function () { refresh(); });
268
+ }
269
+ });
270
+
271
+ refresh();
272
+ setInterval(function () { if (!delivered) refresh(); }, 1000);
273
+ </script>
274
+ </body>
275
+ </html>
276
+ `;
277
+ }