@william2000/dsh-nova-ui-task-board 0.2.0 → 0.2.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.
package/lib/index.js CHANGED
@@ -465,6 +465,19 @@ var HostExecutionRunner = class {
465
465
  };
466
466
  }
467
467
  /**
468
+ * 停止底层会话(§13.3 用户取消执行):尽力而为——RPC 失败/被拒/会话不存在
469
+ * 都只记日志,不抛给控制面(账本侧 cancelled 结算已权威,本调用只是避免
470
+ * 孤儿 agent 继续空转/烧额度)。
471
+ */
472
+ async cancelSession(sessionId) {
473
+ try {
474
+ const response = await this.api.sessions.cancel(request({ sessionId }));
475
+ if (!response.result.ok) console.warn(`[dsh-task-board] session cancel failed (best-effort): ${response.result.error.code}: ${response.result.error.message}`);
476
+ } catch (error) {
477
+ console.warn(`[dsh-task-board] session cancel failed (best-effort): ${error instanceof Error ? error.message : String(error)}`);
478
+ }
479
+ }
480
+ /**
468
481
  * 读取会话 transcript 全文(T012/B8:拆分会话结果解析用)。翻页读取历史事件,
469
482
  * 拼接全部 assistant 消息文本(顺序拼接、换行分隔;总长封顶
470
483
  * TRANSCRIPT_TEXT_LIMIT)。会话不存在/历史不可读 → undefined(调用方按失败
@@ -1802,6 +1815,28 @@ function parseActionEnvelope(value) {
1802
1815
  taskId
1803
1816
  }
1804
1817
  };
1818
+ case "cancel-execution":
1819
+ if (!exactKeys(action, [
1820
+ "kind",
1821
+ "taskId",
1822
+ "executionId"
1823
+ ])) return void 0;
1824
+ if (taskId === void 0) return void 0;
1825
+ if (action.executionId !== void 0 && (typeof action.executionId !== "string" || action.executionId.trim() === "")) return;
1826
+ return action.executionId === void 0 ? {
1827
+ requestId: envelope.requestId,
1828
+ action: {
1829
+ kind: "cancel-execution",
1830
+ taskId
1831
+ }
1832
+ } : {
1833
+ requestId: envelope.requestId,
1834
+ action: {
1835
+ kind: "cancel-execution",
1836
+ taskId,
1837
+ executionId: action.executionId
1838
+ }
1839
+ };
1805
1840
  case "add-comment":
1806
1841
  if (!exactKeys(action, [
1807
1842
  "kind",
@@ -2945,6 +2980,27 @@ function applySettleExecution(doc, taskId, executionId, result, now, error) {
2945
2980
  doc.tasks = doc.tasks.map((candidate) => candidate.id === taskId ? next : candidate);
2946
2981
  return true;
2947
2982
  }
2983
+ /**
2984
+ * cancel-execution(§11.2,用户手动停止执行):把一条未结算执行结算为
2985
+ * cancelled(endedAt=now、result=cancelled、error 注明用户取消),任务按
2986
+ * settleStatusOf 回落(cancelled → todo)——running 独占随即释放,任务恢复
2987
+ * 可移动/可删除/可归档。这是 running 任务「停不下来」的唯一解锁路径:即使
2988
+ * 底层会话已停滞(无 turn/end、会话列表仍在),账本侧结算仍然权威生效。
2989
+ *
2990
+ * 守卫:任务存在;无未结算执行 → 抛错('task has no running execution');
2991
+ * `executionId` 缺省取最后一条未结算执行,提供时须是该任务的未结算执行
2992
+ * (已结算执行不可作为取消目标,否则 'execution not found')。
2993
+ * 底层会话的停止由 Host 尽力而为(runner.cancelSession),本层不触碰会话。
2994
+ */
2995
+ function applyCancelExecution(doc, taskId, executionId, now) {
2996
+ const task = doc.tasks.find((candidate) => candidate.id === taskId);
2997
+ if (task === void 0) throw new TaskBoardTransitionError("task not found");
2998
+ const open = task.executions.filter((execution) => execution.endedAt === void 0);
2999
+ if (open.length === 0) throw new TaskBoardTransitionError("task has no running execution");
3000
+ const target = executionId === void 0 ? open[open.length - 1] : open.find((execution) => execution.id === executionId);
3001
+ if (target === void 0) throw new TaskBoardTransitionError("execution not found");
3002
+ return applySettleExecution(doc, taskId, target.id, "cancelled", now, "cancelled by user");
3003
+ }
2948
3004
  /** 从 cron 分支构造一条新的 ScheduleRule(丢弃 lastTriggeredAt 之外的字段)。 */
2949
3005
  function cronRule(schedule, cron, enabled, nextRunAt) {
2950
3006
  const rule = {
@@ -4153,6 +4209,11 @@ var TaskBoardHostService = class {
4153
4209
  */
4154
4210
  apply(requestId, action) {
4155
4211
  if (!this.active) throw new Error("task board is disabled");
4212
+ let cancelTargetExecutionId;
4213
+ if (action.kind === "cancel-execution") {
4214
+ const open = this.ledger.peekTasks().find((candidate) => candidate.id === action.taskId)?.executions.filter((execution) => execution.endedAt === void 0) ?? [];
4215
+ cancelTargetExecutionId = action.executionId ?? open.at(-1)?.id;
4216
+ }
4156
4217
  const before = this.ledger.summary().revision;
4157
4218
  const state = this.ledger.applyRequest(requestId, action, (doc) => this.applyAction(doc, action));
4158
4219
  if (state.revision > before) {
@@ -4160,6 +4221,9 @@ var TaskBoardHostService = class {
4160
4221
  const task = state.tasks.find((candidate) => candidate.id === action.taskId);
4161
4222
  const execution = task?.executions.at(-1);
4162
4223
  if (task !== void 0 && execution !== void 0) this.scheduleLaunch(task, execution);
4224
+ } else if (action.kind === "cancel-execution" && cancelTargetExecutionId !== void 0) {
4225
+ const execution = state.tasks.find((candidate) => candidate.id === action.taskId)?.executions.find((candidate) => candidate.id === cancelTargetExecutionId);
4226
+ if (execution?.sessionId !== void 0) this.scheduleSessionCancel(execution.sessionId);
4163
4227
  } else if (action.kind === "run-automation") this.scheduleAutomationRun(action.ruleId);
4164
4228
  } else if (action.kind === "start-split") {
4165
4229
  const job = this.splitJobs.get(action.id);
@@ -4247,6 +4311,7 @@ var TaskBoardHostService = class {
4247
4311
  case "set-schedule": return applySetSchedule(doc, action.taskId, action.patch, this.now());
4248
4312
  case "set-one-shot": return applySetOneShot(doc, action.taskId, action.patch, this.now());
4249
4313
  case "delete": return applyDeleteTask(doc, action.taskId);
4314
+ case "cancel-execution": return applyCancelExecution(doc, action.taskId, action.executionId, this.now());
4250
4315
  case "move": return applyMoveTask(doc, action.taskId, action.status, this.now(), action.order);
4251
4316
  case "reorder": return applyReorderTask(doc, action.taskId, action.status, action.order, {
4252
4317
  ...action.project !== void 0 ? { project: action.project } : {},
@@ -4275,6 +4340,18 @@ var TaskBoardHostService = class {
4275
4340
  });
4276
4341
  }
4277
4342
  /**
4343
+ * 异步停止底层会话(用户取消执行,§13.3):尽力而为——runner 未接线/
4344
+ * 不支持 cancelSession 时 no-op;RPC 失败仅记日志(账本侧 cancelled 结算
4345
+ * 已权威,不因会话停止失败而回滚)。
4346
+ */
4347
+ scheduleSessionCancel(sessionId) {
4348
+ const runner = this.runner;
4349
+ if (runner?.cancelSession === void 0) return;
4350
+ runner.cancelSession(sessionId).catch((error) => {
4351
+ console.warn(`[dsh-task-board] session cancel failed (best-effort): ${error instanceof Error ? error.message : String(error)}`);
4352
+ });
4353
+ }
4354
+ /**
4278
4355
  * launch 编排(§13.2 尾部):runner 创建会话成功 → 回填 sessionId(之后由
4279
4356
  * 轮询结算);任何失败 → 结算 failed 并保留错误文本(会话创建后的失败携带
4280
4357
  * sessionId,先回填再结算,详情页保留跳转线索)。
@@ -89,7 +89,14 @@ export interface BoardSnapshot {
89
89
  executionOptions: ExecutionOptionsSnapshot;
90
90
  /** 已提交、等待 Host 确认的任务 id(只做展示,不先写状态)。 */
91
91
  pendingTaskIds: readonly string[];
92
+ /** 最近一次 Host/传输错误(业务拒绝或传输失败统一进此字段展示)。 */
92
93
  transportError?: string;
94
+ /**
95
+ * 该错误是否可通过「重试连接 Host」修复:true = 传输失败(网络/超时/解析,
96
+ * 重试有意义);false = Host 业务拒绝(HTTP 4xx,重试无意义,错误条据此
97
+ * 不渲染重试按钮)。
98
+ */
99
+ transportErrorRetryable?: boolean;
93
100
  /** Host 元信息:revision + scheduler(时区)+ power。 */
94
101
  host?: {
95
102
  revision: number;
@@ -135,7 +142,7 @@ export declare class BoardController {
135
142
  private readonly uuid;
136
143
  private readonly pendingTaskIds;
137
144
  private readonly taskQueues;
138
- private transportError;
145
+ private transportFailure;
139
146
  private hostState;
140
147
  private remoteSubscribed;
141
148
  private remoteInitialization;
@@ -214,6 +221,13 @@ export declare class BoardController {
214
221
  tags?: string[];
215
222
  }): Promise<boolean>;
216
223
  deleteTask(id: string): void;
224
+ /**
225
+ * 停止运行中的执行(§11.2 cancel-execution,用户手动取消):Host 把目标执行
226
+ * 结算为 cancelled → 任务回落 todo,running 独占随即释放——随后可移动/删除/
227
+ * 归档。`executionId` 缺省 = 最后一条未结算执行;底层会话由 Host 尽力停止。
228
+ * 返回 Host 是否确认。
229
+ */
230
+ cancelExecution(id: string, executionId?: string): Promise<boolean>;
217
231
  /** 归档已结算任务(done/failed)。Host 守卫拒绝非法归档并置错误条。 */
218
232
  archiveTask(id: string): void;
219
233
  /** 恢复归档任务回原列。确认后关闭详情(任务已离开归档视图)。 */
@@ -21,6 +21,15 @@ import type { TaskRecord } from '../core/model.ts';
21
21
  import { type TaskBoardAction, type TaskBoardEventPayload, type TaskBoardSnapshot } from '../protocol.ts';
22
22
  /** 单次 Host 请求超时(§17 性能边界;超时按传输错误暴露,可重试)。 */
23
23
  export declare const REQUEST_TIMEOUT_MS = 15000;
24
+ /**
25
+ * Host 业务拒绝(HTTP 4xx,§11.1):与传输层错误区分——业务拒绝由操作修正,
26
+ * 重试无意义;错误条据此**不显示「重试连接 Host」**,只对传输失败(网络/
27
+ * 超时/解析)保留重试入口(board-controller 按 instanceof 判定 retryable)。
28
+ */
29
+ export declare class TaskBoardRequestError extends Error {
30
+ readonly status: number;
31
+ constructor(message: string, status: number);
32
+ }
24
33
  /** 浏览器存储的读写面(localStorage;测试注入 fake)。 */
25
34
  export interface MarkerStorage {
26
35
  getItem(key: string): string | null;
@@ -50,6 +50,7 @@ export declare const zh: {
50
50
  'detail.noExecution': string;
51
51
  'detail.run': string;
52
52
  'detail.rerun': string;
53
+ 'detail.stop': string;
53
54
  'detail.delete': string;
54
55
  'detail.archive': string;
55
56
  'detail.restore': string;
@@ -65,6 +66,9 @@ export declare const zh: {
65
66
  'delete.confirm': string;
66
67
  'delete.ok': string;
67
68
  'delete.cancel': string;
69
+ 'stop.title': string;
70
+ 'stop.confirm': string;
71
+ 'stop.ok': string;
68
72
  'status.move.backlog': string;
69
73
  'status.move.todo': string;
70
74
  'time.justNow': string;
@@ -64,6 +64,11 @@
64
64
  * proposed 守卫)、不可作为拖拽来源(T009 已含);编辑(update)允许
65
65
  * (§10.2「只读(除编辑/确认/拒绝)」)。
66
66
  *
67
+ * 用户取消执行(running 解锁):`applyCancelExecution`(§11.2 cancel-execution,
68
+ * 用户手动停止)——把目标未结算执行结算为 cancelled(endedAt/result/error 落账),
69
+ * 任务按 settleStatusOf 回落 todo,running 独占随即释放(可移动/删除/归档);
70
+ * 底层会话的停止由 Host 尽力而为(runner.cancelSession),本层只做账本侧权威结算。
71
+ *
67
72
  * T012 需求拆分(§11.2/§12.9/§14.5,P3.2):
68
73
  * - `applyProposeBatch`:propose-batch(批量创建候选,需求拆分产物)——
69
74
  * items 逐条沿用 propose 的清洗(标题/描述/prompt 控制字符 + 长度封顶,
@@ -285,6 +290,19 @@ export declare function settleStatusOf(result: ExecutionResult, current: TaskSta
285
290
  * 守卫:执行存在;已结算执行为 no-op(返回 false,不 bump revision)。
286
291
  */
287
292
  export declare function applySettleExecution(doc: LedgerDocument, taskId: string, executionId: string, result: ExecutionResult, now: number, error?: string): boolean;
293
+ /**
294
+ * cancel-execution(§11.2,用户手动停止执行):把一条未结算执行结算为
295
+ * cancelled(endedAt=now、result=cancelled、error 注明用户取消),任务按
296
+ * settleStatusOf 回落(cancelled → todo)——running 独占随即释放,任务恢复
297
+ * 可移动/可删除/可归档。这是 running 任务「停不下来」的唯一解锁路径:即使
298
+ * 底层会话已停滞(无 turn/end、会话列表仍在),账本侧结算仍然权威生效。
299
+ *
300
+ * 守卫:任务存在;无未结算执行 → 抛错('task has no running execution');
301
+ * `executionId` 缺省取最后一条未结算执行,提供时须是该任务的未结算执行
302
+ * (已结算执行不可作为取消目标,否则 'execution not found')。
303
+ * 底层会话的停止由 Host 尽力而为(runner.cancelSession),本层不触碰会话。
304
+ */
305
+ export declare function applyCancelExecution(doc: LedgerDocument, taskId: string, executionId: string | undefined, now: number): boolean;
288
306
  /** set-schedule 补丁(§11.2):只允许 enabled/cron 两个浏览器可写字段。 */
289
307
  export interface SetSchedulePatch {
290
308
  enabled?: boolean;
@@ -120,6 +120,11 @@ export interface ExecutionApi {
120
120
  events: HistoryEntry[];
121
121
  hasMore: boolean;
122
122
  }>>;
123
+ cancel(request: RpcRequest<{
124
+ sessionId: SessionId;
125
+ }>): Promise<RpcResponse<{
126
+ accepted: true;
127
+ }>>;
123
128
  };
124
129
  }
125
130
  /** 会话创建后的启动失败:仍标识会话(服务层先回填 sessionId 再结算 failed)。 */
@@ -167,6 +172,12 @@ export declare class HostExecutionRunner {
167
172
  * 实现);缺省时自行拉取。
168
173
  */
169
174
  inspect(sessionId: string, startedAt: number, sessions?: readonly ExecutionSessionRow[]): Promise<ExecutionInspection>;
175
+ /**
176
+ * 停止底层会话(§13.3 用户取消执行):尽力而为——RPC 失败/被拒/会话不存在
177
+ * 都只记日志,不抛给控制面(账本侧 cancelled 结算已权威,本调用只是避免
178
+ * 孤儿 agent 继续空转/烧额度)。
179
+ */
180
+ cancelSession(sessionId: string): Promise<void>;
170
181
  /**
171
182
  * 读取会话 transcript 全文(T012/B8:拆分会话结果解析用)。翻页读取历史事件,
172
183
  * 拼接全部 assistant 消息文本(顺序拼接、换行分隔;总长封顶
@@ -94,6 +94,12 @@ export interface TaskBoardExecutionRunner {
94
94
  * 历史不可读 → undefined。拆分会话是独立专用会话,全文即拆分结果。
95
95
  */
96
96
  readTranscriptText(sessionId: string): Promise<string | undefined>;
97
+ /**
98
+ * 停止底层会话(用户取消执行,§13.3):尽力而为——缺省/失败不影响账本侧
99
+ * cancelled 结算(cancel-execution 的 ledger 落账不依赖它)。可缺省(测试
100
+ * fake 无需实现)。
101
+ */
102
+ cancelSession?(sessionId: string): Promise<void>;
97
103
  }
98
104
  /**
99
105
  * 需求拆分作业(T012/B8,§12.9/§14.5):一次 `start-split` 对应一个独立拆分
@@ -176,6 +182,12 @@ export declare class TaskBoardHostService {
176
182
  private applyAction;
177
183
  /** 异步调度 launch(fire-and-forget;结算失败仅记录日志,不中断控制面)。 */
178
184
  private scheduleLaunch;
185
+ /**
186
+ * 异步停止底层会话(用户取消执行,§13.3):尽力而为——runner 未接线/
187
+ * 不支持 cancelSession 时 no-op;RPC 失败仅记日志(账本侧 cancelled 结算
188
+ * 已权威,不因会话停止失败而回滚)。
189
+ */
190
+ private scheduleSessionCancel;
179
191
  /**
180
192
  * launch 编排(§13.2 尾部):runner 创建会话成功 → 回填 sessionId(之后由
181
193
  * 轮询结算);任何失败 → 结算 failed 并保留错误文本(会话创建后的失败携带
@@ -133,6 +133,10 @@ export type TaskBoardAction = {
133
133
  } | {
134
134
  kind: 'rerun';
135
135
  taskId: string;
136
+ } | {
137
+ kind: 'cancel-execution';
138
+ taskId: string;
139
+ executionId?: string;
136
140
  } | {
137
141
  kind: 'propose';
138
142
  id: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@william2000/dsh-nova-ui-task-board",
3
3
  "description": "Host-authoritative task board for the DSH Web GUI with real session execution, Host cron/one-shot scheduling, sorting/grouping/drag-drop, tag & project filtering; mounted without DSH source changes. Nova 系列核心功能包(任务看板插件本体:Host 服务 + 浏览器 UI)。",
4
- "version": "0.2.0",
4
+ "version": "0.2.1",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=20"