nx-kv 0.3.0 → 0.3.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  本文件记录对外可见的变更。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。
4
4
 
5
+ ## [0.3.2] - 2026-10-08
6
+
7
+ ### Fixed
8
+
9
+ - **实时任务弹窗:agent 领取(回填 delivered)后不再显示倒计时**。此前弹窗在
10
+ agent 已取走任务后,状态栏的倒计时仍继续走到 0;现 agent 一旦领取即隐藏整个
11
+ 倒计时状态条(倒计时只在「等待回填」时有意义),计时器同步停止。常驻面板主题
12
+ 卡本就仅在等待中显示倒计时、领取后显示「已结束」,行为一致。
13
+
14
+ ## [0.3.1] - 2026-10-08
15
+
16
+ ### Changed
17
+
18
+ - **实时任务(sessiontask)主题改按「工作目录名」识别**:主题 key 取 cwd 的
19
+ 目录名(basename,如 `br_ct`)——在该目录执行 CLI 即自动领取对应队列的一条
20
+ 任务;同名目录(即使完整路径不同)共用同一队列,不同目录互不串。
21
+ - **主题拥有可堆积的任务队列**:同一主题下的回填按队列堆积、逐条领取;现状
22
+ (`--summary`)不再是 key,仅作状态展示。替代 0.3.0「现状总结即等待点 key」
23
+ 的 slots 模型,旧数据自动迁移为主题。
24
+
25
+ ### Added
26
+
27
+ - **阻塞超时支持在 Web 页面配置**:「实时任务」页顶部新增超时选择,默认/推荐
28
+ **180 秒(3 分钟)**、上限 **600 秒(10 分钟)**,超上限自动截断;`wait`
29
+ 未带 `--timeout` 时采用该配置。新增 `sessiontask config --timeout <秒>`。
30
+ - 放宽单条任务内容与现状长度上限(任务 20000 字、现状 2000 字)。
31
+
5
32
  ## [0.3.0] - 2026-10-06
6
33
 
7
34
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nx-kv",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "nx-kv — KV 清单管理中枢 + 对话内实时任务接力:serve 驱动 CLI 与 Web 面板,同源同构,附带可安装 skill 供 agent 领取任务与阻塞接收用户输入",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,28 +2,32 @@
2
2
  //
3
3
  // 声明了 `resource: 'sessiontask'`,故 CRUD 五操作齐备且两端可调用
4
4
  // (tests/unit/registry.test.mjs 据此断言)。核心动作是纯 CLI 的
5
- // `sessiontask wait --summary "<现状总结>"`——agent 把它放在本轮**最后一步**
6
- // 调用:阻塞、自动打开浏览器弹窗,用户针对该现状回填后解除阻塞,把新任务返回给
7
- // agent。现状总结即等待点 key:相同现状复用、不同现状分散答复。
5
+ // `sessiontask wait --cwd "<工作目录>" --summary "<当前现状>"`——agent 把它放在本轮
6
+ // **最后一步**调用:阻塞、自动打开浏览器弹窗,用户回填后解除阻塞,把新任务返回给
7
+ // agent。工作目录即主题、主题拥有任务队列:同目录共用队列、不同目录互不串,任务可
8
+ // 在主题队列里堆积。
8
9
  //
9
10
  // 与 todo 的区别:todo 管 KV 后端上的清单(需登录);sessiontask 是**本机**
10
11
  // 实时人机接力,数据存本地文件,不依赖后端登录态。
11
12
  import * as service from './service.js';
12
13
 
13
- const SUMMARY = { type: 'string', required: true, hint: '当前现状总结(作为等待点 key)' };
14
+ const CWD = { type: 'string', hint: '工作目录(作为主题;默认当前目录)' };
15
+ const SUMMARY = { type: 'string', required: true, hint: '当前现状(agent 当前状态,展示给用户)' };
14
16
 
15
17
  const STATUS_TAG = { pending: '待领取', consumed: '已领取' };
16
18
 
17
- // 总览:每个等待点一行,先看现状再决定回填哪条
19
+ // 总览:每个主题(工作目录)一行,先看目录与现状再决定回填哪条
18
20
  function renderOverview(d) {
19
- const lines = [`等待点 ${d.slots.length} 个(等待中 ${d.waiting})`];
20
- if (!d.slots.length) {
21
- lines.push('(暂无等待点)');
21
+ const lines = [`主题 ${d.topics.length} 个(等待中 ${d.waiting} · 默认超时 ${d.settings.timeoutSec}s)`];
22
+ if (!d.topics.length) {
23
+ lines.push('(暂无主题)');
22
24
  return lines.join('\n');
23
25
  }
24
- for (const s of d.slots) {
25
- const tag = s.waiting ? '[等待中]' : '[已结束]';
26
- lines.push(` ${tag} ${s.summary} · 待领取 ${s.pending}/${s.total}`);
26
+ for (const t of d.topics) {
27
+ const tag = t.waiting ? '[等待中]' : '[已结束]';
28
+ const head = t.cwd ? `${t.name} ${t.cwd}` : t.name;
29
+ lines.push(` ${tag} ${head} · 待领取 ${t.pending}/${t.total}`);
30
+ if (t.summary) lines.push(` 现状: ${t.summary}`);
27
31
  }
28
32
  return lines.join('\n');
29
33
  }
@@ -40,17 +44,20 @@ const renderTask = (t) =>
40
44
  function renderAdd(d) {
41
45
  const head = `#${d.task.id} [${d.task.status}] ${d.task.text}`;
42
46
  return d.delivered
43
- ? `已实时回填给等待中的 agent: ${head}`
44
- : `已排队(agent 下一次 wait 取走): ${head}`;
47
+ ? `已实时回填给等待中的 agent(主题 ${d.name}): ${head}`
48
+ : `已排入主题「${d.name}」队列(agent 下一次 wait 取走): ${head}`;
45
49
  }
46
50
 
47
51
  // wait 的人读输出:成功给任务,超时明确提示「本轮结束」
48
52
  function renderWait(r) {
49
53
  if (r.status === 'timeout') {
50
- return `等待超时(${Math.round(r.timeoutMs / 1000)} 秒未回填):现状「${r.summary}」结束本轮。`;
54
+ return `等待超时(${Math.round(r.timeoutMs / 1000)} 秒未回填):主题「${r.cwd}」现状「${r.summary}」结束本轮。`;
51
55
  }
52
56
  const origin = r.queued ? '排队任务' : '实时回填';
53
- return `已取得下一条任务(${origin} · 现状「${r.summary}」): #${r.task.id}\n ${r.task.text.replace(/\n/g, '\n ')}`;
57
+ return (
58
+ `已取得下一条任务(${origin} · 主题「${r.cwd}」现状「${r.summary}」): #${r.task.id}\n` +
59
+ ` ${r.task.text.replace(/\n/g, '\n ')}`
60
+ );
54
61
  }
55
62
 
56
63
  export default {
@@ -59,7 +66,7 @@ export default {
59
66
  order: 20,
60
67
  view: () => import('./view.jsx'),
61
68
 
62
- // CRUD 资源声明:等待点任务的增删查改齐备,CLI 与面板都能调用
69
+ // CRUD 资源声明:主题任务的增删查改齐备,CLI 与面板都能调用
63
70
  resource: 'sessiontask',
64
71
 
65
72
  actions: [
@@ -68,7 +75,7 @@ export default {
68
75
  id: 'sessiontask.list',
69
76
  cli: ['sessiontask', 'list'],
70
77
  http: ['GET', '/api/sessiontasks'],
71
- summary: '总览全部等待点(现状、是否等待中、待领取数)',
78
+ summary: '总览全部主题(工作目录、现状、是否等待中、待领取数)',
72
79
  run: () => service.listTasks(),
73
80
  render: renderOverview,
74
81
  },
@@ -76,42 +83,52 @@ export default {
76
83
  id: 'sessiontask.get',
77
84
  cli: ['sessiontask', 'get'],
78
85
  http: ['GET', '/api/sessiontasks/item'],
79
- summary: '查看单条任务(按 --id,定位到 --summary 现状)',
80
- flags: { id: { type: 'number', required: true, hint: '任务编号' }, summary: SUMMARY },
81
- run: (ctx) => service.getTask({ id: ctx.id, summary: ctx.summary }),
86
+ summary: '查看单条任务(按 --id,定位到 --cwd 主题)',
87
+ flags: { id: { type: 'number', required: true, hint: '任务编号' }, cwd: CWD },
88
+ run: (ctx) => service.getTask({ id: ctx.id, cwd: ctx.cwd }),
82
89
  render: renderTask,
83
90
  },
84
91
  {
85
92
  id: 'sessiontask.add',
86
93
  cli: ['sessiontask', 'add'],
87
94
  http: ['POST', '/api/sessiontasks'],
88
- summary: '针对某现状回填一条任务(有 wait 在等则即时投递,否则排队)',
95
+ summary: '向某主题(工作目录)的队列回填一条任务(有 wait 在等则即时投递,否则排队)',
89
96
  args: ['text'],
90
- flags: { summary: SUMMARY },
91
- run: (ctx) => service.addTask({ text: ctx.text, summary: ctx.summary }),
97
+ flags: { cwd: CWD, summary: { type: 'string', hint: '顺带刷新该主题的当前现状(可选)' } },
98
+ run: (ctx) => service.addTask({ text: ctx.text, cwd: ctx.cwd, summary: ctx.summary }),
92
99
  render: renderAdd,
93
100
  },
94
101
  {
95
102
  id: 'sessiontask.update',
96
103
  cli: ['sessiontask', 'update'],
97
104
  http: ['PATCH', '/api/sessiontasks/item'],
98
- summary: '编辑任务内容(按 --id,定位到 --summary 现状)',
105
+ summary: '编辑任务内容(按 --id,定位到 --cwd 主题)',
99
106
  flags: {
100
107
  id: { type: 'number', required: true, hint: '任务编号' },
101
108
  text: { type: 'string', required: true, hint: '改成这个内容' },
102
- summary: SUMMARY,
109
+ cwd: CWD,
103
110
  },
104
- run: (ctx) => service.updateTask({ id: ctx.id, text: ctx.text, summary: ctx.summary }),
105
- render: (d) => `已更新 #${d.task.id}: ${d.task.text}`,
111
+ run: (ctx) => service.updateTask({ id: ctx.id, text: ctx.text, cwd: ctx.cwd }),
112
+ render: (d) => `已更新主题「${d.name}」#${d.task.id}: ${d.task.text}`,
106
113
  },
107
114
  {
108
115
  id: 'sessiontask.remove',
109
116
  cli: ['sessiontask', 'remove'],
110
117
  http: ['DELETE', '/api/sessiontasks/item'],
111
- summary: '删除一条任务(按 --id,定位到 --summary 现状)',
112
- flags: { id: { type: 'number', required: true, hint: '任务编号' }, summary: SUMMARY },
113
- run: (ctx) => service.removeTask({ id: ctx.id, summary: ctx.summary }),
114
- render: (d) => `已删除 #${d.removed.id}: ${d.removed.text}`,
118
+ summary: '删除一条任务(按 --id,定位到 --cwd 主题)',
119
+ flags: { id: { type: 'number', required: true, hint: '任务编号' }, cwd: CWD },
120
+ run: (ctx) => service.removeTask({ id: ctx.id, cwd: ctx.cwd }),
121
+ render: (d) => `已删除主题「${d.name}」#${d.removed.id}: ${d.removed.text}`,
122
+ },
123
+
124
+ {
125
+ id: 'sessiontask.config',
126
+ cli: ['sessiontask', 'config'],
127
+ http: ['PATCH', '/api/sessiontasks/setting'],
128
+ summary: '配置等待超时(秒;推荐 180=3 分钟,最大 600=10 分钟,超上限自动截断)',
129
+ flags: { timeout: { type: 'number', required: true, hint: '超时秒数(1-600)' } },
130
+ run: (ctx) => service.updateSettings({ timeoutSec: ctx.timeout }),
131
+ render: (s) => `已配置等待超时 ${s.timeoutSec} 秒(推荐 ${s.recommendedSec},最大 ${s.maxSec})`,
115
132
  },
116
133
 
117
134
  // ─── 阻塞等待(纯 CLI,agent 本轮最后一步)────────────────
@@ -119,16 +136,18 @@ export default {
119
136
  id: 'sessiontask.wait',
120
137
  cli: ['sessiontask', 'wait'],
121
138
  http: null,
122
- summary: '针对当前现状阻塞等待用户回填(默认 120 秒,超时结束本轮)',
139
+ summary: '针对某主题(按 cwd 目录名识别)阻塞等待用户回填(默认 3 分钟,最大 10 分钟)',
123
140
  flags: {
141
+ cwd: CWD,
124
142
  summary: SUMMARY,
125
- timeout: { type: 'number', default: 120, hint: '阻塞上限(秒),默认 120' },
143
+ timeout: { type: 'number', hint: '阻塞上限(秒);缺省用页面配置(推荐 180,最大 600)' },
126
144
  'no-open': { type: 'boolean', hint: '不自动打开浏览器(测试用)' },
127
145
  },
128
146
  run: (ctx) =>
129
147
  service.waitForTask({
148
+ cwd: ctx.cwd,
130
149
  summary: ctx.summary,
131
- timeoutMs: ctx.timeout * 1000,
150
+ timeoutSec: ctx.timeout,
132
151
  open: !ctx['no-open'],
133
152
  }),
134
153
  render: renderWait,
@@ -3,7 +3,10 @@
3
3
  // 注意:本文件返回的是一整段 HTML 字符串,内联 <script> 刻意只用单/双引号与
4
4
  // 字符串拼接(不用模板字符串、不用 ${}),以免与外层模板串冲突;内联脚本里
5
5
  // 出现的反斜杠在此外层模板中需双写(如换行正则写成 /\\n/g)。
6
- export function renderPopupPage({ key, summary, timeoutMs }) {
6
+ import { basename } from 'node:path';
7
+
8
+ export function renderPopupPage({ key, cwd, summary, timeoutMs }) {
9
+ const name = (cwd && basename(cwd)) || '';
7
10
  return `<!doctype html>
8
11
  <html lang="zh-CN">
9
12
  <head>
@@ -36,6 +39,14 @@ export function renderPopupPage({ key, summary, timeoutMs }) {
36
39
  .meta { margin-left: auto; color: var(--mid); font-family: ui-monospace, Consolas, monospace; font-size: 11.5px; }
37
40
  .meta .pill { background: var(--soft-2); border-radius: 5px; padding: 1px 7px; color: var(--ink); }
38
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
+
39
50
  .state {
40
51
  background: var(--paper); border-radius: var(--radius); box-shadow: var(--shadow);
41
52
  border-left: 3px solid var(--ink); padding: 11px 14px; margin-bottom: 12px;
@@ -101,12 +112,18 @@ export function renderPopupPage({ key, summary, timeoutMs }) {
101
112
  <div class="meta"><span class="pill">回填后立即返回 agent</span></div>
102
113
  </div>
103
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
+
104
121
  <div class="state">
105
122
  <div class="state-label">当前现状</div>
106
123
  <div class="state-text" id="summary"></div>
107
124
  </div>
108
125
 
109
- <div class="statusbar">
126
+ <div class="statusbar" id="statusbar">
110
127
  <span class="dot"></span>
111
128
  <span>agent 正在等待你针对上述现状回填 · 剩余</span>
112
129
  <span id="clock">--:--</span>
@@ -125,7 +142,7 @@ export function renderPopupPage({ key, summary, timeoutMs }) {
125
142
  <div class="banner" id="banner"></div>
126
143
 
127
144
  <div class="card">
128
- <div class="colhead"><h3>本现状的任务表</h3><span class="muted" id="count"></span></div>
145
+ <div class="colhead"><h3>本主题的任务队列</h3><span class="muted" id="count"></span></div>
129
146
  <table>
130
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>
131
148
  <tbody id="list"></tbody>
@@ -135,6 +152,8 @@ export function renderPopupPage({ key, summary, timeoutMs }) {
135
152
 
136
153
  <script>
137
154
  var KEY = ${JSON.stringify(key)};
155
+ var CWD = ${JSON.stringify(cwd || '')};
156
+ var TOPIC_NAME = ${JSON.stringify(name || '')};
138
157
  var SUMMARY = ${JSON.stringify(summary)};
139
158
  var TIMEOUT_MS = ${JSON.stringify(timeoutMs)};
140
159
  var delivered = false;
@@ -152,9 +171,12 @@ export function renderPopupPage({ key, summary, timeoutMs }) {
152
171
  return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
153
172
  }
154
173
 
174
+ el('topicName').textContent = TOPIC_NAME || '(未命名)';
175
+ el('topicCwd').textContent = CWD || '(旧数据,无目录)';
155
176
  el('summary').textContent = SUMMARY;
156
177
  var deadline = Date.now() + TIMEOUT_MS;
157
178
  function tickClock() {
179
+ if (delivered) return;
158
180
  var left = deadline - Date.now();
159
181
  el('clock').textContent = fmtClock(left);
160
182
  if (left <= 0 && !delivered) showBanner('等待已结束(超时未回填),本窗口可关闭。', true);
@@ -168,9 +190,9 @@ export function renderPopupPage({ key, summary, timeoutMs }) {
168
190
  b.className = 'banner show ' + (bad ? 'bad' : 'good');
169
191
  }
170
192
 
171
- function render(slot) {
172
- el('count').textContent = '待领取 ' + slot.pending + ' · 共 ' + slot.tasks.length;
173
- var tasks = slot.tasks.slice().reverse();
193
+ function render(topic) {
194
+ el('count').textContent = '待领取 ' + topic.pending + ' · 共 ' + topic.tasks.length;
195
+ var tasks = topic.tasks.slice().reverse();
174
196
  var rows = '';
175
197
  if (!tasks.length) {
176
198
  rows = '<tr><td colspan="5" class="muted empty">(暂无任务,在上方回填第一条)</td></tr>';
@@ -181,7 +203,7 @@ export function renderPopupPage({ key, summary, timeoutMs }) {
181
203
  ? '<span class="tag strong">已领取</span>'
182
204
  : '<span class="tag">待领取</span>';
183
205
  var body = esc(t.text).replace(/\\n/g, '<br>');
184
- rows += '<tr><td class="mono">#' + t.id + '</td><td class="text">' + body + '</td><td>' + status +
206
+ rows += '<tr><td class="mono">#' + t.id + '</td><td class="text">' + body + '<\/td><td>' + status +
185
207
  '</td><td class="mono time">' + esc(t.consumedAt || t.createdAt || '') +
186
208
  '</td><td class="ops"><button class="btn small ghost" data-id="' + t.id + '">删除</button></td></tr>';
187
209
  }
@@ -194,11 +216,11 @@ export function renderPopupPage({ key, summary, timeoutMs }) {
194
216
  .then(function (r) { return r.json(); })
195
217
  .then(function (j) {
196
218
  if (!j.ok) return;
197
- var slot = null;
198
- for (var i = 0; i < j.data.slots.length; i++) {
199
- if (j.data.slots[i].key === KEY) { slot = j.data.slots[i]; break; }
219
+ var topic = null;
220
+ for (var i = 0; i < j.data.topics.length; i++) {
221
+ if (j.data.topics[i].key === KEY) { topic = j.data.topics[i]; break; }
200
222
  }
201
- if (slot) render(slot);
223
+ if (topic) render(topic);
202
224
  })
203
225
  .catch(function () { /* 交付后服务器关闭,静默 */ });
204
226
  }
@@ -221,9 +243,11 @@ export function renderPopupPage({ key, summary, timeoutMs }) {
221
243
  delivered = true;
222
244
  showBanner('已回填给 agent,正在返回,本窗口可关闭。', false);
223
245
  el('composer').style.opacity = '0.5';
246
+ // agent 已领取:不再显示倒计时(倒计时只在「等待回填」时有意义)
247
+ el('statusbar').style.display = 'none';
224
248
  refresh();
225
249
  } else {
226
- showBanner('已排队,agent 下一次 wait 会取走。', false);
250
+ showBanner('已排入主题队列,agent 下一次 wait 会取走。', false);
227
251
  refresh();
228
252
  }
229
253
  })