agent-loft 0.2.30 → 0.2.32

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 (47) hide show
  1. package/README.md +11 -1
  2. package/package.json +1 -1
  3. package/src/agents/claude/driver.ts +13 -8
  4. package/src/agents/claude/session.ts +38 -6
  5. package/src/agents/codex/session.ts +32 -5
  6. package/src/channels/feishu/bot.ts +8 -4
  7. package/src/channels/feishu/turn-stream.ts +3 -2
  8. package/src/channels/web/api.ts +8 -0
  9. package/src/channels/web/http.ts +2 -2
  10. package/src/channels/web/server.ts +21 -1
  11. package/src/channels/web/skills-api.ts +225 -0
  12. package/src/channels/web/tasks-api.ts +94 -0
  13. package/src/core/chat-mode.ts +16 -16
  14. package/src/core/command-catalog.ts +8 -1
  15. package/src/core/commands.ts +3 -0
  16. package/src/core/hub.ts +147 -19
  17. package/src/core/memory-mcp.ts +1 -1
  18. package/src/core/personal.ts +5 -1
  19. package/src/core/skill-runtime.ts +359 -0
  20. package/src/core/skills-bridge.ts +288 -0
  21. package/src/core/skills.ts +675 -0
  22. package/src/core/store.ts +2 -0
  23. package/src/core/task-prompt.ts +38 -0
  24. package/src/core/task-runner.ts +232 -0
  25. package/src/core/task-schedule.ts +250 -0
  26. package/src/core/tasks-bridge.ts +256 -0
  27. package/src/core/tasks-mcp.ts +66 -0
  28. package/src/core/tasks.ts +281 -0
  29. package/src/core/types.ts +21 -0
  30. package/src/core/zip.ts +98 -0
  31. package/src/main.ts +12 -0
  32. package/src/mcp/tasks-server.ts +327 -0
  33. package/web/css/skills.css +368 -0
  34. package/web/css/tasks.css +455 -0
  35. package/web/index.html +2 -0
  36. package/web/lib/dom.js +1 -0
  37. package/web/lib/ws.js +8 -0
  38. package/web/state.js +2 -0
  39. package/web/views/composer.js +15 -3
  40. package/web/views/conv-actions.js +15 -7
  41. package/web/views/layout.js +22 -1
  42. package/web/views/mention.js +193 -0
  43. package/web/views/messages.js +23 -2
  44. package/web/views/sidebar.js +4 -0
  45. package/web/views/skills.js +703 -0
  46. package/web/views/tasks.js +469 -0
  47. package/web/views/topbar.js +9 -3
@@ -0,0 +1,469 @@
1
+ import { fill, h, icon, starsLoader, toast } from "../lib/dom.js";
2
+ import { api } from "../lib/api.js";
3
+ import { clockTime } from "../lib/time.js";
4
+ import { confirmDialog, modal, showMenu } from "../lib/overlay.js";
5
+ import { S, ui } from "../state.js";
6
+ import { WS } from "../lib/ws.js";
7
+ import { agentLabel, closeSide, renderSidebar } from "./sidebar.js";
8
+ import { baseName, renderTopbar, usableAgents } from "./topbar.js";
9
+ import { openConversation } from "./messages.js";
10
+ import { changeCwd } from "./conv-actions.js";
11
+ import { attachMention } from "./mention.js";
12
+
13
+ // ---------- 定时任务页 ----------
14
+ // 到点让 agent 按提示词干活,结果回到一条对话里;「有需要才提醒」时由 agent 判断这次要不要打扰你。
15
+ // 列表只有自己的任务(服务端按人过滤)。任务有变化时服务端推 { t: "tasks" },这里重新拉一遍。
16
+
17
+ const T = { list: [], tz: "", token: 0, reloadTimer: 0 };
18
+
19
+ const STATUS = {
20
+ running: { text: "运行中", cls: "run" },
21
+ notified: { text: "已提醒", cls: "ok" },
22
+ silent: { text: "无需提醒", cls: "quiet" },
23
+ error: { text: "出错", cls: "bad" },
24
+ skipped: { text: "跳过", cls: "quiet" },
25
+ interrupted: { text: "中断", cls: "bad" },
26
+ };
27
+
28
+ const FREQS = [
29
+ ["once", "一次"],
30
+ ["daily", "每天"],
31
+ ["weekdays", "工作日(周一到周五)"],
32
+ ["weekly", "每周"],
33
+ ["monthly", "每月"],
34
+ ["hourly", "每隔几小时"],
35
+ ["cron", "自定义(cron)"],
36
+ ];
37
+
38
+ const EXAMPLES = [
39
+ {
40
+ title: "检查 agent-loft 更新",
41
+ prompt: "查一下 npm 上 agent-loft 的最新版本(https://www.npmjs.com/package/agent-loft),和当前服务端版本比较。有新版本就告诉我新版本号和主要更新内容。",
42
+ schedule: { kind: "daily", time: "09:00" },
43
+ notify: "smart",
44
+ mode: "chat",
45
+ },
46
+ {
47
+ title: "科技新闻早报",
48
+ prompt: "搜一下过去 24 小时最重要的科技新闻,挑 5 条,每条用一两句话说明为什么值得关注,附上链接。",
49
+ schedule: { kind: "daily", time: "08:30" },
50
+ notify: "always",
51
+ mode: "chat",
52
+ },
53
+ {
54
+ title: "下班前看看改了什么",
55
+ prompt: "用 git status 和 git log --since=midnight 看看这个目录今天改了什么,帮我列一个简短的日报草稿。今天没有任何改动的话就不用提醒我。",
56
+ schedule: { kind: "weekdays", time: "18:00" },
57
+ notify: "smart",
58
+ mode: "work",
59
+ },
60
+ ];
61
+
62
+ /** 「今天 09:00」「明天 02:00」「10月25日 08:30」 */
63
+ function whenText(ms) {
64
+ const d = new Date(ms);
65
+ const now = new Date();
66
+ const hm = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
67
+ const day = (x) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
68
+ const diff = Math.round((day(d) - day(now)) / 86400000);
69
+ if (diff === 0) return `今天 ${hm}`;
70
+ if (diff === 1) return `明天 ${hm}`;
71
+ if (diff === 2) return `后天 ${hm}`;
72
+ return d.getFullYear() === now.getFullYear() ? `${d.getMonth() + 1}月${d.getDate()}日 ${hm}` : `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日 ${hm}`;
73
+ }
74
+
75
+ /** <input type="datetime-local"> 要的本地时间格式 */
76
+ function toLocalInput(ms) {
77
+ const d = new Date(ms);
78
+ const p = (n) => String(n).padStart(2, "0");
79
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`;
80
+ }
81
+
82
+ export async function openTasks() {
83
+ if (S.current) WS.raw({ t: "unsub", key: S.current });
84
+ S.current = null;
85
+ S.view = "tasks";
86
+ S.items = [];
87
+ S.turnAi = null;
88
+ history.replaceState(null, "", "#tasks");
89
+ WS.presence();
90
+ ui.spacer.style.height = "0px";
91
+ // 对话页的「贴底跟随」在这里不要:内容一变它就会滚到最底下
92
+ ui.follow = false;
93
+ // 输入框、「回到底部」藏起来(样式见 css/tasks.css 的 .tasks-view)
94
+ ui.main.classList.add("tasks-view");
95
+ closeSide();
96
+ renderTopbar();
97
+ renderSidebar();
98
+ fill(ui.msgInner, h("div", { class: "empty" }, starsLoader("lg")));
99
+ await reload();
100
+ // 从对话切过来时滚动条还停在对话底部
101
+ ui.messages.scrollTop = 0;
102
+ }
103
+
104
+ async function reload() {
105
+ const my = ++T.token;
106
+ try {
107
+ const data = await api("/api/tasks");
108
+ if (my !== T.token || S.view !== "tasks") return;
109
+ T.list = data.tasks;
110
+ T.tz = data.tz;
111
+ render();
112
+ } catch (err) {
113
+ if (my === T.token && S.view === "tasks") fill(ui.msgInner, h("div", { class: "empty" }, err.message));
114
+ }
115
+ }
116
+
117
+ // 服务端说任务变了(别的设备改了、跑完了一次):在这个页面上就重新拉,连着来的合成一次
118
+ document.addEventListener("aim:tasks", () => {
119
+ if (S.view !== "tasks") return;
120
+ clearTimeout(T.reloadTimer);
121
+ T.reloadTimer = setTimeout(() => void reload(), 300);
122
+ });
123
+
124
+ function render() {
125
+ fill(
126
+ ui.msgInner,
127
+ h(
128
+ "div",
129
+ { class: "tasks-page" },
130
+ h(
131
+ "div",
132
+ { class: "tasks-head" },
133
+ h(
134
+ "div",
135
+ { class: "grow" },
136
+ h("h2", {}, "定时任务"),
137
+ h("p", {}, `到点让 agent 自动去做,结果发回对话;可以只在有需要时才提醒你。时间按服务端时区(${T.tz})。`),
138
+ ),
139
+ h("button", { class: "btn primary", onclick: () => openTaskForm() }, icon("plus"), "新建"),
140
+ ),
141
+ T.list.length ? h("div", { class: "task-list" }, T.list.map(taskCard)) : emptyState(),
142
+ h(
143
+ "p",
144
+ { class: "tasks-tip" },
145
+ "也可以直接在对话里说「每天早上 9 点帮我……」,agent 会帮你建好(会先弹一张卡让你确认)。",
146
+ ),
147
+ ),
148
+ );
149
+ }
150
+
151
+ function emptyState() {
152
+ return h(
153
+ "div",
154
+ { class: "tasks-empty" },
155
+ h("div", { class: "tasks-empty-icon" }, icon("clock")),
156
+ h("div", { class: "t" }, "还没有定时任务"),
157
+ h("div", { class: "s" }, "从一个例子开始:"),
158
+ h(
159
+ "div",
160
+ { class: "task-examples" },
161
+ EXAMPLES.map((ex) =>
162
+ h("button", { class: "task-example", onclick: () => openTaskForm(null, ex) }, h("div", { class: "t" }, ex.title), h("div", { class: "s" }, ex.prompt)),
163
+ ),
164
+ ),
165
+ );
166
+ }
167
+
168
+ function openConv(key) {
169
+ if (!key) return;
170
+ void openConversation(key);
171
+ }
172
+
173
+ function runLine(run, compact) {
174
+ const st = STATUS[run.status] ?? { text: run.status, cls: "quiet" };
175
+ const detail = run.preview || run.note || "";
176
+ return h(
177
+ run.convKey ? "button" : "div",
178
+ {
179
+ class: `task-run${compact ? " compact" : ""}`,
180
+ ...(run.convKey ? { title: "打开这次运行所在的对话", onclick: () => openConv(run.convKey) } : {}),
181
+ },
182
+ h("span", { class: `task-status ${st.cls}` }, run.status === "running" ? h("span", { class: "conv-spin" }) : null, st.text),
183
+ h("span", { class: "task-run-at" }, clockTime(run.at)),
184
+ detail ? h("span", { class: "task-run-text" }, detail) : null,
185
+ );
186
+ }
187
+
188
+ function taskCard(t) {
189
+ const once = t.schedule.kind === "once";
190
+ const state = t.running
191
+ ? "运行中"
192
+ : t.enabled && t.nextRunAt
193
+ ? `下次 ${whenText(t.nextRunAt)}`
194
+ : once && t.runs.some((r) => r.status !== "skipped")
195
+ ? "已完成"
196
+ : "已暂停";
197
+ const where =
198
+ t.target.kind === "conv"
199
+ ? `发到「${t.convTitle || "对话"}」`
200
+ : `${agentLabel(t.agent)} · ${t.mode === "work" ? "工作模式" : "聊天模式"}${t.mode === "work" && t.cwd ? ` · ${baseName(t.cwd)}` : ""}`;
201
+ const toggle = h("input", { type: "checkbox", checked: t.enabled, title: t.enabled ? "暂停" : "启用" });
202
+ toggle.onchange = async () => {
203
+ try {
204
+ await api(`/api/tasks/${encodeURIComponent(t.id)}`, { method: "PATCH", body: { enabled: toggle.checked } });
205
+ toast(toggle.checked ? "已启用" : "已暂停");
206
+ void reload();
207
+ } catch (err) {
208
+ toggle.checked = !toggle.checked;
209
+ toast(err.message);
210
+ }
211
+ };
212
+ const more = h("button", { class: "icon-btn", title: "更多" }, icon("more"));
213
+ more.onclick = () =>
214
+ showMenu(more, [
215
+ { label: "编辑…", onclick: () => openTaskForm(t) },
216
+ { label: "立即运行", onclick: () => runNow(t) },
217
+ ...(t.convKey ? [{ label: "打开对话", onclick: () => openConv(t.convKey) }] : []),
218
+ "sep",
219
+ { label: "删除", danger: true, onclick: () => removeTask(t) },
220
+ ]);
221
+ const [last, ...older] = t.runs;
222
+ return h(
223
+ "div",
224
+ { class: `task-card${t.enabled ? "" : " off"}` },
225
+ h(
226
+ "div",
227
+ { class: "task-top" },
228
+ h("button", { class: "task-title", title: "编辑", onclick: () => openTaskForm(t) }, t.title),
229
+ h("label", { class: "switch", title: t.enabled ? "已启用,点击暂停" : "已暂停,点击启用" }, toggle, h("span", { class: "slider" })),
230
+ more,
231
+ ),
232
+ h(
233
+ "div",
234
+ { class: "task-meta" },
235
+ h("span", {}, t.summary),
236
+ h("span", {}, state),
237
+ h("span", {}, t.notify === "smart" ? "有需要才提醒" : "每次都提醒"),
238
+ h("span", {}, where),
239
+ ),
240
+ h("div", { class: "task-prompt" }, t.prompt),
241
+ last ? runLine(last) : h("div", { class: "task-run muted" }, "还没有运行过"),
242
+ older.length
243
+ ? h("details", { class: "task-history" }, h("summary", {}, `更早的 ${older.length} 次`), older.map((r) => runLine(r, true)))
244
+ : null,
245
+ );
246
+ }
247
+
248
+ async function runNow(t) {
249
+ try {
250
+ await api(`/api/tasks/${encodeURIComponent(t.id)}/run`, { method: "POST" });
251
+ toast("已开始运行,结果会发到对话里");
252
+ void reload();
253
+ } catch (err) {
254
+ toast(err.message);
255
+ }
256
+ }
257
+
258
+ async function removeTask(t) {
259
+ const sure = await confirmDialog({
260
+ title: `删除「${t.title}」?`,
261
+ message: "删除后不会再运行。之前运行的结果还留在对话里。",
262
+ confirmText: "删除",
263
+ danger: true,
264
+ });
265
+ if (!sure) return;
266
+ try {
267
+ await api(`/api/tasks/${encodeURIComponent(t.id)}`, { method: "DELETE" });
268
+ toast("已删除");
269
+ void reload();
270
+ } catch (err) {
271
+ toast(err.message);
272
+ }
273
+ }
274
+
275
+ // ---------- 新建 / 编辑 ----------
276
+
277
+ const WEEK_ORDER = [1, 2, 3, 4, 5, 6, 0];
278
+ const WEEK_NAME = ["日", "一", "二", "三", "四", "五", "六"];
279
+
280
+ /** task:编辑这个;preset:新建时预填(例子) */
281
+ export function openTaskForm(task, preset) {
282
+ const src = task ?? preset ?? {};
283
+ const agents = usableAgents();
284
+ const title = h("input", { value: src.title ?? "", maxlength: "60", placeholder: "比如:检查 agent-loft 更新" });
285
+ const prompt = h("textarea", { rows: 5, maxlength: "4000", placeholder: "到点让 agent 做什么。写成完整的指令:那时你多半不在,它没法再问你。可以用 @ 指定技能。" });
286
+ prompt.value = src.prompt ?? "";
287
+ // @ 技能:和输入框一样,到点时服务端认提示词里的 @name
288
+ const mentionMenu = h("div", { class: "slash-menu below", hidden: true });
289
+ const mention = attachMention(prompt, mentionMenu);
290
+ prompt.addEventListener("input", () => void mention.update());
291
+ prompt.addEventListener("blur", () => setTimeout(mention.close, 150));
292
+ prompt.addEventListener("keydown", (e) => {
293
+ if (mention.keydown(e)) e.preventDefault();
294
+ });
295
+
296
+ // ---- 频率 ----
297
+ const sch = { kind: "daily", time: "09:00", days: [1], day: 1, every: 2, minute: 0, expr: "0 9 * * *", at: Date.now() + 3600_000, ...(src.schedule ?? {}) };
298
+ if (sch.kind === "weekdays" || sch.kind === "daily") sch.days = sch.days ?? [1];
299
+ const freq = h("select", {}, FREQS.map(([v, label]) => h("option", { value: v, selected: v === sch.kind }, label)));
300
+ const freqBox = h("div", { class: "task-freq" });
301
+ const timeInput = () => {
302
+ const el = h("input", { type: "time", value: sch.time, required: true });
303
+ el.oninput = () => (sch.time = el.value);
304
+ return el;
305
+ };
306
+ const renderFreq = () => {
307
+ sch.kind = freq.value;
308
+ const parts = [];
309
+ if (sch.kind === "once") {
310
+ const at = h("input", { type: "datetime-local", value: toLocalInput(sch.at) });
311
+ at.oninput = () => (sch.at = new Date(at.value).getTime());
312
+ parts.push(at);
313
+ } else if (sch.kind === "daily" || sch.kind === "weekdays") {
314
+ parts.push(timeInput());
315
+ } else if (sch.kind === "weekly") {
316
+ parts.push(
317
+ h(
318
+ "div",
319
+ { class: "day-chips" },
320
+ WEEK_ORDER.map((d) => {
321
+ const b = h("button", { type: "button", class: `day-chip${sch.days.includes(d) ? " on" : ""}` }, WEEK_NAME[d]);
322
+ b.onclick = () => {
323
+ sch.days = sch.days.includes(d) ? sch.days.filter((x) => x !== d) : [...sch.days, d];
324
+ b.classList.toggle("on", sch.days.includes(d));
325
+ };
326
+ return b;
327
+ }),
328
+ ),
329
+ timeInput(),
330
+ );
331
+ } else if (sch.kind === "monthly") {
332
+ const day = h("input", { type: "number", min: "1", max: "31", value: String(sch.day), class: "narrow" });
333
+ day.oninput = () => (sch.day = Number(day.value));
334
+ parts.push(h("span", { class: "freq-word" }, "每月"), day, h("span", { class: "freq-word" }, "日"), timeInput());
335
+ } else if (sch.kind === "hourly") {
336
+ const every = h("select", { class: "narrow" }, [1, 2, 3, 4, 6, 8, 12].map((n) => h("option", { value: String(n), selected: n === sch.every }, String(n))));
337
+ every.onchange = () => (sch.every = Number(every.value));
338
+ const minute = h("input", { type: "number", min: "0", max: "59", value: String(sch.minute), class: "narrow" });
339
+ minute.oninput = () => (sch.minute = Number(minute.value));
340
+ parts.push(h("span", { class: "freq-word" }, "每"), every, h("span", { class: "freq-word" }, "小时,第"), minute, h("span", { class: "freq-word" }, "分"));
341
+ } else {
342
+ const expr = h("input", { value: sch.expr, placeholder: "分 时 日 月 周", spellcheck: "false" });
343
+ expr.oninput = () => (sch.expr = expr.value);
344
+ parts.push(expr);
345
+ }
346
+ fill(freqBox, parts);
347
+ };
348
+ freq.onchange = renderFreq;
349
+ renderFreq();
350
+ const freqHint = `按服务端时区(${T.tz || "本地时间"})。两次运行至少隔 15 分钟。`;
351
+
352
+ // ---- 提醒 ----
353
+ const notify = h(
354
+ "select",
355
+ {},
356
+ h("option", { value: "smart", selected: (src.notify ?? "smart") === "smart" }, "有需要才提醒(agent 判断,没事不打扰)"),
357
+ h("option", { value: "always", selected: src.notify === "always" }, "每次都提醒"),
358
+ );
359
+
360
+ // ---- 结果发到哪 ----
361
+ const convTarget = task?.target.kind === "conv";
362
+ const target = convTarget
363
+ ? h(
364
+ "select",
365
+ {},
366
+ h("option", { value: "conv", selected: true }, `原来的对话「${task.convTitle || "对话"}」`),
367
+ h("option", { value: "dedicated" }, "专用对话(单独开一条)"),
368
+ )
369
+ : null;
370
+ const agent = h(
371
+ "select",
372
+ {},
373
+ agents.map((a) => h("option", { value: a.id, selected: a.id === (src.agent ?? S.draftAgent) }, a.label)),
374
+ );
375
+ const mode = h(
376
+ "select",
377
+ {},
378
+ h("option", { value: "chat", selected: (src.mode ?? "chat") === "chat" }, "聊天模式:只能联网搜索,开销小,不需要授权"),
379
+ h("option", { value: "work", selected: src.mode === "work" }, "工作模式:能执行命令、读写本机文件"),
380
+ );
381
+ let cwd = src.cwd || "";
382
+ const cwdBtn = h("button", { type: "button", class: "btn task-cwd" });
383
+ const showCwd = () => {
384
+ cwdBtn.textContent = cwd ? baseName(cwd) : `${baseName(S.workspaceRoot) || "工作区"}(工作区根目录)`;
385
+ cwdBtn.title = cwd || S.workspaceRoot || "";
386
+ };
387
+ showCwd();
388
+ cwdBtn.onclick = () =>
389
+ changeCwd({
390
+ current: cwd || S.workspaceRoot || "",
391
+ onPick: (p) => {
392
+ cwd = p === S.workspaceRoot ? "" : p;
393
+ showCwd();
394
+ },
395
+ });
396
+ const cwdField = field("工作目录", cwdBtn, "工作模式下 agent 在这个目录里干活。需要授权时会推送给你,没人处理就按拒绝。");
397
+ const dedicatedBox = h(
398
+ "div",
399
+ { class: "task-dedicated" },
400
+ agents.length > 1 ? field("Agent", agent) : null,
401
+ field("模式", mode),
402
+ cwdField,
403
+ );
404
+ const syncVisibility = () => {
405
+ const dedicated = !target || target.value === "dedicated";
406
+ dedicatedBox.hidden = !dedicated;
407
+ cwdField.hidden = mode.value !== "work";
408
+ };
409
+ mode.onchange = syncVisibility;
410
+ if (target) target.onchange = syncVisibility;
411
+ syncVisibility();
412
+
413
+ const body = h(
414
+ "div",
415
+ { class: "modal-body task-form" },
416
+ field("名称", title),
417
+ field("要做的事", h("div", { class: "mention-wrap" }, prompt, mentionMenu), "需要对比的写清楚怎么判断,比如「和上次的结果比」「版本号比当前的新才算更新」。输入 @ 可以指定这次要用的技能。"),
418
+ field("频率", h("div", { class: "task-freq-row" }, freq, freqBox), freqHint),
419
+ field("提醒", notify),
420
+ target ? field("结果发到", target) : field("结果发到", h("div", { class: "task-static" }, task?.convKey ? `专用对话「${task.convTitle || "对话"}」` : "专用对话(第一次运行时新建,之后都发到这条)")),
421
+ dedicatedBox,
422
+ );
423
+ const save = h("button", { class: "btn primary" }, task ? "保存" : "创建");
424
+ const close = modal({ title: task ? "编辑定时任务" : "新建定时任务", body, foot: h("div", { class: "modal-foot" }, save) });
425
+ (task ? title : prompt).focus();
426
+
427
+ save.onclick = async () => {
428
+ if (!prompt.value.trim()) return toast("写一下要让 agent 做什么");
429
+ const schedule =
430
+ sch.kind === "once"
431
+ ? { kind: "once", at: sch.at }
432
+ : sch.kind === "daily" || sch.kind === "weekdays"
433
+ ? { kind: sch.kind, time: sch.time }
434
+ : sch.kind === "weekly"
435
+ ? { kind: "weekly", days: sch.days, time: sch.time }
436
+ : sch.kind === "monthly"
437
+ ? { kind: "monthly", day: sch.day, time: sch.time }
438
+ : sch.kind === "hourly"
439
+ ? { kind: "hourly", every: sch.every, minute: sch.minute }
440
+ : { kind: "cron", expr: sch.expr };
441
+ if (schedule.kind === "once" && !(schedule.at > Date.now())) return toast("运行时间要在现在之后");
442
+ const dedicated = !target || target.value === "dedicated";
443
+ const payload = {
444
+ title: title.value,
445
+ prompt: prompt.value,
446
+ schedule,
447
+ notify: notify.value,
448
+ target: dedicated ? { kind: "dedicated" } : { kind: "conv", key: task.target.key },
449
+ ...(dedicated ? { agent: agent.value || agents[0]?.id, mode: mode.value, cwd: mode.value === "work" ? cwd : "" } : {}),
450
+ };
451
+ save.disabled = true;
452
+ try {
453
+ if (task) await api(`/api/tasks/${encodeURIComponent(task.id)}`, { method: "PATCH", body: payload });
454
+ else await api("/api/tasks", { method: "POST", body: payload });
455
+ close();
456
+ toast(task ? "已保存" : "已创建");
457
+ if (S.view === "tasks") void reload();
458
+ else void openTasks();
459
+ } catch (err) {
460
+ save.disabled = false;
461
+ toast(err.message);
462
+ }
463
+ };
464
+ }
465
+
466
+ /** 不用 <label>:里面有按钮(星期、选目录),点标题文字会把按钮也点了 */
467
+ function field(label, input, hint) {
468
+ return h("div", { class: "field" }, h("span", {}, label), input, hint ? h("small", {}, hint) : null);
469
+ }
@@ -12,11 +12,17 @@ import { changeCwd, deleteConv, doAction, forkConversation, openModelPicker, ren
12
12
 
13
13
  export function renderTopbar() {
14
14
  const c = S.current ? S.convs.get(S.current) : null;
15
+ // 定时任务页、技能页:只有标题,模式切换、「更多」都是对话用的
16
+ const tasks = S.view === "tasks" || S.view === "skills";
15
17
  fill(ui.topbar,
16
18
  h("button", { class: "icon-btn menu-btn", onclick: openSide, title: "会话列表" }, icon("menu")),
17
- h("div", { class: "top-title" }, c ? h("span", { class: "conv-name", title: convTitle(c) }, convTitle(c)) : null),
18
- ui.modeSlot,
19
- h("button", { class: "icon-btn top-more", title: "更多", onclick: (e) => openTopMore(e.currentTarget) }, icon("more")),
19
+ h(
20
+ "div",
21
+ { class: "top-title" },
22
+ tasks ? h("span", { class: "conv-name" }, S.view === "skills" ? "技能" : "定时任务") : c ? h("span", { class: "conv-name", title: convTitle(c) }, convTitle(c)) : null,
23
+ ),
24
+ tasks ? null : ui.modeSlot,
25
+ tasks ? null : h("button", { class: "icon-btn top-more", title: "更多", onclick: (e) => openTopMore(e.currentTarget) }, icon("more")),
20
26
  );
21
27
  renderContext();
22
28
  }