dsh-tree-task-flow 0.1.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/LICENSE +21 -0
- package/README.md +250 -0
- package/client.js +921 -0
- package/cordis.patch.yml +20 -0
- package/enable.example.yml +4 -0
- package/lib/auto-continue.js +132 -0
- package/lib/commands.js +189 -0
- package/lib/http.js +243 -0
- package/lib/index.js +110 -0
- package/lib/plan.js +430 -0
- package/lib/prompt.js +56 -0
- package/lib/region.js +345 -0
- package/lib/store.js +126 -0
- package/lib/tools.js +308 -0
- package/package.json +68 -0
package/client.js
ADDED
|
@@ -0,0 +1,921 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-tree-task-flow 的客户端:输入框上方的任务树条目 + 设置页里的插件开关说明。
|
|
3
|
+
*
|
|
4
|
+
* 这是**手写**的模块包装格式(和 dsh-purge 的 client.js 一样),不经过打包,
|
|
5
|
+
* 所以用 `h()` 而不是 JSX。
|
|
6
|
+
*
|
|
7
|
+
* 挂载点是 `conversation.input.dock`——输入框上方的全宽条目区,内置的任务清单
|
|
8
|
+
* (`todo`)与目标(`goal`)就在那里。这是一个 `list` 槽:**用自己的 id 注册就
|
|
9
|
+
* 会并列加在它们旁边**,不会替换任何一个;用内置的 id 才会顶掉它。
|
|
10
|
+
*
|
|
11
|
+
* 没有计划的会话**一个像素都不占**:读不到计划就返回 null,免得平白挤掉内置条目。
|
|
12
|
+
*
|
|
13
|
+
* 数据全部来自 host 端的 `/dsh-tree-task-flow/*` 接口;条目本身不持有任何状态,
|
|
14
|
+
* 动作(完成 / 丢弃 / 停止 / 恢复 / 重置)都是 POST 回 host,由 host 改真正的文件。
|
|
15
|
+
*
|
|
16
|
+
* 面板只展示两样东西:**每个节点提交的结果**,以及**现在是不是停在检查点**。
|
|
17
|
+
*
|
|
18
|
+
* 「完成」必须带结果,所以它不是一步按钮:先在节点下展开一个输入框,
|
|
19
|
+
* 写清产出了什么,再提交。结果一旦提交,这个节点整段执行过程就会被它顶替,
|
|
20
|
+
* 所以输入框的提示语要把这一点讲明白。
|
|
21
|
+
*/
|
|
22
|
+
window.__ModuleLoader__.load({
|
|
23
|
+
id: "dsh-tree-task-flow",
|
|
24
|
+
factory: (require) => {
|
|
25
|
+
var module = { exports: {} };
|
|
26
|
+
var exports = module.exports;
|
|
27
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
28
|
+
|
|
29
|
+
const react = require("react");
|
|
30
|
+
const h = react.createElement;
|
|
31
|
+
const { useState, useEffect, useCallback } = react;
|
|
32
|
+
|
|
33
|
+
const name = "dsh-tree-task-flow";
|
|
34
|
+
const inject = ["slots", "locale"];
|
|
35
|
+
const NS = "dsh-tree-task-flow";
|
|
36
|
+
/** 条目 id。用自己的 id,所以只会加在内置条目旁边。 */
|
|
37
|
+
const ENTRY_ID = "dsh-tree-task-flow";
|
|
38
|
+
|
|
39
|
+
let translate = (key) => key;
|
|
40
|
+
const useT = () => translate;
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------- 基础
|
|
43
|
+
|
|
44
|
+
function urlOf(path) {
|
|
45
|
+
try {
|
|
46
|
+
return new URL(path, window.location.origin).toString();
|
|
47
|
+
} catch {
|
|
48
|
+
return path;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function api(path, init) {
|
|
53
|
+
const response = await fetch(
|
|
54
|
+
urlOf(path),
|
|
55
|
+
Object.assign({ cache: "no-store", credentials: "same-origin" }, init || {}),
|
|
56
|
+
);
|
|
57
|
+
const text = await response.text();
|
|
58
|
+
let data = null;
|
|
59
|
+
try {
|
|
60
|
+
data = text ? JSON.parse(text) : null;
|
|
61
|
+
} catch {
|
|
62
|
+
throw new Error(
|
|
63
|
+
response.status + " 返回了非 JSON:" + String(text).slice(0, 120),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const error = new Error((data && data.error) || response.status + " " + response.statusText);
|
|
68
|
+
// 调用方要能区分「这个会话没有计划」(正常)与真出错。
|
|
69
|
+
error.status = response.status;
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
return data;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 写操作。host 用 HTTP 200 携带 `{ ok: false, error }` 表示业务拒绝
|
|
77
|
+
* (比如完成时没给结果),所以这里要把 `ok` 也当错误判一次。
|
|
78
|
+
*/
|
|
79
|
+
async function post(body) {
|
|
80
|
+
const data = await api("/dsh-tree-task-flow/action", {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: { "content-type": "application/json" },
|
|
83
|
+
body: JSON.stringify(body),
|
|
84
|
+
});
|
|
85
|
+
if (data && data.ok === false) throw new Error(data.error || "host 拒绝了这次操作");
|
|
86
|
+
return data;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 取首行并截断。树上只报"产出了什么",全文展开看。 */
|
|
90
|
+
function firstLine(text, limit) {
|
|
91
|
+
const line = String(text ?? "").split("\n")[0].trim();
|
|
92
|
+
return line.length > limit ? line.slice(0, limit) + "…" : line;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function formatTime(at) {
|
|
96
|
+
const n = Number(at);
|
|
97
|
+
if (!n) return "";
|
|
98
|
+
try {
|
|
99
|
+
return new Date(n).toLocaleString();
|
|
100
|
+
} catch {
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function mark(node) {
|
|
106
|
+
if (node.status === "done") return "[x]";
|
|
107
|
+
if (node.status === "dropped") return "[-]";
|
|
108
|
+
return "[ ]";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 三层节点总数与已完成数,给折叠时的摘要用。 */
|
|
112
|
+
function countNodes(goal) {
|
|
113
|
+
let total = 1;
|
|
114
|
+
let done = goal.status === "done" ? 1 : 0;
|
|
115
|
+
for (const task of goal.tasks || []) {
|
|
116
|
+
total += 1;
|
|
117
|
+
if (task.status === "done") done += 1;
|
|
118
|
+
for (const step of task.steps || []) {
|
|
119
|
+
total += 1;
|
|
120
|
+
if (step.status === "done") done += 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { total, done };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 当前活动节点的标题:子任务优先,其次任务,最后目标。 */
|
|
127
|
+
function currentLabel(data) {
|
|
128
|
+
const active = data.active;
|
|
129
|
+
if (!active) return "";
|
|
130
|
+
const goal = data.plan.goal;
|
|
131
|
+
const task = (goal.tasks || []).find((each) => each.id === active.taskId);
|
|
132
|
+
if (!task) return goal.title;
|
|
133
|
+
const step = (task.steps || []).find((each) => each.id === active.stepId);
|
|
134
|
+
return step ? step.title : task.title;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------- 文案
|
|
138
|
+
|
|
139
|
+
const zh = {
|
|
140
|
+
nav: "树形任务流",
|
|
141
|
+
"empty.title": "这个会话还没有计划",
|
|
142
|
+
"empty.hint": "让模型调用 tree_task_create 建立目标与任务,这里就会显示整棵树。",
|
|
143
|
+
"state.running": "进行中",
|
|
144
|
+
"state.stopped": "自动续行已停止",
|
|
145
|
+
"state.done": "已全部完成",
|
|
146
|
+
"state.checkpoint": "检查点",
|
|
147
|
+
"checkpoint.note": "「{title}」的子节点已全部结束。要么继续给它拆子节点,要么提交结果并完成它。",
|
|
148
|
+
"btn.stop": "停止自动续行",
|
|
149
|
+
"btn.resume": "恢复自动续行",
|
|
150
|
+
"btn.reset": "清空计划",
|
|
151
|
+
"btn.refresh": "刷新",
|
|
152
|
+
"btn.done": "完成…",
|
|
153
|
+
"btn.drop": "丢弃",
|
|
154
|
+
"btn.submit": "提交结果",
|
|
155
|
+
"btn.cancel": "取消",
|
|
156
|
+
"label.result": "结果",
|
|
157
|
+
"label.noResult": "(还没提交结果)",
|
|
158
|
+
"label.current": "当前",
|
|
159
|
+
"label.checkpointHere": "检查点",
|
|
160
|
+
"placeholder.result":
|
|
161
|
+
"写清这个节点产出了什么。它整段执行过程会被这条结果顶替,所以要能独立看懂。",
|
|
162
|
+
"confirm.reset": "确定要清空这个会话的计划树吗?",
|
|
163
|
+
"confirm.drop": "确定要丢弃「{title}」吗?",
|
|
164
|
+
"settings.title": "树形任务流",
|
|
165
|
+
"settings.enabled": "已启用",
|
|
166
|
+
"settings.disabled": "未启用",
|
|
167
|
+
"settings.impact": "启用后它会做什么",
|
|
168
|
+
"settings.impact.1": "在每个会话的系统提示词里加一段固定的工具组说明(会话内逐字节不变)。",
|
|
169
|
+
"settings.impact.2": "注册 6 个 tree_task_* 工具。",
|
|
170
|
+
"settings.impact.3":
|
|
171
|
+
"一个节点完成时,把它那一段执行过程从模型可见的上下文里折叠掉,换成一条携带提交结果的汇总消息。",
|
|
172
|
+
"settings.impact.4":
|
|
173
|
+
"一个节点的子节点全部结束时转入检查点:那一刻只放行 tree_task_plan 与 tree_task_done。",
|
|
174
|
+
"settings.impact.5":
|
|
175
|
+
"可选:模型停下来时自动续行(默认关闭;你按下停止后不会再被拉起来)。",
|
|
176
|
+
"settings.howto": "怎么开 / 怎么关",
|
|
177
|
+
"settings.howto.body":
|
|
178
|
+
"改 profile 的 cordis.patch.yml 里 task-tree 那一行的 config.enabled,然后重启 dsh web。装上不等于启用——默认是关的。",
|
|
179
|
+
"settings.stateDir": "状态目录",
|
|
180
|
+
"settings.sessions": "有计划的会话",
|
|
181
|
+
"settings.noSessions": "(还没有)",
|
|
182
|
+
"settings.checkpoint": "检查点",
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const en = {
|
|
186
|
+
nav: "Tree Task Flow",
|
|
187
|
+
"empty.title": "No plan in this session yet",
|
|
188
|
+
"empty.hint": "Ask the model to call tree_task_create to build a goal and tasks.",
|
|
189
|
+
"state.running": "In progress",
|
|
190
|
+
"state.stopped": "Auto-continue stopped",
|
|
191
|
+
"state.done": "All done",
|
|
192
|
+
"state.checkpoint": "Checkpoint",
|
|
193
|
+
"checkpoint.note":
|
|
194
|
+
"Every child of “{title}” has finished. Either break out more children, or submit its result and complete it.",
|
|
195
|
+
"btn.stop": "Stop auto-continue",
|
|
196
|
+
"btn.resume": "Resume auto-continue",
|
|
197
|
+
"btn.reset": "Clear plan",
|
|
198
|
+
"btn.refresh": "Refresh",
|
|
199
|
+
"btn.done": "Complete…",
|
|
200
|
+
"btn.drop": "Drop",
|
|
201
|
+
"btn.submit": "Submit result",
|
|
202
|
+
"btn.cancel": "Cancel",
|
|
203
|
+
"label.result": "Result",
|
|
204
|
+
"label.noResult": "(no result submitted yet)",
|
|
205
|
+
"label.current": "current",
|
|
206
|
+
"label.checkpointHere": "checkpoint",
|
|
207
|
+
"placeholder.result":
|
|
208
|
+
"State what this node produced. Its whole execution range is replaced by this result, so it must stand on its own.",
|
|
209
|
+
"confirm.reset": "Clear this session's plan tree?",
|
|
210
|
+
"confirm.drop": "Drop “{title}”?",
|
|
211
|
+
"settings.title": "Tree Task Flow",
|
|
212
|
+
"settings.enabled": "Enabled",
|
|
213
|
+
"settings.disabled": "Not enabled",
|
|
214
|
+
"settings.impact": "What it does once enabled",
|
|
215
|
+
"settings.impact.1": "Adds one static tool-group section to every session's system prompt.",
|
|
216
|
+
"settings.impact.2": "Registers six tree_task_* tools.",
|
|
217
|
+
"settings.impact.3":
|
|
218
|
+
"When a node completes, folds its execution range out of the model-visible context and replaces it with one summary message carrying the submitted result.",
|
|
219
|
+
"settings.impact.4":
|
|
220
|
+
"When every child of a node finishes, the tree enters a checkpoint: only tree_task_plan and tree_task_done are allowed there.",
|
|
221
|
+
"settings.impact.5":
|
|
222
|
+
"Optional auto-continue when the model goes idle (off by default; never resumes after you stop it).",
|
|
223
|
+
"settings.howto": "How to enable / disable",
|
|
224
|
+
"settings.howto.body":
|
|
225
|
+
"Set config.enabled on the task-tree row in the profile's cordis.patch.yml, then restart dsh web. Installing is not enabling — it is off by default.",
|
|
226
|
+
"settings.stateDir": "State directory",
|
|
227
|
+
"settings.sessions": "Sessions with a plan",
|
|
228
|
+
"settings.noSessions": "(none yet)",
|
|
229
|
+
"settings.checkpoint": "checkpoint",
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------- 样式
|
|
233
|
+
|
|
234
|
+
const S = {
|
|
235
|
+
wrap: { padding: "10px 12px", font: "13px/1.6 system-ui, sans-serif", overflow: "auto", height: "100%" },
|
|
236
|
+
// 输入框上方的条目:和内置 todo / goal 条目共用同一片区域。
|
|
237
|
+
// 宽度必须照它们的算法来——直接写 100% 会顶满整栏,和消息正文对不齐。
|
|
238
|
+
dock: {
|
|
239
|
+
boxSizing: "border-box",
|
|
240
|
+
width:
|
|
241
|
+
"calc(100% - var(--dsh-composer-side-clearance) - var(--dsh-composer-side-clearance)" +
|
|
242
|
+
" - var(--dsh-composer-dock-inset) - var(--dsh-composer-dock-inset)" +
|
|
243
|
+
" - var(--dsh-composer-dock-inset) - var(--dsh-composer-dock-inset))",
|
|
244
|
+
maxWidth:
|
|
245
|
+
"calc(var(--dsh-composer-card-max-width)" +
|
|
246
|
+
" - var(--dsh-composer-dock-inset) - var(--dsh-composer-dock-inset)" +
|
|
247
|
+
" - var(--dsh-composer-dock-inset) - var(--dsh-composer-dock-inset))",
|
|
248
|
+
border: "0.5px solid var(--dsw-alias-border-l1, #00000018)",
|
|
249
|
+
background: "var(--dsw-specific-tip, #00000008)",
|
|
250
|
+
borderRadius: "12px",
|
|
251
|
+
flex: "none",
|
|
252
|
+
margin: "0 auto",
|
|
253
|
+
overflow: "hidden",
|
|
254
|
+
"--dsh-scrollbar-thumb": "var(--dsw-alias-scrollbar-bg-l2)",
|
|
255
|
+
"--dsh-scrollbar-thumb-hover": "var(--dsw-alias-scrollbar-hover-l2)",
|
|
256
|
+
},
|
|
257
|
+
dockHead: { display: "flex", alignItems: "center", gap: "8px", padding: "0 12px" },
|
|
258
|
+
dockToggle: {
|
|
259
|
+
display: "flex",
|
|
260
|
+
alignItems: "center",
|
|
261
|
+
gap: "8px",
|
|
262
|
+
flex: "auto",
|
|
263
|
+
minWidth: 0,
|
|
264
|
+
background: "none",
|
|
265
|
+
border: "none",
|
|
266
|
+
color: "inherit",
|
|
267
|
+
cursor: "pointer",
|
|
268
|
+
padding: "6px 0",
|
|
269
|
+
font: "inherit",
|
|
270
|
+
textAlign: "left",
|
|
271
|
+
},
|
|
272
|
+
dockTitle: { fontWeight: 500, flex: "none", fontSize: "13px" },
|
|
273
|
+
dockProgress: {
|
|
274
|
+
color: "var(--dsw-alias-label-tertiary, #00000088)",
|
|
275
|
+
fontSize: "13px",
|
|
276
|
+
flex: "auto",
|
|
277
|
+
minWidth: 0,
|
|
278
|
+
overflow: "hidden",
|
|
279
|
+
textOverflow: "ellipsis",
|
|
280
|
+
whiteSpace: "nowrap",
|
|
281
|
+
},
|
|
282
|
+
dockChevron: { color: "var(--dsw-alias-label-tertiary, #00000088)", flex: "none" },
|
|
283
|
+
dockBody: { padding: "0 12px 8px", maxHeight: "260px", overflow: "auto" },
|
|
284
|
+
bar: { display: "flex", gap: "6px", flexWrap: "wrap", alignItems: "center" },
|
|
285
|
+
btn: {
|
|
286
|
+
border: "1px solid var(--dsh-border, #d0d0d0)",
|
|
287
|
+
background: "transparent",
|
|
288
|
+
color: "inherit",
|
|
289
|
+
borderRadius: "6px",
|
|
290
|
+
padding: "3px 8px",
|
|
291
|
+
cursor: "pointer",
|
|
292
|
+
font: "inherit",
|
|
293
|
+
},
|
|
294
|
+
badge: { borderRadius: "6px", padding: "2px 6px", background: "var(--dsh-muted, #00000010)" },
|
|
295
|
+
badgeWarn: {
|
|
296
|
+
borderRadius: "6px",
|
|
297
|
+
padding: "2px 6px",
|
|
298
|
+
background: "var(--dsh-warn, #ffcc0033)",
|
|
299
|
+
border: "1px solid var(--dsh-warn-border, #cc990055)",
|
|
300
|
+
},
|
|
301
|
+
warn: {
|
|
302
|
+
marginTop: "6px",
|
|
303
|
+
padding: "6px 8px",
|
|
304
|
+
borderRadius: "6px",
|
|
305
|
+
fontSize: "13px",
|
|
306
|
+
background: "var(--dsh-warn, #ffcc0033)",
|
|
307
|
+
},
|
|
308
|
+
node: { marginLeft: "0", padding: "3px 0" },
|
|
309
|
+
child: { marginLeft: "14px", borderLeft: "1px solid var(--dsh-border, #00000018)", paddingLeft: "8px" },
|
|
310
|
+
stepTitle: { display: "flex", gap: "6px", alignItems: "center", flexWrap: "wrap" },
|
|
311
|
+
done: { opacity: 0.55 },
|
|
312
|
+
cur: { fontWeight: 600 },
|
|
313
|
+
meta: { opacity: 0.7, font: "12px/1.5 ui-monospace, monospace", wordBreak: "break-all" },
|
|
314
|
+
resultLine: { opacity: 0.75, marginTop: "1px" },
|
|
315
|
+
hint: { opacity: 0.65, marginTop: "6px" },
|
|
316
|
+
err: { color: "var(--dsh-danger, #c00)", whiteSpace: "pre-wrap" },
|
|
317
|
+
pre: {
|
|
318
|
+
margin: "2px 0 0",
|
|
319
|
+
padding: "6px 8px",
|
|
320
|
+
background: "var(--dsh-muted, #00000010)",
|
|
321
|
+
borderRadius: "6px",
|
|
322
|
+
whiteSpace: "pre-wrap",
|
|
323
|
+
wordBreak: "break-word",
|
|
324
|
+
font: "12px/1.5 ui-monospace, monospace",
|
|
325
|
+
maxHeight: "220px",
|
|
326
|
+
overflow: "auto",
|
|
327
|
+
},
|
|
328
|
+
form: { marginTop: "6px", border: "1px solid var(--dsh-border, #d0d0d0)", borderRadius: "6px", padding: "6px" },
|
|
329
|
+
textarea: {
|
|
330
|
+
width: "100%",
|
|
331
|
+
boxSizing: "border-box",
|
|
332
|
+
minHeight: "64px",
|
|
333
|
+
font: "12px/1.5 ui-monospace, monospace",
|
|
334
|
+
background: "transparent",
|
|
335
|
+
color: "inherit",
|
|
336
|
+
border: "1px solid var(--dsh-border, #d0d0d0)",
|
|
337
|
+
borderRadius: "4px",
|
|
338
|
+
padding: "4px",
|
|
339
|
+
resize: "vertical",
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// ---------------------------------------------------------------- 零件
|
|
344
|
+
|
|
345
|
+
/** 一行节点的抬头:标记 + 标题 + 徽章 + id + 操作按钮。 */
|
|
346
|
+
function NodeHeader({ node, t, isCurrent, isCheckpoint, onToggle, onDone, onDrop, canDrop, busy }) {
|
|
347
|
+
const canAct = node.status === "pending";
|
|
348
|
+
return h(
|
|
349
|
+
"div",
|
|
350
|
+
{ style: S.stepTitle },
|
|
351
|
+
h("span", null, mark(node)),
|
|
352
|
+
h(
|
|
353
|
+
"a",
|
|
354
|
+
{
|
|
355
|
+
href: "#",
|
|
356
|
+
style: { color: "inherit" },
|
|
357
|
+
onClick: (event) => {
|
|
358
|
+
event.preventDefault();
|
|
359
|
+
onToggle();
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
node.title,
|
|
363
|
+
),
|
|
364
|
+
isCurrent ? h("span", { style: S.badge }, t("label.current")) : null,
|
|
365
|
+
isCheckpoint ? h("span", { style: S.badgeWarn }, t("label.checkpointHere")) : null,
|
|
366
|
+
h("span", { style: S.meta }, node.id),
|
|
367
|
+
canAct ? h("button", { style: S.btn, disabled: busy, onClick: onDone }, t("btn.done")) : null,
|
|
368
|
+
canAct && canDrop
|
|
369
|
+
? h("button", { style: S.btn, disabled: busy, onClick: onDrop }, t("btn.drop"))
|
|
370
|
+
: null,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** 提交结果的输入框。空文本提交不出去——host 也会拒绝,这里先挡住。 */
|
|
375
|
+
function CompleteForm({ onSubmit, onCancel, busy, error, t }) {
|
|
376
|
+
const [text, setText] = useState("");
|
|
377
|
+
return h(
|
|
378
|
+
"div",
|
|
379
|
+
{ style: S.form },
|
|
380
|
+
h("textarea", {
|
|
381
|
+
style: S.textarea,
|
|
382
|
+
value: text,
|
|
383
|
+
rows: 4,
|
|
384
|
+
placeholder: t("placeholder.result"),
|
|
385
|
+
onChange: (event) => setText(event.target.value),
|
|
386
|
+
}),
|
|
387
|
+
error ? h("div", { style: S.err }, error) : null,
|
|
388
|
+
h(
|
|
389
|
+
"div",
|
|
390
|
+
{ style: Object.assign({}, S.bar, { marginTop: "6px" }) },
|
|
391
|
+
h(
|
|
392
|
+
"button",
|
|
393
|
+
{
|
|
394
|
+
style: S.btn,
|
|
395
|
+
disabled: busy || text.trim() === "",
|
|
396
|
+
onClick: () => onSubmit(text),
|
|
397
|
+
},
|
|
398
|
+
t("btn.submit"),
|
|
399
|
+
),
|
|
400
|
+
h("button", { style: S.btn, disabled: busy, onClick: onCancel }, t("btn.cancel")),
|
|
401
|
+
),
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** 一个节点提交过的结果。折叠时只报首行,展开时给全文。 */
|
|
406
|
+
function ResultBlock({ node, t, expanded }) {
|
|
407
|
+
if (!node.result) {
|
|
408
|
+
return expanded ? h("div", { style: S.meta }, t("label.result") + ":" + t("label.noResult")) : null;
|
|
409
|
+
}
|
|
410
|
+
if (!expanded) {
|
|
411
|
+
return h("div", { style: S.resultLine }, t("label.result") + ":" + firstLine(node.result.text, 100));
|
|
412
|
+
}
|
|
413
|
+
return h(
|
|
414
|
+
"div",
|
|
415
|
+
null,
|
|
416
|
+
h("div", { style: S.meta }, t("label.result") + "(" + formatTime(node.result.at) + ")"),
|
|
417
|
+
h("pre", { style: S.pre }, node.result.text),
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function StatusBar({ data, sessionId, reload, t }) {
|
|
422
|
+
const [busy, setBusy] = useState(false);
|
|
423
|
+
|
|
424
|
+
const act = useCallback(
|
|
425
|
+
async (body) => {
|
|
426
|
+
setBusy(true);
|
|
427
|
+
try {
|
|
428
|
+
await post(Object.assign({ sessionId }, body));
|
|
429
|
+
await reload();
|
|
430
|
+
} finally {
|
|
431
|
+
setBusy(false);
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
[sessionId, reload],
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
const checkpoint = data.checkpoint;
|
|
438
|
+
const badge = checkpoint
|
|
439
|
+
? t("state.checkpoint")
|
|
440
|
+
: data.stopped
|
|
441
|
+
? t("state.stopped")
|
|
442
|
+
: data.active === null
|
|
443
|
+
? t("state.done")
|
|
444
|
+
: t("state.running");
|
|
445
|
+
|
|
446
|
+
return h(
|
|
447
|
+
"div",
|
|
448
|
+
null,
|
|
449
|
+
h(
|
|
450
|
+
"div",
|
|
451
|
+
{ style: S.bar },
|
|
452
|
+
h("span", { style: checkpoint ? S.badgeWarn : S.badge }, badge),
|
|
453
|
+
h(
|
|
454
|
+
"button",
|
|
455
|
+
{ style: S.btn, disabled: busy, onClick: () => act({ action: data.stopped ? "resume" : "stop" }) },
|
|
456
|
+
data.stopped ? t("btn.resume") : t("btn.stop"),
|
|
457
|
+
),
|
|
458
|
+
h(
|
|
459
|
+
"button",
|
|
460
|
+
{
|
|
461
|
+
style: S.btn,
|
|
462
|
+
disabled: busy,
|
|
463
|
+
onClick: () => {
|
|
464
|
+
if (window.confirm(t("confirm.reset"))) act({ action: "reset" });
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
t("btn.reset"),
|
|
468
|
+
),
|
|
469
|
+
h("button", { style: S.btn, disabled: busy, onClick: reload }, t("btn.refresh")),
|
|
470
|
+
),
|
|
471
|
+
checkpoint
|
|
472
|
+
? h(
|
|
473
|
+
"div",
|
|
474
|
+
{ style: S.warn },
|
|
475
|
+
t("state.checkpoint") + ":" + checkpoint.label + "「" + checkpoint.title + "」",
|
|
476
|
+
h("div", null, t("checkpoint.note", { title: checkpoint.title })),
|
|
477
|
+
)
|
|
478
|
+
: null,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function StepNode({ step, isCurrent, isCheckpoint, sessionId, reload, t, defaultOpen = true }) {
|
|
483
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
484
|
+
const [composing, setComposing] = useState(false);
|
|
485
|
+
const [busy, setBusy] = useState(false);
|
|
486
|
+
const [error, setError] = useState(null);
|
|
487
|
+
|
|
488
|
+
const act = useCallback(
|
|
489
|
+
async (body) => {
|
|
490
|
+
setBusy(true);
|
|
491
|
+
setError(null);
|
|
492
|
+
try {
|
|
493
|
+
await post(Object.assign({ sessionId }, body));
|
|
494
|
+
setComposing(false);
|
|
495
|
+
await reload();
|
|
496
|
+
} catch (failure) {
|
|
497
|
+
setError(String((failure && failure.message) || failure));
|
|
498
|
+
} finally {
|
|
499
|
+
setBusy(false);
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
[sessionId, reload],
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
const style = Object.assign({}, S.node, step.status === "done" ? S.done : null);
|
|
506
|
+
|
|
507
|
+
return h(
|
|
508
|
+
"div",
|
|
509
|
+
{ style },
|
|
510
|
+
h(NodeHeader, {
|
|
511
|
+
node: step,
|
|
512
|
+
t,
|
|
513
|
+
isCurrent,
|
|
514
|
+
isCheckpoint,
|
|
515
|
+
onToggle: () => setOpen(!open),
|
|
516
|
+
onDone: () => {
|
|
517
|
+
setComposing(true);
|
|
518
|
+
setOpen(true);
|
|
519
|
+
},
|
|
520
|
+
onDrop: () => {
|
|
521
|
+
if (window.confirm(t("confirm.drop", { title: step.title }))) act({ action: "drop", id: step.id });
|
|
522
|
+
},
|
|
523
|
+
canDrop: true,
|
|
524
|
+
busy,
|
|
525
|
+
}),
|
|
526
|
+
h(ResultBlock, { node: step, t, expanded: false }),
|
|
527
|
+
open
|
|
528
|
+
? h(
|
|
529
|
+
"div",
|
|
530
|
+
{ style: S.child },
|
|
531
|
+
step.detail ? h("div", { style: S.meta }, step.detail) : null,
|
|
532
|
+
h(ResultBlock, { node: step, t, expanded: true }),
|
|
533
|
+
composing
|
|
534
|
+
? h(CompleteForm, {
|
|
535
|
+
t,
|
|
536
|
+
busy,
|
|
537
|
+
error,
|
|
538
|
+
onSubmit: (text) => act({ action: "done", id: step.id, result: text }),
|
|
539
|
+
onCancel: () => {
|
|
540
|
+
setComposing(false);
|
|
541
|
+
setError(null);
|
|
542
|
+
},
|
|
543
|
+
})
|
|
544
|
+
: null,
|
|
545
|
+
)
|
|
546
|
+
: null,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function TaskNode({ task, active, checkpoint, sessionId, reload, t, defaultOpen = true }) {
|
|
551
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
552
|
+
const [composing, setComposing] = useState(false);
|
|
553
|
+
const [busy, setBusy] = useState(false);
|
|
554
|
+
const [error, setError] = useState(null);
|
|
555
|
+
|
|
556
|
+
const act = useCallback(
|
|
557
|
+
async (body) => {
|
|
558
|
+
setBusy(true);
|
|
559
|
+
setError(null);
|
|
560
|
+
try {
|
|
561
|
+
await post(Object.assign({ sessionId }, body));
|
|
562
|
+
setComposing(false);
|
|
563
|
+
await reload();
|
|
564
|
+
} catch (failure) {
|
|
565
|
+
setError(String((failure && failure.message) || failure));
|
|
566
|
+
} finally {
|
|
567
|
+
setBusy(false);
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
[sessionId, reload],
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
const style = Object.assign({}, S.node, task.status === "done" ? S.done : null);
|
|
574
|
+
const steps = task.steps || [];
|
|
575
|
+
|
|
576
|
+
return h(
|
|
577
|
+
"div",
|
|
578
|
+
{ style },
|
|
579
|
+
h(NodeHeader, {
|
|
580
|
+
node: task,
|
|
581
|
+
t,
|
|
582
|
+
isCurrent: active?.taskId === task.id,
|
|
583
|
+
isCheckpoint: checkpoint?.id === task.id,
|
|
584
|
+
onToggle: () => setOpen(!open),
|
|
585
|
+
onDone: () => {
|
|
586
|
+
setComposing(true);
|
|
587
|
+
setOpen(true);
|
|
588
|
+
},
|
|
589
|
+
onDrop: () => {
|
|
590
|
+
if (window.confirm(t("confirm.drop", { title: task.title }))) act({ action: "drop", id: task.id });
|
|
591
|
+
},
|
|
592
|
+
canDrop: true,
|
|
593
|
+
busy,
|
|
594
|
+
}),
|
|
595
|
+
h(ResultBlock, { node: task, t, expanded: false }),
|
|
596
|
+
open
|
|
597
|
+
? h(
|
|
598
|
+
"div",
|
|
599
|
+
{ style: S.child },
|
|
600
|
+
task.detail ? h("div", { style: S.meta }, task.detail) : null,
|
|
601
|
+
h(ResultBlock, { node: task, t, expanded: true }),
|
|
602
|
+
composing
|
|
603
|
+
? h(CompleteForm, {
|
|
604
|
+
t,
|
|
605
|
+
busy,
|
|
606
|
+
error,
|
|
607
|
+
onSubmit: (text) => act({ action: "done", id: task.id, result: text }),
|
|
608
|
+
onCancel: () => {
|
|
609
|
+
setComposing(false);
|
|
610
|
+
setError(null);
|
|
611
|
+
},
|
|
612
|
+
})
|
|
613
|
+
: null,
|
|
614
|
+
steps.length === 0
|
|
615
|
+
? h("div", { style: S.hint }, "(还没有子任务 → 让模型调用 tree_task_plan)")
|
|
616
|
+
: steps.map((step) =>
|
|
617
|
+
h(StepNode, {
|
|
618
|
+
key: step.id,
|
|
619
|
+
step,
|
|
620
|
+
isCurrent: active?.stepId === step.id,
|
|
621
|
+
isCheckpoint: checkpoint?.id === step.id,
|
|
622
|
+
sessionId,
|
|
623
|
+
reload,
|
|
624
|
+
t,
|
|
625
|
+
defaultOpen,
|
|
626
|
+
}),
|
|
627
|
+
),
|
|
628
|
+
)
|
|
629
|
+
: null,
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function GoalNode({ goal, active, checkpoint, sessionId, reload, t, defaultOpen = true }) {
|
|
634
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
635
|
+
const [composing, setComposing] = useState(false);
|
|
636
|
+
const [busy, setBusy] = useState(false);
|
|
637
|
+
const [error, setError] = useState(null);
|
|
638
|
+
|
|
639
|
+
const act = useCallback(
|
|
640
|
+
async (body) => {
|
|
641
|
+
setBusy(true);
|
|
642
|
+
setError(null);
|
|
643
|
+
try {
|
|
644
|
+
await post(Object.assign({ sessionId }, body));
|
|
645
|
+
setComposing(false);
|
|
646
|
+
await reload();
|
|
647
|
+
} catch (failure) {
|
|
648
|
+
setError(String((failure && failure.message) || failure));
|
|
649
|
+
} finally {
|
|
650
|
+
setBusy(false);
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
[sessionId, reload],
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
const tasks = goal.tasks || [];
|
|
657
|
+
|
|
658
|
+
return h(
|
|
659
|
+
"div",
|
|
660
|
+
null,
|
|
661
|
+
h(NodeHeader, {
|
|
662
|
+
node: goal,
|
|
663
|
+
t,
|
|
664
|
+
isCurrent: active?.goalId === goal.id,
|
|
665
|
+
isCheckpoint: checkpoint?.id === goal.id,
|
|
666
|
+
onToggle: () => setOpen(!open),
|
|
667
|
+
onDone: () => {
|
|
668
|
+
setComposing(true);
|
|
669
|
+
setOpen(true);
|
|
670
|
+
},
|
|
671
|
+
// 整个目标不能丢弃:plan.js 会拒绝,这里也不给按钮。
|
|
672
|
+
canDrop: false,
|
|
673
|
+
busy,
|
|
674
|
+
}),
|
|
675
|
+
h(ResultBlock, { node: goal, t, expanded: false }),
|
|
676
|
+
open
|
|
677
|
+
? h(
|
|
678
|
+
"div",
|
|
679
|
+
{ style: S.child },
|
|
680
|
+
goal.detail ? h("div", { style: S.meta }, goal.detail) : null,
|
|
681
|
+
h(ResultBlock, { node: goal, t, expanded: true }),
|
|
682
|
+
composing
|
|
683
|
+
? h(CompleteForm, {
|
|
684
|
+
t,
|
|
685
|
+
busy,
|
|
686
|
+
error,
|
|
687
|
+
onSubmit: (text) => act({ action: "done", id: goal.id, result: text }),
|
|
688
|
+
onCancel: () => {
|
|
689
|
+
setComposing(false);
|
|
690
|
+
setError(null);
|
|
691
|
+
},
|
|
692
|
+
})
|
|
693
|
+
: null,
|
|
694
|
+
tasks.map((task) =>
|
|
695
|
+
h(TaskNode, {
|
|
696
|
+
key: task.id,
|
|
697
|
+
task,
|
|
698
|
+
active,
|
|
699
|
+
checkpoint,
|
|
700
|
+
sessionId,
|
|
701
|
+
reload,
|
|
702
|
+
t,
|
|
703
|
+
defaultOpen,
|
|
704
|
+
}),
|
|
705
|
+
),
|
|
706
|
+
)
|
|
707
|
+
: null,
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ---------------------------------------------------------------- 条目
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* 输入框上方的全宽条目。
|
|
715
|
+
*
|
|
716
|
+
* 折叠时只有一行:标题 + 进度 + 当前节点。展开才画整棵树,并限高滚动——
|
|
717
|
+
* 这是输入框旁边的位置,不能让它把会话挤没了。
|
|
718
|
+
*
|
|
719
|
+
* 会话没有计划时返回 null:内置的 todo / goal 条目不该因为我而挪位。
|
|
720
|
+
*/
|
|
721
|
+
function TaskTreeDock(props) {
|
|
722
|
+
const t = useT();
|
|
723
|
+
const sessionId = props.sessionId;
|
|
724
|
+
const [state, setState] = useState({ loading: true });
|
|
725
|
+
const [open, setOpen] = useState(false);
|
|
726
|
+
|
|
727
|
+
const reload = useCallback(async () => {
|
|
728
|
+
if (!sessionId) {
|
|
729
|
+
setState({ loading: false, empty: true });
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
try {
|
|
733
|
+
const data = await api("/dsh-tree-task-flow/tree?sessionId=" + encodeURIComponent(sessionId));
|
|
734
|
+
setState({ loading: false, data });
|
|
735
|
+
} catch {
|
|
736
|
+
// 没有计划(404)是正常状态;真出错也不该占位。两种都当作"没有"。
|
|
737
|
+
setState({ loading: false, empty: true });
|
|
738
|
+
}
|
|
739
|
+
}, [sessionId]);
|
|
740
|
+
|
|
741
|
+
useEffect(() => {
|
|
742
|
+
reload();
|
|
743
|
+
}, [reload]);
|
|
744
|
+
|
|
745
|
+
if (state.loading || state.empty || !state.data) return null;
|
|
746
|
+
|
|
747
|
+
const data = state.data;
|
|
748
|
+
const stats = countNodes(data.plan.goal);
|
|
749
|
+
const current = currentLabel(data);
|
|
750
|
+
|
|
751
|
+
return h(
|
|
752
|
+
"div",
|
|
753
|
+
{ style: S.dock },
|
|
754
|
+
h(
|
|
755
|
+
"div",
|
|
756
|
+
{ style: S.dockHead },
|
|
757
|
+
h(
|
|
758
|
+
"button",
|
|
759
|
+
{ style: S.dockToggle, onClick: () => setOpen(!open) },
|
|
760
|
+
h("span", { style: S.dockTitle }, t("nav")),
|
|
761
|
+
h(
|
|
762
|
+
"span",
|
|
763
|
+
{ style: S.dockProgress },
|
|
764
|
+
stats.done + "/" + stats.total + (current ? " · " + current : ""),
|
|
765
|
+
),
|
|
766
|
+
h("span", { style: S.dockChevron }, open ? "▾" : "▸"),
|
|
767
|
+
),
|
|
768
|
+
data.checkpoint ? h("span", { style: S.badgeWarn }, t("state.checkpoint")) : null,
|
|
769
|
+
),
|
|
770
|
+
open
|
|
771
|
+
? h(
|
|
772
|
+
"div",
|
|
773
|
+
{ style: S.dockBody },
|
|
774
|
+
h(StatusBar, { data, sessionId, reload, t }),
|
|
775
|
+
h(
|
|
776
|
+
"div",
|
|
777
|
+
{ style: { marginTop: "6px" } },
|
|
778
|
+
h(GoalNode, {
|
|
779
|
+
goal: data.plan.goal,
|
|
780
|
+
active: data.active,
|
|
781
|
+
checkpoint: data.checkpoint,
|
|
782
|
+
sessionId,
|
|
783
|
+
reload,
|
|
784
|
+
t,
|
|
785
|
+
// 展开整棵树的动作已经由上面那一行承担,这里默认为折叠。
|
|
786
|
+
defaultOpen: false,
|
|
787
|
+
}),
|
|
788
|
+
),
|
|
789
|
+
)
|
|
790
|
+
: null,
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// ---------------------------------------------------------------- 设置页
|
|
795
|
+
|
|
796
|
+
function SettingsSection() {
|
|
797
|
+
const t = useT();
|
|
798
|
+
const [state, setState] = useState({ loading: true });
|
|
799
|
+
|
|
800
|
+
useEffect(() => {
|
|
801
|
+
api("/dsh-tree-task-flow/sessions")
|
|
802
|
+
.then((data) => setState({ loading: false, data }))
|
|
803
|
+
.catch((error) => setState({ loading: false, error: String((error && error.message) || error) }));
|
|
804
|
+
}, []);
|
|
805
|
+
|
|
806
|
+
if (state.loading) return h("div", { style: S.wrap }, "…");
|
|
807
|
+
if (state.error) return h("div", { style: S.wrap }, h("div", { style: S.err }, state.error));
|
|
808
|
+
|
|
809
|
+
const enabled = state.data && state.data.enabled === true;
|
|
810
|
+
const sessions = (state.data && state.data.sessions) || [];
|
|
811
|
+
|
|
812
|
+
const row = (label, value) =>
|
|
813
|
+
h(
|
|
814
|
+
"div",
|
|
815
|
+
{ style: { display: "flex", gap: "8px", margin: "2px 0" } },
|
|
816
|
+
h("span", { style: { minWidth: "9em", opacity: 0.7 } }, label),
|
|
817
|
+
h("span", null, value),
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
return h(
|
|
821
|
+
"div",
|
|
822
|
+
{ style: S.wrap },
|
|
823
|
+
h("div", { style: { fontWeight: 600, marginBottom: "6px" } }, t("settings.title")),
|
|
824
|
+
row(
|
|
825
|
+
"",
|
|
826
|
+
h("span", { style: S.badge }, enabled ? t("settings.enabled") : t("settings.disabled")),
|
|
827
|
+
),
|
|
828
|
+
h("div", { style: { marginTop: "10px", fontWeight: 600 } }, t("settings.impact")),
|
|
829
|
+
h(
|
|
830
|
+
"ul",
|
|
831
|
+
{ style: { margin: "4px 0 10px 1.2em", padding: 0 } },
|
|
832
|
+
[
|
|
833
|
+
"settings.impact.1",
|
|
834
|
+
"settings.impact.2",
|
|
835
|
+
"settings.impact.3",
|
|
836
|
+
"settings.impact.4",
|
|
837
|
+
"settings.impact.5",
|
|
838
|
+
].map((key) => h("li", { key }, t(key))),
|
|
839
|
+
),
|
|
840
|
+
h("div", { style: { fontWeight: 600 } }, t("settings.howto")),
|
|
841
|
+
h("div", { style: S.hint }, t("settings.howto.body")),
|
|
842
|
+
h("div", { style: { marginTop: "10px", fontWeight: 600 } }, t("settings.stateDir")),
|
|
843
|
+
h("div", { style: S.meta }, "$DSH_HOME/dsh-task-tree/"),
|
|
844
|
+
h(
|
|
845
|
+
"div",
|
|
846
|
+
{ style: { marginTop: "10px", fontWeight: 600 } },
|
|
847
|
+
t("settings.sessions") + ":" + sessions.length,
|
|
848
|
+
),
|
|
849
|
+
sessions.length === 0
|
|
850
|
+
? h("div", { style: S.hint }, t("settings.noSessions"))
|
|
851
|
+
: h(
|
|
852
|
+
"div",
|
|
853
|
+
{ style: S.child },
|
|
854
|
+
sessions.map((item) =>
|
|
855
|
+
h(
|
|
856
|
+
"div",
|
|
857
|
+
{ key: item.sessionId, style: S.meta },
|
|
858
|
+
item.goal +
|
|
859
|
+
" · " +
|
|
860
|
+
item.done +
|
|
861
|
+
"/" +
|
|
862
|
+
item.steps +
|
|
863
|
+
(item.checkpoint ? " · " + t("settings.checkpoint") : "") +
|
|
864
|
+
" · " +
|
|
865
|
+
item.sessionId,
|
|
866
|
+
),
|
|
867
|
+
),
|
|
868
|
+
),
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// ---------------------------------------------------------------- 挂载
|
|
873
|
+
|
|
874
|
+
function apply(ctx) {
|
|
875
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-tree-task-flow: dictionaries");
|
|
876
|
+
translate = ctx.locale.bind(NS);
|
|
877
|
+
|
|
878
|
+
// 输入框上方的条目。list 槽 + 自己的 id ⇒ 只会并列加在内置
|
|
879
|
+
// 的 queue / todo / goal 旁边,不会替换任何一个。
|
|
880
|
+
ctx.effect(
|
|
881
|
+
() =>
|
|
882
|
+
ctx.slots.inject("conversation.input.dock", () =>
|
|
883
|
+
ctx.slots.register(
|
|
884
|
+
{
|
|
885
|
+
name: "conversation.input.dock",
|
|
886
|
+
id: ENTRY_ID,
|
|
887
|
+
order: 50,
|
|
888
|
+
locale: NS,
|
|
889
|
+
},
|
|
890
|
+
TaskTreeDock,
|
|
891
|
+
),
|
|
892
|
+
),
|
|
893
|
+
"dsh-tree-task-flow: conversation dock",
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
// 设置页:只显示状态与影响说明(启用与否由 profile 配置决定,运行时不改)。
|
|
897
|
+
ctx.effect(
|
|
898
|
+
() =>
|
|
899
|
+
ctx.slots.inject("settings.section", () =>
|
|
900
|
+
ctx.slots.register(
|
|
901
|
+
{
|
|
902
|
+
name: "settings.section",
|
|
903
|
+
id: ENTRY_ID,
|
|
904
|
+
order: 45,
|
|
905
|
+
label: () => translate("nav"),
|
|
906
|
+
locale: NS,
|
|
907
|
+
inject: () => ({}),
|
|
908
|
+
},
|
|
909
|
+
SettingsSection,
|
|
910
|
+
),
|
|
911
|
+
),
|
|
912
|
+
"dsh-tree-task-flow: settings section",
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
exports.name = name;
|
|
917
|
+
exports.inject = inject;
|
|
918
|
+
exports.apply = apply;
|
|
919
|
+
return module.exports;
|
|
920
|
+
},
|
|
921
|
+
});
|