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 +27 -0
- package/package.json +1 -1
- package/src/modules/sessiontask/index.js +53 -34
- package/src/modules/sessiontask/popup.js +36 -12
- package/src/modules/sessiontask/service.js +259 -137
- package/src/modules/sessiontask/view.jsx +89 -34
- package/src/web/public/assets/{CliHints-CDKMmZbP.js → CliHints-uQDPykkl.js} +1 -1
- package/src/web/public/assets/{index-DW8t26MP.js → index-C3LDUTKW.js} +2 -2
- package/src/web/public/assets/{view-CqGxh_0X.js → view-1sDSMusO.js} +1 -1
- package/src/web/public/assets/view-BgQvKmWZ.js +1 -0
- package/src/web/public/assets/{view-CdtYjY9W.js → view-CZkcHw_6.js} +1 -1
- package/src/web/public/assets/{view-C_qjdm2x.js → view-UqnZYH9n.js} +1 -1
- package/src/web/public/index.html +1 -1
- package/src/web/public/assets/view-CLDx6X3W.js +0 -1
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
|
@@ -2,28 +2,32 @@
|
|
|
2
2
|
//
|
|
3
3
|
// 声明了 `resource: 'sessiontask'`,故 CRUD 五操作齐备且两端可调用
|
|
4
4
|
// (tests/unit/registry.test.mjs 据此断言)。核心动作是纯 CLI 的
|
|
5
|
-
// `sessiontask wait --summary "
|
|
6
|
-
//
|
|
7
|
-
// agent
|
|
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
|
|
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 = [
|
|
20
|
-
if (!d.
|
|
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
|
|
25
|
-
const tag =
|
|
26
|
-
|
|
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
|
|
44
|
-
:
|
|
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)}
|
|
54
|
+
return `等待超时(${Math.round(r.timeoutMs / 1000)} 秒未回填):主题「${r.cwd}」现状「${r.summary}」结束本轮。`;
|
|
51
55
|
}
|
|
52
56
|
const origin = r.queued ? '排队任务' : '实时回填';
|
|
53
|
-
return
|
|
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
|
|
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,定位到 --
|
|
80
|
-
flags: { id: { type: 'number', required: true, hint: '任务编号' },
|
|
81
|
-
run: (ctx) => service.getTask({ id: ctx.id,
|
|
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: '
|
|
95
|
+
summary: '向某主题(工作目录)的队列回填一条任务(有 wait 在等则即时投递,否则排队)',
|
|
89
96
|
args: ['text'],
|
|
90
|
-
flags: { 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,定位到 --
|
|
105
|
+
summary: '编辑任务内容(按 --id,定位到 --cwd 主题)',
|
|
99
106
|
flags: {
|
|
100
107
|
id: { type: 'number', required: true, hint: '任务编号' },
|
|
101
108
|
text: { type: 'string', required: true, hint: '改成这个内容' },
|
|
102
|
-
|
|
109
|
+
cwd: CWD,
|
|
103
110
|
},
|
|
104
|
-
run: (ctx) => service.updateTask({ id: ctx.id, text: ctx.text,
|
|
105
|
-
render: (d) =>
|
|
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,定位到 --
|
|
112
|
-
flags: { id: { type: 'number', required: true, hint: '任务编号' },
|
|
113
|
-
run: (ctx) => service.removeTask({ id: ctx.id,
|
|
114
|
-
render: (d) =>
|
|
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: '
|
|
139
|
+
summary: '针对某主题(按 cwd 目录名识别)阻塞等待用户回填(默认 3 分钟,最大 10 分钟)',
|
|
123
140
|
flags: {
|
|
141
|
+
cwd: CWD,
|
|
124
142
|
summary: SUMMARY,
|
|
125
|
-
timeout: { type: 'number',
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
172
|
-
el('count').textContent = '待领取 ' +
|
|
173
|
-
var tasks =
|
|
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 + '
|
|
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
|
|
198
|
-
for (var i = 0; i < j.data.
|
|
199
|
-
if (j.data.
|
|
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 (
|
|
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('
|
|
250
|
+
showBanner('已排入主题队列,agent 下一次 wait 会取走。', false);
|
|
227
251
|
refresh();
|
|
228
252
|
}
|
|
229
253
|
})
|