nx-kv 0.7.0 → 0.8.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.
@@ -1,23 +1,24 @@
1
- # sessiontask-workflow — 阻塞等待用户回填任务的完整 SOP
1
+ # kv-waittask-workflow — 阻塞等待用户回填任务的完整 SOP
2
2
 
3
- > 归属 [[kv-sessiontask]]。当你要在一个对话里**不结束本轮**、靠一个阻塞工具
4
- > 调用等用户在浏览器里继续回填任务时,按本 SOP 走。
3
+ > 归属 [[kv-waittask]]。当你要在一个对话里**不结束本轮**、靠一个阻塞工具
4
+ > 调用等浏览器里继续回填任务时,按本 SOP 走。
5
5
 
6
6
  ## 一、端到端时序
7
7
 
8
8
  ```
9
9
  1. agent 正常干活(分析、读写文件、调用其它工具)。
10
- 2. 当前任务做完、提交阶段结果,需要用户给下一条时:
11
- → 把「nx-kv sessiontask wait --summary "<当前现状总结>" --json」
12
- 作为本轮【最后一个动作】调用;工具调用超时设为 120000 ms。
10
+ 2. 当前任务做完、提交阶段结果,需要下一条输入时:
11
+ → 把「nx-kv localtask wait --summary "<当前现状总结>" --json」
12
+ 作为本轮【最后一个动作】调用;工具调用超时设为 300000 ms。
13
13
  3. wait 的行为:
14
- a. 若该现状的等待点已有用户提前排队的 pending → 立即取走、不开窗、不阻塞;
14
+ a. 若本目录主题队列已有待领取任务(含 kv-submit 委托排队的)→ 立即取走、
15
+ 不开窗、不阻塞(queued:true);
15
16
  b. 否则在本机临时起一个 http 服务器(127.0.0.1,随机端口),
16
17
  自动打开浏览器弹窗,然后阻塞。
17
18
  4. 用户在弹窗/面板看清现状,回填新任务 → 提交(Enter 或按钮)。
18
19
  5. wait 解除阻塞,stdout(--json) 返回 {status:"ok", task},进程退出。
19
20
  6. agent 从工具结果读 task.text,继续干活、提交结果;做完回到第 2 步——循环。
20
- 7. 若 120 秒内没有回填:wait 返回 {status:"timeout"}(exit 0),
21
+ 7. 若阻塞时限(默认 180 秒)内没有回填:wait 返回 {status:"timeout"}(exit 0),
21
22
  agent 不再等待,收尾并结束本轮。
22
23
  ```
23
24
 
@@ -25,36 +26,38 @@
25
26
  回填正是通过这个工具结果进入对话的,等于用工具调用替代了在原始输入框打字,
26
27
  agent 这一轮不会因为「等输入」而提前结束。
27
28
 
28
- ## 二、现状总结就是 key(怎么写)
29
+ ## 二、现状总结怎么写
29
30
 
30
- wait 必带一句当前现状总结,它被归一化(折叠首尾/连续空白)后哈希成等待点 key:
31
+ wait 必带一句当前现状总结(≤2000 字,连续空白会折叠为单空格):
31
32
 
32
- - **回到同一个现状 → 用同一句总结(逐字一致)**,复用同一个等待点;首尾空格、
33
- 连续空格的差异会被忽略,不影响 key。
34
- - **进入新的现状 → 换一句新总结**,自然形成新的等待点。
33
+ - 它**只是状态展示**:显示在弹窗/面板顶部,并作为「本轮完成总结」记录在被取走
34
+ 的任务上(`task.roundSummary`),供用户和后续追溯。
35
+ - **它不参与队列分桶**:领取顺序只由本目录队列的先后决定,与措辞无关——同一
36
+ 目录的多次 wait(即便现状措辞不同)共用同一个任务队列。所以不必刻意保持
37
+ 逐字一致,每次如实写当前现状即可。
35
38
  - 总结要让用户**不读整段对话也能快速了解现状**,建议包含三要素:
36
39
  1. 已经做到哪一步 / 产出了什么;
37
40
  2. 当前卡在哪、有哪些可选项;
38
41
  3. 具体等用户决定或补充什么。
39
- - 一句话为宜,≤200 字。示例:
42
+ - 一句话为宜。示例:
40
43
  - `已完成登录,等待选择工作空间`
41
44
  - `已生成 3 个方案(A/B/C),等待你选一个再细化`
42
45
  - `面板已能提交,但删除还没接,等待确认是否现在补`
43
46
 
44
- 展示给用户的始终是现状原文;key 只是内部标识,agent 不需要自己算 key。
47
+ ## 三、队列语义(按目录划分、可提前排队)
45
48
 
46
- ## 三、排队语义(用户可以提前投递)
47
-
48
- - 用户在弹窗、常驻面板「实时任务」页或用 `sessiontask add` 提前提交的回填,会
49
- 以 `pending` 进入对应现状的等待点。
50
- - wait 总是取走该等待点**最早的一条 pending**,标记为已领取(consumed)后返回。
51
- - 所以:
52
- - 等待点非空 → wait 立刻返回(`queued:true`),连弹窗都不打开;
53
- - 等待点为空 → 才开窗阻塞(`delivered:true`)。
49
+ - **工作目录即主题**:wait 不传 `--cwd` 时按进程当前目录的**目录名**定位主题
50
+ (Windows 折叠大小写,同名目录共用队列);`add` 可用 `--cwd` 给任意目录的
51
+ 队列排任务。不同目录 → 不同主题 → 队列互不串。
52
+ - 用户在弹窗、常驻面板「本地任务」页、或别人用 `localtask add`(kv-submit)
53
+ 提交的回填,都会以 `pending` 进入对应目录的队列。
54
+ - wait 总是取走该队列**最早的一条 pending**,标记为已领取(consumed)后返回:
55
+ - 队列非空 → wait 立刻返回(`queued:true`),连弹窗都不打开;
56
+ - 队列为空 → 才开窗阻塞(`delivered:true`)。
54
57
  - 已领取的任务保留为历史(状态「已领取」),不会被二次取走。
55
58
 
56
- 这让「用户趁 agent 还在忙,先把后面几条排好」成为可能:agent 每次 wait 都能
57
- 立刻拿到下一条,不打断节奏。
59
+ 这让「趁 agent 还在忙,先把后面几条排好」成为可能:agent 每次 wait 都能立刻
60
+ 拿到下一条,不打断节奏。
58
61
 
59
62
  ## 四、返回形状与退出码
60
63
 
@@ -63,26 +66,29 @@ wait 必带一句当前现状总结,它被归一化(折叠首尾/连续空
63
66
  ```json
64
67
  {
65
68
  "status": "ok",
66
- "key": "3f9a2c1d7e8b4a10",
69
+ "cwd": "D:\\code\\a_js\\proj\\demo",
67
70
  "summary": "已完成登录,等待选择工作空间",
68
71
  "task": { "id": 3, "text": "选择工作空间 A", "status": "consumed",
69
- "createdAt": "2026-10-06 10:00:00", "consumedAt": "2026-10-06 10:00:04" },
72
+ "createdAt": "2026-10-11 10:00:00", "consumedAt": "2026-10-11 10:00:04",
73
+ "roundSummary": "已完成登录,等待选择工作空间" },
70
74
  "delivered": true,
71
75
  "queued": false
72
76
  }
73
77
  ```
74
78
 
75
- - `delivered:true` = 实时回填;`queued:true` = 取到提前排队的任务(二者互斥)。
79
+ - `delivered:true` = 实时回填;`queued:true` = 取到队列里排队的任务(二者互斥)。
76
80
  - **任务内容永远读 `task.text`**,多行换行原样保留。
77
81
 
78
82
  超时(exit 0,业务结果而非错误):
79
83
 
80
84
  ```json
81
- { "status": "timeout", "key": "3f9a2c1d7e8b4a10",
82
- "summary": "已完成登录,等待选择工作空间", "timeoutMs": 120000 }
85
+ { "status": "timeout", "cwd": "D:\\code\\a_js\\proj\\demo",
86
+ "summary": "已完成登录,等待选择工作空间", "timeoutMs": 180000 }
83
87
  ```
84
88
 
85
89
  - 超时**不是失败**:退出码 0,按「用户没继续」收尾,不要按错误处理。
90
+ - 阻塞时限:默认用页面配置(推荐 180 秒,最大 600 秒),可用 `--timeout <秒>`
91
+ 覆盖(超上限自动截断)。
86
92
  - 参数非法(如缺 `--summary`、缺 `--id`)才是错误(exit 1,文本带「用法」),
87
93
  按宿主失败流程处理。
88
94
 
@@ -91,17 +97,17 @@ wait 必带一句当前现状总结,它被归一化(折叠首尾/连续空
91
97
  - 顶部「当前现状」直接显示 agent 传入的现状总结,先看这句再决定回填什么。
92
98
  - 大输入框:回填下一条任务;**Enter 提交,Shift+Enter 换行**。
93
99
  - 状态栏倒计时显示还会等多久;归零未回填则本轮自动结束。
94
- - 下方是该现状的任务表,状态分「待领取 / 已领取」,可删除。
100
+ - 下方是该主题的任务表,状态分「待领取 / 已领取」,可删除。
95
101
  - 提交后显示「已回填给 agent,正在返回」即说明 wait 已取走、窗口可关闭;agent
96
102
  做完会再开下一个弹窗。
97
103
 
98
- ## 六、分散答复:多个等待点并存
104
+ ## 六、多主题并存
99
105
 
100
- - 不同现状总结 → 不同等待点。常驻面板「等待点总览」把它们并排展示,每张卡显示
101
- 现状、等待状态/倒计时、待领取数。
102
- - 用户点选任一等待点即可**分别回填**,也可给多个等待点提前排队——答复被分散到
103
- 各现状下,而不是挤在一个队列。
104
- - agent 用对应现状调 wait 时只取该等待点的任务,互不串。
106
+ - 每个工作目录一个主题;常驻面板「本地任务」页把所有主题并排展示,每张卡显示
107
+ 目录、当前现状、等待状态/倒计时、待领取数。
108
+ - 用户在任意主题卡上都能**分别回填**,也可趁 agent 在忙给几个目录提前排队。
109
+ - agent 在哪个目录跑 wait(或 `--cwd` 指哪个目录),就取哪个目录队列的任务,
110
+ 互不串。
105
111
 
106
112
  ## 七、删除主题与批量删除
107
113
 
@@ -109,10 +115,10 @@ wait 必带一句当前现状总结,它被归一化(折叠首尾/连续空
109
115
 
110
116
  - **单个删除**:
111
117
  - 面板:主题卡片右下角「删除主题」;
112
- - CLI:`nx-kv sessiontask topic remove --cwd "<工作目录>"`(或 `--key <key>`)。
118
+ - CLI:`nx-kv localtask topic remove --cwd "<工作目录>"`(或 `--key <key>`)。
113
119
  - **批量删除**:
114
120
  - 面板:勾选卡片左上角(或总览标题处「全选」)→「批量删除 (N)」;
115
- - CLI:`nx-kv sessiontask topic remove-many --keys "<key,key>"`,也可
121
+ - CLI:`nx-kv localtask topic remove-many --keys "<key,key>"`,也可
116
122
  `--cwds "<dir,dir>"`,或 `--all` 清空。
117
123
  - **安全保护**:等待中的主题默认不删(删了阻塞的 agent 只能等到超时);界面会
118
124
  二次确认,命令需加 `--force` 才连等待中一起删,agent 随后超时收尾。
@@ -124,20 +130,20 @@ wait 必带一句当前现状总结,它被归一化(折叠首尾/连续空
124
130
  | 错误做法 | 后果 | 正确做法 |
125
131
  | --- | --- | --- |
126
132
  | wait 后又安排别的工具/推理 | wait 阻塞,后续动作被卡住、顺序全乱 | wait 严格放在最后一步,之后只处理返回 |
127
- | 工具超时没设/设太短(如 60s) | 宿主在回填前杀掉调用,任务中断 | 工具超时设 `120000` ms |
128
- | wait 不带 / 空着 --summary | 无法定位等待点,命令报错退出 | 每次 wait 都写一句现状总结 |
129
- | 同一现状却每次换措辞 | 被当成多个等待点,排队任务取不到 | 同现状用同一句总结,新现状才换新句 |
130
- | 把不同阶段都写成同一句 | 不同现状无法分散,回填容易给错点 | 进入新现状就换新总结 |
133
+ | 工具超时没设 / 设成 120000 | CLI 默认阻塞 180 秒,宿主会在阻塞中途杀掉调用 | 工具超时设 `300000` ms;用更长的 `--timeout` 时同步放大 |
134
+ | wait 不带 / 空着 --summary | 无法展示现状,命令报错退出 | 每次 wait 都写一句现状总结 |
135
+ | 刻意维持现状措辞逐字一致 | 白费功夫——现状只是展示,不影响领取 | 每次如实写当前现状即可 |
136
+ | 在错误的目录调 wait(没传 --cwd) | 取到别的目录队列的任务,或开出一个没人看的弹窗 | 确认进程 cwd 或显式 `--cwd "<目标目录>"` |
131
137
  | 把 timeout 当报错、反复重试 | 用户想停却被反复弹窗 | timeout 即收尾结束本轮 |
132
138
  | 读 `task.id` 当任务内容 | id 只是编号,内容在 `task.text` | 一律读 `task.text` |
133
- | 同时开多个 wait(同一等待点) | 只有一个收到回填,另一个等到超时 | 同一等待点同一时刻只调一个 wait |
139
+ | 同时开多个 wait(同一目录主题) | 只有一个收到回填,另一个等到超时 | 同一目录同一时刻只调一个 wait |
134
140
  | 没用 --json | 输出人读文本,宿主难以可靠解析 | agent 调用一律加 `--json` |
135
141
 
136
142
  ## 九、与常驻面板配合 / 排障
137
143
 
138
- - 已 `nx-kv serve` 打开面板时,直接在「等待点总览」点选现状回填;正在阻塞的
144
+ - 已 `nx-kv serve` 打开面板时,直接在「本地任务」页点选主题回填;正在阻塞的
139
145
  wait 通过轮询(约每 0.5 秒)取走,不依赖弹窗本身。
140
146
  - 弹窗没自动打开时:终端输出里有本次地址(`http://127.0.0.1:<端口>/`),手动
141
147
  复制到浏览器打开;该窗口只在本次 wait 期间有效。
142
148
  - wait 结束(拿到任务或超时)后弹窗服务器随即关闭,窗口里再提交会失败——下一
143
- 条等 agent 下一次 wait 打开新窗口再给。
149
+ 条等 agent 下一次 wait 打开新窗口再给,或直接在常驻面板回填。
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nx-kv",
3
- "version": "0.7.0",
4
- "description": "nx-kv — KV 清单管理中枢 + 对话内实时任务接力:serve 驱动 CLI 与 Web 面板,同源同构,附带可安装 skill 供 agent 领取任务与阻塞接收用户输入",
3
+ "version": "0.8.0",
4
+ "description": "nx-kv — KV 清单管理中枢(远程)+ 本机本地任务队列(localtask):serve 驱动 CLI 与 Web 面板,同源同构,附带可安装 skill 供 agent 提交 / 领取 / 阻塞接收任务",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "nx-kv": "bin/cli.mjs"
package/src/index.js CHANGED
@@ -4,7 +4,7 @@ export * as config from './core/config.js';
4
4
  export * as kvapi from './core/kvapi.js';
5
5
  export * as errors from './core/errors.js';
6
6
  export * as todo from './modules/todo/service.js';
7
- export * as sessiontask from './modules/sessiontask/service.js';
7
+ export * as localtask from './modules/localtask/service.js';
8
8
 
9
9
  export { ACTIONS, MODULES } from './runtime/registry.js';
10
10
  export { startServer } from './runtime/server.js';
@@ -1,14 +1,16 @@
1
- // 实时任务输入模块:在一个对话里通过工具调用向 agent 实时回填任务,替代原始输入框。
1
+ // 本地任务(localtask)模块:本机按「工作目录(cwd)」划分的任务队列——提交(add)、
2
+ // 领取(wait)、查看/编辑/删除/调序(list/get/update/remove/reorder)与主题管理。
3
+ // 阻塞等待只是当前的一个子功能:agent 在一个对话里需要用户继续给任务时,不必结束
4
+ // 本轮、等用户去原始输入框打字,而是把
5
+ // `localtask wait --cwd "<工作目录>" --summary "<当前现状>"` 作为本轮**最后一步**调用:
6
+ // 命令阻塞、自动打开浏览器弹窗,用户回填后解除阻塞、把新任务作为结果返回,agent 接着干。
2
7
  //
3
- // 声明了 `resource: 'sessiontask'`,故 CRUD 五操作齐备且两端可调用
4
- // (tests/unit/registry.test.mjs 据此断言)。核心动作是纯 CLI 的
5
- // `sessiontask wait --cwd "<工作目录>" --summary "<当前现状>"`——agent 把它放在本轮
6
- // **最后一步**调用:阻塞、自动打开浏览器弹窗,用户回填后解除阻塞,把新任务返回给
7
- // agent。工作目录即主题、主题拥有任务队列:同目录共用队列、不同目录互不串,任务可
8
- // 在主题队列里堆积。
8
+ // 声明了 `resource: 'localtask'`,故 CRUD 五操作齐备且两端可调用
9
+ // (tests/unit/registry.test.mjs 据此断言)。工作目录即主题、主题拥有任务队列:
10
+ // 同目录共用队列、不同目录互不串,任务可在主题队列里堆积。
9
11
  //
10
- // 与 todo 的区别:todo 管 KV 后端上的清单(需登录);sessiontask 是**本机**
11
- // 实时人机接力,数据存本地文件,不依赖后端登录态。
12
+ // 与 todo 的区别:todo 管 KV 后端上的清单(需登录);localtask 是**本机**
13
+ // 任务队列,数据存本地文件,不依赖后端登录态。
12
14
  import * as service from './service.js';
13
15
 
14
16
  const CWD = { type: 'string', hint: '工作目录(作为主题;默认当前目录)' };
@@ -76,37 +78,37 @@ function renderRemoveMany(d) {
76
78
  }
77
79
 
78
80
  export default {
79
- id: 'sessiontask',
80
- title: '实时任务(= Web「实时任务」页)',
81
+ id: 'localtask',
82
+ title: '本地任务(= Web「本地任务」页)',
81
83
  order: 20,
82
84
  view: () => import('./view.jsx'),
83
85
 
84
86
  // CRUD 资源声明:主题任务的增删查改齐备,CLI 与面板都能调用
85
- resource: 'sessiontask',
87
+ resource: 'localtask',
86
88
 
87
89
  actions: [
88
90
  // ─── CRUD 五操作 ───────────────────────────────────────────
89
91
  {
90
- id: 'sessiontask.list',
91
- cli: ['sessiontask', 'list'],
92
- http: ['GET', '/api/sessiontasks'],
92
+ id: 'localtask.list',
93
+ cli: ['localtask', 'list'],
94
+ http: ['GET', '/api/localtasks'],
93
95
  summary: '总览全部主题(工作目录、现状、是否等待中、待领取数)',
94
96
  run: () => service.listTasks(),
95
97
  render: renderOverview,
96
98
  },
97
99
  {
98
- id: 'sessiontask.get',
99
- cli: ['sessiontask', 'get'],
100
- http: ['GET', '/api/sessiontasks/item'],
100
+ id: 'localtask.get',
101
+ cli: ['localtask', 'get'],
102
+ http: ['GET', '/api/localtasks/item'],
101
103
  summary: '查看单条任务(按 --id,定位到 --cwd 主题)',
102
104
  flags: { id: { type: 'number', required: true, hint: '任务编号' }, cwd: CWD },
103
105
  run: (ctx) => service.getTask({ id: ctx.id, cwd: ctx.cwd }),
104
106
  render: renderTask,
105
107
  },
106
108
  {
107
- id: 'sessiontask.add',
108
- cli: ['sessiontask', 'add'],
109
- http: ['POST', '/api/sessiontasks'],
109
+ id: 'localtask.add',
110
+ cli: ['localtask', 'add'],
111
+ http: ['POST', '/api/localtasks'],
110
112
  summary: '向某主题(工作目录)的队列回填一条任务(有 wait 在等则即时投递,否则排队)',
111
113
  args: ['text'],
112
114
  flags: { cwd: CWD, summary: { type: 'string', hint: '顺带刷新该主题的当前现状(可选)' } },
@@ -114,9 +116,9 @@ export default {
114
116
  render: renderAdd,
115
117
  },
116
118
  {
117
- id: 'sessiontask.update',
118
- cli: ['sessiontask', 'update'],
119
- http: ['PATCH', '/api/sessiontasks/item'],
119
+ id: 'localtask.update',
120
+ cli: ['localtask', 'update'],
121
+ http: ['PATCH', '/api/localtasks/item'],
120
122
  summary: '编辑任务内容(按 --id,定位到 --cwd 主题)',
121
123
  flags: {
122
124
  id: { type: 'number', required: true, hint: '任务编号' },
@@ -127,9 +129,9 @@ export default {
127
129
  render: (d) => `已更新主题「${d.name}」#${d.task.id}: ${d.task.text}`,
128
130
  },
129
131
  {
130
- id: 'sessiontask.remove',
131
- cli: ['sessiontask', 'remove'],
132
- http: ['DELETE', '/api/sessiontasks/item'],
132
+ id: 'localtask.remove',
133
+ cli: ['localtask', 'remove'],
134
+ http: ['DELETE', '/api/localtasks/item'],
133
135
  summary: '删除一条任务(按 --id,定位到 --cwd 主题)',
134
136
  flags: { id: { type: 'number', required: true, hint: '任务编号' }, cwd: CWD },
135
137
  run: (ctx) => service.removeTask({ id: ctx.id, cwd: ctx.cwd }),
@@ -138,9 +140,9 @@ export default {
138
140
 
139
141
  // ─── 主题删除(单个 / 批量)──────────────────────────────
140
142
  {
141
- id: 'sessiontask.topic.remove',
142
- cli: ['sessiontask', 'topic', 'remove'],
143
- http: ['DELETE', '/api/sessiontasks/topic'],
143
+ id: 'localtask.topic.remove',
144
+ cli: ['localtask', 'topic', 'remove'],
145
+ http: ['DELETE', '/api/localtasks/topic'],
144
146
  summary: '删除整个主题(含全部任务;按 --cwd 或 --key,等待中需 --force)',
145
147
  flags: {
146
148
  cwd: { type: 'string', hint: '工作目录(与 --key 二选一)' },
@@ -152,9 +154,9 @@ export default {
152
154
  `已删除主题「${d.name}」(待领取/共 ${d.topic.pending}/${d.topic.total} 条任务一并删除)`,
153
155
  },
154
156
  {
155
- id: 'sessiontask.topic.removeMany',
156
- cli: ['sessiontask', 'topic', 'remove-many'],
157
- http: ['POST', '/api/sessiontasks/topics/remove'],
157
+ id: 'localtask.topic.removeMany',
158
+ cli: ['localtask', 'topic', 'remove-many'],
159
+ http: ['POST', '/api/localtasks/topics/remove'],
158
160
  summary: '批量删除主题(--keys/--cwds 逗号分隔,或 --all;等待中默认跳过,--force 连等待中删)',
159
161
  flags: {
160
162
  keys: { type: 'array', hint: '主题 key,逗号分隔' },
@@ -168,9 +170,9 @@ export default {
168
170
  },
169
171
 
170
172
  {
171
- id: 'sessiontask.config',
172
- cli: ['sessiontask', 'config'],
173
- http: ['PATCH', '/api/sessiontasks/setting'],
173
+ id: 'localtask.config',
174
+ cli: ['localtask', 'config'],
175
+ http: ['PATCH', '/api/localtasks/setting'],
174
176
  summary: '配置等待超时(秒;推荐 180=3 分钟,最大 600=10 分钟,超上限自动截断)',
175
177
  flags: { timeout: { type: 'number', required: true, hint: '超时秒数(1-600)' } },
176
178
  run: (ctx) => service.updateSettings({ timeoutSec: ctx.timeout }),
@@ -178,9 +180,9 @@ export default {
178
180
  },
179
181
 
180
182
  {
181
- id: 'sessiontask.prune',
182
- cli: ['sessiontask', 'prune'],
183
- http: ['POST', '/api/sessiontasks/prune'],
183
+ id: 'localtask.prune',
184
+ cli: ['localtask', 'prune'],
185
+ http: ['POST', '/api/localtasks/prune'],
184
186
  summary: '清理旧脏数据(旧模型无目录主题、空主题;等待中一律保留)',
185
187
  flags: {
186
188
  'all-finished': { type: 'boolean', hint: '连同已全部领取、无待领的主题一起清' },
@@ -195,9 +197,9 @@ export default {
195
197
  },
196
198
 
197
199
  {
198
- id: 'sessiontask.reorder',
199
- cli: ['sessiontask', 'reorder'],
200
- http: ['PATCH', '/api/sessiontasks/reorder'],
200
+ id: 'localtask.reorder',
201
+ cli: ['localtask', 'reorder'],
202
+ http: ['PATCH', '/api/localtasks/reorder'],
201
203
  summary: '调整待领取任务优先级(--id <编号> 移到 --before-id <编号> 前或 --after-id <编号> 后)',
202
204
  flags: {
203
205
  id: { type: 'number', required: true, hint: '要移动的任务编号' },
@@ -215,8 +217,8 @@ export default {
215
217
 
216
218
  // ─── 阻塞等待(纯 CLI,agent 本轮最后一步)────────────────
217
219
  {
218
- id: 'sessiontask.wait',
219
- cli: ['sessiontask', 'wait'],
220
+ id: 'localtask.wait',
221
+ cli: ['localtask', 'wait'],
220
222
  http: null,
221
223
  summary: '针对某主题(按 cwd 目录名识别)阻塞等待用户回填(默认 3 分钟,最大 10 分钟)',
222
224
  flags: {
@@ -12,7 +12,7 @@ export function renderPopupPage({ key, cwd, summary, timeoutMs }) {
12
12
  <head>
13
13
  <meta charset="utf-8" />
14
14
  <meta name="viewport" content="width=device-width, initial-scale=1" />
15
- <title>nx-kv · 实时任务</title>
15
+ <title>nx-kv · 本地任务</title>
16
16
  <style>
17
17
  :root {
18
18
  --ink: #14161a;
@@ -109,7 +109,7 @@ export function renderPopupPage({ key, cwd, summary, timeoutMs }) {
109
109
  <body>
110
110
  <div class="wrap">
111
111
  <div class="top">
112
- <div class="brand">nx-kv<span class="sub">实时任务输入</span></div>
112
+ <div class="brand">nx-kv<span class="sub">本地任务</span></div>
113
113
  <div class="meta"><span class="pill">回填后立即返回 agent</span></div>
114
114
  </div>
115
115
 
@@ -217,7 +217,7 @@ export function renderPopupPage({ key, cwd, summary, timeoutMs }) {
217
217
 
218
218
  function refresh() {
219
219
  if (delivered) return;
220
- fetch('/api/sessiontasks', { cache: 'no-store' })
220
+ fetch('/api/localtasks', { cache: 'no-store' })
221
221
  .then(function (r) { return r.json(); })
222
222
  .then(function (j) {
223
223
  if (!j.ok) return;
@@ -234,7 +234,7 @@ export function renderPopupPage({ key, cwd, summary, timeoutMs }) {
234
234
  var input = el('input');
235
235
  var text = input.value.trim();
236
236
  if (!text) { input.focus(); return; }
237
- fetch('/api/sessiontasks', {
237
+ fetch('/api/localtasks', {
238
238
  method: 'POST',
239
239
  headers: { 'content-type': 'application/json' },
240
240
  body: JSON.stringify({ text: text })
@@ -266,7 +266,7 @@ export function renderPopupPage({ key, cwd, summary, timeoutMs }) {
266
266
  el('list').addEventListener('click', function (e) {
267
267
  var b = e.target;
268
268
  if (b && b.dataset && b.dataset.id) {
269
- fetch('/api/sessiontasks/item', {
269
+ fetch('/api/localtasks/item', {
270
270
  method: 'DELETE',
271
271
  headers: { 'content-type': 'application/json' },
272
272
  body: JSON.stringify({ id: Number(b.dataset.id) })
@@ -1,8 +1,8 @@
1
- // 实时任务输入 service —— 按「工作目录(cwd)」划分的主题(topic)+ 主题任务队列 +
2
- // 阻塞等待 + 内嵌弹窗。
1
+ // 本地任务(localtask)service —— 按「工作目录(cwd)」划分的主题(topic)+ 主题任务队列 +
2
+ // 领取(含阻塞等待子功能)+ 内嵌弹窗。
3
3
  //
4
4
  // 解决的问题:agent 在一个对话里需要用户继续给任务时,不必结束本轮、等用户去
5
- // 原始输入框打字,而是把 `nx-kv sessiontask wait --cwd "<工作目录>" --summary "<当前现状>"`
5
+ // 原始输入框打字,而是把 `nx-kv localtask wait --cwd "<工作目录>" --summary "<当前现状>"`
6
6
  // 当作 **本轮最后一个动作**调用——命令阻塞、自动打开浏览器弹窗;用户在弹窗/面板回填,
7
7
  // 命令解除阻塞、把新任务作为结果返回给 agent,agent 接着干。
8
8
  //
@@ -16,7 +16,7 @@
16
16
  // http 服务器直出,未登录也能用。
17
17
  // 4. **排队语义。** 用户可提前回填:主题已有 pending 时 wait 立即取走、不开窗;
18
18
  // 没有 pending 才开窗阻塞。
19
- // 5. **超时是业务结果不是错误。** 默认阻塞 2 分钟;超时返回 {status:'timeout'}、
19
+ // 5. **超时是业务结果不是错误。** 默认阻塞 3 分钟(页面可调,上限 10 分钟);超时返回 {status:'timeout'}、
20
20
  // exit 0,agent 据此安静收尾结束本轮,绝不抛异常。
21
21
  //
22
22
  // 跨进程:弹窗与 wait 同进程时靠内存 waiter 即时唤醒;任务由别的进程写入(如常驻
@@ -37,10 +37,21 @@ export const MAX_TIMEOUT_SEC = 600; // 阻塞时长上限(10 分钟)
37
37
  export const MAX_SUMMARY = 2000; // 现状(状态)长度上限(原 200,已放宽)
38
38
  export const MAX_TEXT = 20_000; // 单条任务内容长度上限
39
39
 
40
- // ─── 存储位置:可环境变量覆盖(测试隔离) ───────────────────────────
40
+ // ─── 存储位置:可环境变量覆盖(测试隔离);旧名/旧文件自动兼容 ──────────
41
41
 
42
42
  export function storePath() {
43
- return process.env.NX_KV_SESSIONTASK_STORE || join(APP_DIR, 'sessiontasks.json');
43
+ return (
44
+ process.env.NX_KV_LOCALTASK_STORE ||
45
+ process.env.NX_KV_SESSIONTASK_STORE || // 改名前的环境变量,兼容既有脚本
46
+ join(APP_DIR, 'localtasks.json')
47
+ );
48
+ }
49
+
50
+ // 改名前的存储文件。显式覆盖(任一环境变量)时不迁移;新文件缺失时读它,
51
+ // 首次保存即落到新文件(旧文件保留不删,数据零丢失)。
52
+ function legacyStorePath() {
53
+ if (process.env.NX_KV_LOCALTASK_STORE || process.env.NX_KV_SESSIONTASK_STORE) return null;
54
+ return join(APP_DIR, 'sessiontasks.json');
44
55
  }
45
56
 
46
57
  // ─── 工作目录 → 主题 ───────────────────────────────────────────────
@@ -141,7 +152,15 @@ async function loadStore() {
141
152
  let topics = null;
142
153
  let settings = null;
143
154
  try {
144
- const raw = await fsp.readFile(storePath(), 'utf8');
155
+ let raw;
156
+ try {
157
+ raw = await fsp.readFile(storePath(), 'utf8');
158
+ } catch (err) {
159
+ // 新文件不存在 → 尝试改名前的旧文件(一次性迁移读取;再缺失才落到空 store)
160
+ const legacy = err && err.code === 'ENOENT' ? legacyStorePath() : null;
161
+ if (!legacy) throw err;
162
+ raw = await fsp.readFile(legacy, 'utf8');
163
+ }
145
164
  const data = JSON.parse(raw);
146
165
  if (data && typeof data === 'object') {
147
166
  if (data.topics && typeof data.topics === 'object') {
@@ -703,12 +722,12 @@ async function handlePopup(req, res, key, cwd, summary, timeoutMs) {
703
722
  return;
704
723
  }
705
724
 
706
- if (url.pathname === '/api/sessiontasks' && method === 'GET') {
725
+ if (url.pathname === '/api/localtasks' && method === 'GET') {
707
726
  sendJson(res, 200, { ok: true, data: await listTasks() });
708
727
  return;
709
728
  }
710
729
 
711
- if (url.pathname === '/api/sessiontasks' && method === 'POST') {
730
+ if (url.pathname === '/api/localtasks' && method === 'POST') {
712
731
  if (!originAllowed(req)) {
713
732
  sendJson(res, 403, { ok: false, error: '跨站请求被拒绝(弹窗仅接受本机来源)', code: 'BLOCKED' });
714
733
  return;
@@ -718,7 +737,7 @@ async function handlePopup(req, res, key, cwd, summary, timeoutMs) {
718
737
  return;
719
738
  }
720
739
 
721
- if (url.pathname === '/api/sessiontasks/item' && method === 'DELETE') {
740
+ if (url.pathname === '/api/localtasks/item' && method === 'DELETE') {
722
741
  if (!originAllowed(req)) {
723
742
  sendJson(res, 403, { ok: false, error: '跨站请求被拒绝(弹窗仅接受本机来源)', code: 'BLOCKED' });
724
743
  } else {
@@ -1,4 +1,4 @@
1
- // 实时任务页:主题(按工作目录划分)总览 + 选中某个主题回填 + 该主题的任务队列。
1
+ // 本地任务(localtask)页:主题(按工作目录划分)总览 + 选中某个主题回填 + 该主题的任务队列。
2
2
  // 本质是「agent 提交结果 → 阻塞 → 用户在 web 针对主题回填 → agent 接收」。
3
3
  import { useCallback, useEffect, useState } from 'react';
4
4
  import { api } from '../../web/frontend/api/client.js';
@@ -47,7 +47,7 @@ export default function SessionTaskView() {
47
47
 
48
48
  const load = useCallback(async () => {
49
49
  try {
50
- setOverview(await api('/api/sessiontasks'));
50
+ setOverview(await api('/api/localtasks'));
51
51
  } catch (e) {
52
52
  toast(e.message);
53
53
  }
@@ -75,7 +75,7 @@ export default function SessionTaskView() {
75
75
  setTimeoutSec(Number(sec));
76
76
  setCfgSaving(true);
77
77
  try {
78
- const s = await api('/api/sessiontasks/setting', {
78
+ const s = await api('/api/localtasks/setting', {
79
79
  method: 'PATCH',
80
80
  body: { timeout: Number(sec) },
81
81
  });
@@ -91,7 +91,7 @@ export default function SessionTaskView() {
91
91
  guard(async () => {
92
92
  setPruning(true);
93
93
  try {
94
- const r = await api('/api/sessiontasks/prune', { method: 'POST', body: {} });
94
+ const r = await api('/api/localtasks/prune', { method: 'POST', body: {} });
95
95
  toast(r.count ? `已清理 ${r.count} 个旧主题` : '没有需要清理的旧数据');
96
96
  await load();
97
97
  } finally {
@@ -144,7 +144,7 @@ export default function SessionTaskView() {
144
144
  }
145
145
  setSaving(true);
146
146
  try {
147
- const r = await api('/api/sessiontasks', {
147
+ const r = await api('/api/localtasks', {
148
148
  method: 'POST',
149
149
  body: { cwd: active.cwd, text: body },
150
150
  });
@@ -158,7 +158,7 @@ export default function SessionTaskView() {
158
158
 
159
159
  const remove = (id) =>
160
160
  guard(async () => {
161
- await api('/api/sessiontasks/item', {
161
+ await api('/api/localtasks/item', {
162
162
  method: 'DELETE',
163
163
  body: { cwd: active.cwd, id },
164
164
  });
@@ -175,7 +175,7 @@ export default function SessionTaskView() {
175
175
  if (!window.confirm(msg)) return;
176
176
  setDeleting(true);
177
177
  try {
178
- await api('/api/sessiontasks/topic', {
178
+ await api('/api/localtasks/topic', {
179
179
  method: 'DELETE',
180
180
  body: t.waiting ? { key: t.key, force: true } : { key: t.key },
181
181
  });
@@ -202,7 +202,7 @@ export default function SessionTaskView() {
202
202
  if (!window.confirm(msg)) return;
203
203
  setDeleting(true);
204
204
  try {
205
- const r = await api('/api/sessiontasks/topics/remove', {
205
+ const r = await api('/api/localtasks/topics/remove', {
206
206
  method: 'POST',
207
207
  body: { keys: [...selected], force: waitingCount > 0 },
208
208
  });
@@ -233,7 +233,7 @@ export default function SessionTaskView() {
233
233
  }
234
234
  setEditSaving(true);
235
235
  try {
236
- await api('/api/sessiontasks/item', {
236
+ await api('/api/localtasks/item', {
237
237
  method: 'PATCH',
238
238
  body: { cwd: active.cwd, id: editingId, text: body },
239
239
  });
@@ -255,7 +255,7 @@ export default function SessionTaskView() {
255
255
  guard(async () => {
256
256
  setReordering(true);
257
257
  try {
258
- await api('/api/sessiontasks/reorder', {
258
+ await api('/api/localtasks/reorder', {
259
259
  method: 'PATCH',
260
260
  body: { cwd: active.cwd, id, beforeId, afterId },
261
261
  });
@@ -301,7 +301,7 @@ export default function SessionTaskView() {
301
301
  <div className="card" style={{ padding: '10px 14px' }}>
302
302
  <div className="muted" style={{ fontSize: 12 }}>
303
303
  agent 用法:完成当前任务后,把
304
- <code> nx-kv sessiontask wait --cwd "&lt;工作目录&gt;" --summary "&lt;当前现状&gt;" --json </code>
304
+ <code> nx-kv localtask wait --cwd "&lt;工作目录&gt;" --summary "&lt;当前现状&gt;" --json </code>
305
305
  作为本轮<strong>最后一个动作</strong>调用——工作目录即<strong>主题</strong>、主题拥有任务队列,
306
306
  同目录共用队列、不同目录互不串,任务可在队列里堆积;命令阻塞并自动打开浏览器弹窗,
307
307
  用户回填后解除阻塞、返回新任务;超时未回填则返回 <code>status:"timeout"</code>,agent 收尾结束本轮。
@@ -432,7 +432,7 @@ export default function SessionTaskView() {
432
432
  })
433
433
  ) : (
434
434
  <div className="card muted" style={{ padding: '20px 16px', width: '100%', textAlign: 'center' }}>
435
- (暂无主题;agent 调用 sessiontask wait 后会出现在这里)
435
+ (暂无主题;agent 调用 localtask wait 后会出现在这里)
436
436
  </div>
437
437
  )}
438
438
  </div>
@@ -610,7 +610,7 @@ export default function SessionTaskView() {
610
610
  </>
611
611
  )}
612
612
 
613
- <CliHints module="sessiontask" />
613
+ <CliHints module="localtask" />
614
614
  </>
615
615
  );
616
616
  }