@william2000/dsh-nova-ui-task-board 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.
- package/lib/client.js +1944 -1723
- package/lib/client.js.map +1 -1
- package/lib/index.js +77 -0
- package/lib/types/client/board-controller.d.ts +15 -1
- package/lib/types/client/host-api.d.ts +9 -0
- package/lib/types/client/locales.d.ts +8 -0
- package/lib/types/core/transitions.d.ts +18 -0
- package/lib/types/host-runner.d.ts +11 -0
- package/lib/types/host-service.d.ts +12 -0
- package/lib/types/protocol.d.ts +4 -0
- package/package.json +1 -1
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
|
|
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;
|
|
@@ -44,12 +44,17 @@ export declare const zh: {
|
|
|
44
44
|
'new.required': string;
|
|
45
45
|
'detail.title': string;
|
|
46
46
|
'detail.close': string;
|
|
47
|
+
'detail.edit': string;
|
|
48
|
+
'detail.editSave': string;
|
|
49
|
+
'detail.editCancel': string;
|
|
50
|
+
'detail.editRunningHint': string;
|
|
47
51
|
'detail.prompt': string;
|
|
48
52
|
'detail.description': string;
|
|
49
53
|
'detail.execution': string;
|
|
50
54
|
'detail.noExecution': string;
|
|
51
55
|
'detail.run': string;
|
|
52
56
|
'detail.rerun': string;
|
|
57
|
+
'detail.stop': string;
|
|
53
58
|
'detail.delete': string;
|
|
54
59
|
'detail.archive': string;
|
|
55
60
|
'detail.restore': string;
|
|
@@ -65,6 +70,9 @@ export declare const zh: {
|
|
|
65
70
|
'delete.confirm': string;
|
|
66
71
|
'delete.ok': string;
|
|
67
72
|
'delete.cancel': string;
|
|
73
|
+
'stop.title': string;
|
|
74
|
+
'stop.confirm': string;
|
|
75
|
+
'stop.ok': string;
|
|
68
76
|
'status.move.backlog': string;
|
|
69
77
|
'status.move.todo': string;
|
|
70
78
|
'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 并保留错误文本(会话创建后的失败携带
|
package/lib/types/protocol.d.ts
CHANGED
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.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20"
|