nx-kv 0.2.0 → 0.3.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +32 -4
  3. package/assets/kv-sessiontask/SKILL.md +106 -0
  4. package/assets/kv-sessiontask/references/sessiontask-workflow.md +127 -0
  5. package/package.json +5 -3
  6. package/src/index.js +1 -0
  7. package/src/modules/sessiontask/index.js +137 -0
  8. package/src/modules/sessiontask/popup.js +256 -0
  9. package/src/modules/sessiontask/service.js +476 -0
  10. package/src/modules/sessiontask/view.jsx +239 -0
  11. package/src/runtime/registry.js +2 -1
  12. package/src/web/frontend/App.jsx +1 -1
  13. package/src/web/frontend/index.html +6 -2
  14. package/src/web/frontend/public/favicon-16.png +0 -0
  15. package/src/web/frontend/public/favicon-32.png +0 -0
  16. package/src/web/frontend/public/favicon-48.png +0 -0
  17. package/src/web/frontend/public/favicon.ico +0 -0
  18. package/src/web/frontend/public/logo-rounded.png +0 -0
  19. package/src/web/frontend/public/logo.png +0 -0
  20. package/src/web/frontend/registry.js +1 -0
  21. package/src/web/public/assets/{CliHints-Bc3Me3vO.js → CliHints-CDKMmZbP.js} +1 -1
  22. package/src/web/public/assets/{index-Dv9g9jmR.js → index-DW8t26MP.js} +2 -2
  23. package/src/web/public/assets/view-CLDx6X3W.js +1 -0
  24. package/src/web/public/assets/{view-BKTVRs9U.js → view-C_qjdm2x.js} +1 -1
  25. package/src/web/public/assets/{view-Cdi5TUV1.js → view-CdtYjY9W.js} +1 -1
  26. package/src/web/public/assets/{view-B8AnDK0i.js → view-CqGxh_0X.js} +1 -1
  27. package/src/web/public/favicon-16.png +0 -0
  28. package/src/web/public/favicon-32.png +0 -0
  29. package/src/web/public/favicon-48.png +0 -0
  30. package/src/web/public/favicon.ico +0 -0
  31. package/src/web/public/index.html +7 -3
  32. package/src/web/public/logo-rounded.png +0 -0
  33. package/src/web/public/logo.png +0 -0
@@ -0,0 +1,239 @@
1
+ // 实时任务页:等待点(按现状总结分散)总览 + 选中某个现状回填 + 该现状任务表。
2
+ // 本质是「agent 提交结果 → 阻塞 → 用户在 web 针对现状回填 → agent 接收」。
3
+ import { useCallback, useEffect, useState } from 'react';
4
+ import { api } from '../../web/frontend/api/client.js';
5
+ import { useToast, useGuard } from '../../web/frontend/components/ui.jsx';
6
+ import { CliHints } from '../../web/frontend/components/CliHints.jsx';
7
+
8
+ // “YYYY-MM-DD HH:mm:ss” → 本地 Date
9
+ function parseStamp(s) {
10
+ const m = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/.exec(s || '');
11
+ if (!m) return null;
12
+ return new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]);
13
+ }
14
+ function countdown(s) {
15
+ const d = parseStamp(s);
16
+ if (!d) return '';
17
+ const ms = d.getTime() - Date.now();
18
+ if (ms <= 0) return '等待中 0:00';
19
+ const mm = Math.floor(ms / 60000);
20
+ const ss = Math.floor((ms % 60000) / 1000);
21
+ return `等待中 ${mm}:${String(ss).padStart(2, '0')}`;
22
+ }
23
+
24
+ export default function SessionTaskView() {
25
+ const toast = useToast();
26
+ const guard = useGuard();
27
+
28
+ const [overview, setOverview] = useState(null);
29
+ const [activeKey, setActiveKey] = useState(null);
30
+ const [text, setText] = useState('');
31
+ const [saving, setSaving] = useState(false);
32
+
33
+ const load = useCallback(async () => {
34
+ try {
35
+ setOverview(await api('/api/sessiontasks'));
36
+ } catch (e) {
37
+ toast(e.message);
38
+ }
39
+ }, [toast]);
40
+
41
+ useEffect(() => {
42
+ load();
43
+ }, [load]);
44
+
45
+ // 实时刷新:等待点状态/倒计时、任务是否被取走,每 1.5s 轮询
46
+ useEffect(() => {
47
+ const t = setInterval(load, 1500);
48
+ return () => clearInterval(t);
49
+ }, [load]);
50
+
51
+ const slots = overview ? overview.slots : [];
52
+
53
+ // 默认选中:优先等待中的,否则第一个;用户已选且仍存在则保留
54
+ useEffect(() => {
55
+ if (!slots.length) {
56
+ setActiveKey(null);
57
+ return;
58
+ }
59
+ if (activeKey && slots.some((s) => s.key === activeKey)) return;
60
+ const w = slots.find((s) => s.waiting) || slots[0];
61
+ setActiveKey(w.key);
62
+ }, [slots, activeKey]);
63
+
64
+ const active = slots.find((s) => s.key === activeKey) || null;
65
+
66
+ const submit = () =>
67
+ guard(async () => {
68
+ const body = text.trim();
69
+ if (!body) {
70
+ toast('请输入任务内容');
71
+ return;
72
+ }
73
+ if (!active) {
74
+ toast('请先选择一个等待点(现状)');
75
+ return;
76
+ }
77
+ setSaving(true);
78
+ try {
79
+ const r = await api('/api/sessiontasks', {
80
+ method: 'POST',
81
+ body: { summary: active.summary, text: body },
82
+ });
83
+ setText('');
84
+ toast(r.delivered ? '已返回给等待中的 agent' : '已排队,等待 agent 下一次 wait');
85
+ await load();
86
+ } finally {
87
+ setSaving(false);
88
+ }
89
+ });
90
+
91
+ const remove = (id) =>
92
+ guard(async () => {
93
+ await api('/api/sessiontasks/item', {
94
+ method: 'DELETE',
95
+ body: { summary: active.summary, id },
96
+ });
97
+ toast('已删除');
98
+ await load();
99
+ });
100
+
101
+ const tasks = active ? active.tasks.slice().reverse() : [];
102
+
103
+ return (
104
+ <>
105
+ <div className="card" style={{ padding: '10px 14px' }}>
106
+ <div className="muted" style={{ fontSize: 12 }}>
107
+ agent 用法:完成当前任务后,把
108
+ <code> nx-kv sessiontask wait --summary "&lt;当前现状总结&gt;" --json </code>
109
+ 作为本轮<strong>最后一个动作</strong>调用——现状总结即等待点 key,命令阻塞并
110
+ 自动打开浏览器弹窗;用户针对该现状回填后解除阻塞、返回新任务;120 秒未回填
111
+ 则返回 <code>status:"timeout"</code>,agent 收尾结束本轮。
112
+ </div>
113
+ </div>
114
+
115
+ <div className="colhead">
116
+ <h3>等待点总览</h3>
117
+ <span className="muted">{overview ? `${slots.length} 个 · 等待中 ${overview.waiting}` : ''}</span>
118
+ </div>
119
+
120
+ <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', marginBottom: 14 }}>
121
+ {slots.length ? (
122
+ slots.map((s) => {
123
+ const isActive = s.key === activeKey;
124
+ return (
125
+ <button
126
+ key={s.key}
127
+ onClick={() => setActiveKey(s.key)}
128
+ className="card"
129
+ style={{
130
+ textAlign: 'left',
131
+ width: 250,
132
+ padding: '10px 12px',
133
+ cursor: 'pointer',
134
+ font: 'inherit',
135
+ color: 'inherit',
136
+ border: isActive ? '1px solid var(--ink,#14161a)' : '1px solid transparent',
137
+ boxShadow: isActive
138
+ ? '0 0 0 2px rgba(20,22,26,.12)'
139
+ : '0 1px 2px rgba(20,22,26,.06), 0 6px 16px rgba(20,22,26,.05)',
140
+ }}
141
+ >
142
+ <div style={{ fontWeight: 700, whiteSpace: 'pre-wrap', marginBottom: 6 }}>{s.summary}</div>
143
+ <div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
144
+ {s.waiting ? (
145
+ <span className="tag strong">{countdown(s.waitingUntil)}</span>
146
+ ) : (
147
+ <span className="tag">已结束</span>
148
+ )}
149
+ <span className="muted" style={{ fontSize: 11 }}>
150
+ 待领取 {s.pending}/{s.total}
151
+ </span>
152
+ </div>
153
+ </button>
154
+ );
155
+ })
156
+ ) : (
157
+ <div className="card muted" style={{ padding: '20px 16px', width: '100%', textAlign: 'center' }}>
158
+ (暂无等待点;agent 调用 sessiontask wait 后会出现在这里)
159
+ </div>
160
+ )}
161
+ </div>
162
+
163
+ {active && (
164
+ <>
165
+ <div className="toolbar">
166
+ <span style={{ fontWeight: 700, maxWidth: 380, whiteSpace: 'pre-wrap' }} title={active.summary}>
167
+ {active.summary}
168
+ </span>
169
+ <span className="sep"></span>
170
+ <textarea
171
+ style={{ flex: 1, minWidth: 240, minHeight: 40, resize: 'vertical' }}
172
+ spellCheck="false"
173
+ value={text}
174
+ onChange={(e) => setText(e.target.value)}
175
+ onKeyDown={(e) => {
176
+ if (e.key === 'Enter' && !e.shiftKey) {
177
+ e.preventDefault();
178
+ submit();
179
+ }
180
+ }}
181
+ placeholder="针对该现状回填下一条任务(Enter 提交,Shift+Enter 换行)"
182
+ />
183
+ <button className="btn" onClick={submit} disabled={saving}>
184
+ {saving ? '提交中…' : '回填新任务'}
185
+ </button>
186
+ </div>
187
+
188
+ <div className="colhead">
189
+ <h3>该现状的任务表</h3>
190
+ <span className="muted">待领取 {active.pending} · 共 {active.total} 条</span>
191
+ </div>
192
+
193
+ <div className="card" style={{ padding: 4 }}>
194
+ <table>
195
+ <thead>
196
+ <tr>
197
+ <th style={{ width: 56 }}>#</th>
198
+ <th>任务</th>
199
+ <th style={{ width: 80 }}>状态</th>
200
+ <th style={{ width: 160 }}>时间</th>
201
+ <th style={{ width: 76 }}>操作</th>
202
+ </tr>
203
+ </thead>
204
+ <tbody>
205
+ {tasks.length ? (
206
+ tasks.map((t) => (
207
+ <tr key={t.id}>
208
+ <td className="mono">#{t.id}</td>
209
+ <td style={{ whiteSpace: 'pre-wrap' }}>{t.text}</td>
210
+ <td>
211
+ {t.status === 'consumed' ? (
212
+ <span className="tag strong">已领取</span>
213
+ ) : (
214
+ <span className="tag">待领取</span>
215
+ )}
216
+ </td>
217
+ <td className="mono">{t.consumedAt || t.createdAt}</td>
218
+ <td className="ops">
219
+ <button className="btn small ghost" onClick={() => remove(t.id)}>删除</button>
220
+ </td>
221
+ </tr>
222
+ ))
223
+ ) : (
224
+ <tr>
225
+ <td colSpan="5" className="muted" style={{ textAlign: 'center', padding: '22px 0' }}>
226
+ (暂无任务,在上方回填第一条)
227
+ </td>
228
+ </tr>
229
+ )}
230
+ </tbody>
231
+ </table>
232
+ </div>
233
+ </>
234
+ )}
235
+
236
+ <CliHints module="sessiontask" />
237
+ </>
238
+ );
239
+ }
@@ -12,10 +12,11 @@ import { cliPathsOf } from './spec.js';
12
12
  import system from '../modules/system/index.js';
13
13
  import auth from '../modules/auth/index.js';
14
14
  import group from '../modules/group/index.js';
15
+ import sessiontask from '../modules/sessiontask/index.js';
15
16
  import todo from '../modules/todo/index.js';
16
17
  import bundled from '../modules/bundled/index.js';
17
18
 
18
- export const MODULES = [system, auth, group, todo, bundled].sort(
19
+ export const MODULES = [system, auth, group, sessiontask, todo, bundled].sort(
19
20
  (a, b) => (a.order ?? 100) - (b.order ?? 100)
20
21
  );
21
22
 
@@ -41,7 +41,7 @@ export default function App() {
41
41
  return (
42
42
  <>
43
43
  <header>
44
- <div className="brand"><img src="/logo-rounded.png" alt="" />nx-kv<span className="sub">nx-kv</span></div>
44
+ <div className="brand"><img src="/logo-rounded.png" alt="nx-kv" />nx-kv<span className="sub">nx-kv</span></div>
45
45
  <nav>
46
46
  {VIEWS.map((v) => (
47
47
  <button
@@ -3,11 +3,15 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <!-- 取图标主色:移动端把地址栏染成同一个青绿,标签页与页面看起来是一件东西 -->
7
+ <meta name="theme-color" content="#0f7a72">
6
8
  <title>nx-kv · nx-kv</title>
7
- <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png">
8
9
  <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16.png">
10
+ <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png">
11
+ <link rel="icon" type="image/png" sizes="48x48" href="/favicon-48.png">
12
+ <!-- .ico 兜底:浏览器不认上面的 <link> 时会自己请求 /favicon.ico -->
9
13
  <link rel="icon" type="image/x-icon" href="/favicon.ico">
10
- <link rel="apple-touch-icon" href="/logo-rounded.png">
14
+ <link rel="apple-touch-icon" sizes="512x512" href="/logo-rounded.png">
11
15
  <link rel="stylesheet" href="/style.css">
12
16
  </head>
13
17
  <body>
Binary file
@@ -6,6 +6,7 @@
6
6
  import { lazy } from 'react';
7
7
 
8
8
  export const VIEWS = [
9
+ { id: 'sessiontask', title: '实时任务', component: lazy(() => import('../../modules/sessiontask/view.jsx')) },
9
10
  { id: 'todo', title: '清单', component: lazy(() => import('../../modules/todo/view.jsx')) },
10
11
  { id: 'group', title: '工作空间', component: lazy(() => import('../../modules/group/view.jsx')) },
11
12
  { id: 'auth', title: '登录', component: lazy(() => import('../../modules/auth/view.jsx')) },
@@ -1 +1 @@
1
- import{o as e,s as t,t as n}from"./index-Dv9g9jmR.js";var r=t();function i({module:t}){let{boot:i}=e(),a=(i?.commands||[]).filter(e=>e.module===t);return a.length?(0,r.jsxs)(`div`,{className:`cli-hint`,children:[(0,r.jsx)(`span`,{className:`cli-hint-label`,children:`CLI 等价(同构命令,加 --json 得机器可读输出):`}),a.map(e=>(0,r.jsx)(n,{className:`cli-cmd`,text:e.command,title:`点击复制:${e.usage}`,children:e.command},e.id))]}):null}export{i as t};
1
+ import{o as e,s as t,t as n}from"./index-DW8t26MP.js";var r=t();function i({module:t}){let{boot:i}=e(),a=(i?.commands||[]).filter(e=>e.module===t);return a.length?(0,r.jsxs)(`div`,{className:`cli-hint`,children:[(0,r.jsx)(`span`,{className:`cli-hint-label`,children:`CLI 等价(同构命令,加 --json 得机器可读输出):`}),a.map(e=>(0,r.jsx)(n,{className:`cli-cmd`,text:e.command,title:`点击复制:${e.usage}`,children:e.command},e.id))]}):null}export{i as t};