@co0ontty/wand 4.89.5 → 4.90.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/README.md +5 -3
- package/dist/agent-dispatch.js +4 -9
- package/dist/ai-team-employee-binding.js +10 -9
- package/dist/ai-team-runner.d.ts +2 -1
- package/dist/ai-team-runner.js +38 -2
- package/dist/ai-team-types.js +3 -2
- package/dist/ai-text-runner.d.ts +61 -0
- package/dist/ai-text-runner.js +653 -0
- package/dist/avatar-input.d.ts +2 -0
- package/dist/avatar-input.js +19 -0
- package/dist/bounded-process-output.d.ts +5 -0
- package/dist/bounded-process-output.js +9 -0
- package/dist/build-info.json +3 -3
- package/dist/command-error.d.ts +6 -0
- package/dist/command-error.js +8 -0
- package/dist/config.js +11 -42
- package/dist/conversation-service.d.ts +12 -2
- package/dist/conversation-service.js +104 -25
- package/dist/conversation-session-preview.d.ts +2 -0
- package/dist/conversation-session-preview.js +4 -0
- package/dist/decision-client.js +22 -6
- package/dist/decision-expert-employee.js +2 -2
- package/dist/decision-expert-identity.d.ts +1 -1
- package/dist/decision-expert-identity.js +1 -1
- package/dist/decision-expert-service.d.ts +1 -0
- package/dist/decision-expert-service.js +82 -76
- package/dist/decision-hardware.js +4 -3
- package/dist/decision-types.d.ts +4 -0
- package/dist/decision-types.js +4 -0
- package/dist/deployment-lock.d.ts +13 -0
- package/dist/deployment-lock.js +189 -0
- package/dist/employee-text.js +1 -1
- package/dist/fixed-employee-avatar.d.ts +7 -0
- package/dist/fixed-employee-avatar.js +13 -0
- package/dist/git-quick-commit.d.ts +5 -40
- package/dist/git-quick-commit.js +8 -636
- package/dist/hr-employee-identity.d.ts +5 -0
- package/dist/hr-employee-identity.js +5 -0
- package/dist/hr-employee.d.ts +5 -0
- package/dist/hr-employee.js +21 -0
- package/dist/middleware/path-safety.js +1 -1
- package/dist/milestone-scope.js +1 -1
- package/dist/model-auto-assign.d.ts +1 -1
- package/dist/model-auto-assign.js +1 -1
- package/dist/models.d.ts +4 -0
- package/dist/models.js +21 -1
- package/dist/npm-update-utils.js +5 -44
- package/dist/pi-auto-resources.js +6 -3
- package/dist/plush-avatar.d.ts +10 -1
- package/dist/plush-avatar.js +13 -1
- package/dist/plush-cat-appearance.d.ts +97 -0
- package/dist/plush-cat-appearance.js +118 -0
- package/dist/plush-cat-whiskers.d.ts +6 -0
- package/dist/plush-cat-whiskers.js +42 -0
- package/dist/process-manager.d.ts +22 -21
- package/dist/process-manager.js +49 -83
- package/dist/prompt-optimizer.d.ts +1 -1
- package/dist/prompt-optimizer.js +1 -1
- package/dist/render-host.js +1 -19
- package/dist/scheduled-task-service.d.ts +41 -0
- package/dist/scheduled-task-service.js +294 -0
- package/dist/scheduled-task-state.d.ts +11 -0
- package/dist/scheduled-task-state.js +65 -0
- package/dist/scheduled-task-time.d.ts +4 -0
- package/dist/scheduled-task-time.js +82 -0
- package/dist/scheduled-task-types.d.ts +71 -0
- package/dist/scheduled-task-types.js +1 -0
- package/dist/server-ai-team-routes.js +3 -17
- package/dist/server-employee-routes.d.ts +3 -0
- package/dist/server-employee-routes.js +20 -38
- package/dist/server-file-routes.js +44 -1
- package/dist/server-pi-recommendation-routes.js +13 -4
- package/dist/server-request.d.ts +2 -0
- package/dist/server-request.js +9 -0
- package/dist/server-resume-routes.js +1 -1
- package/dist/server-scheduled-task-routes.d.ts +4 -0
- package/dist/server-scheduled-task-routes.js +34 -0
- package/dist/server-session-routes.d.ts +5 -13
- package/dist/server-session-routes.js +35 -70
- package/dist/server-task-routes.d.ts +6 -21
- package/dist/server-task-routes.js +25 -120
- package/dist/server-team-dispatch-routes.js +12 -2
- package/dist/server-workspace-routes.d.ts +1 -1
- package/dist/server-workspace-routes.js +33 -78
- package/dist/server.js +55 -20
- package/dist/session-model.d.ts +15 -0
- package/dist/session-model.js +23 -0
- package/dist/session-registry.d.ts +5 -1
- package/dist/session-registry.js +20 -11
- package/dist/session-request.d.ts +9 -0
- package/dist/session-request.js +27 -0
- package/dist/session-topic.d.ts +1 -1
- package/dist/session-topic.js +1 -1
- package/dist/session-transport.d.ts +3 -1
- package/dist/session-transport.js +11 -5
- package/dist/silicon-employee-draft.d.ts +2 -2
- package/dist/silicon-employee-draft.js +3 -3
- package/dist/socket-probe.d.ts +2 -0
- package/dist/socket-probe.js +20 -0
- package/dist/storage-schedules.d.ts +20 -0
- package/dist/storage-schedules.js +78 -0
- package/dist/storage-schema.d.ts +3 -0
- package/dist/storage-schema.js +702 -0
- package/dist/storage.d.ts +42 -8
- package/dist/storage.js +182 -715
- package/dist/structured-claude-adapter.js +12 -0
- package/dist/structured-claude-protocol.d.ts +4 -0
- package/dist/structured-claude-protocol.js +18 -4
- package/dist/structured-codex-adapter.js +11 -0
- package/dist/structured-exec-pump.js +79 -38
- package/dist/structured-failure.d.ts +21 -0
- package/dist/structured-failure.js +14 -0
- package/dist/structured-gemini-adapter.js +12 -2
- package/dist/structured-grok-adapter.js +12 -2
- package/dist/structured-opencode-adapter.js +8 -1
- package/dist/structured-qoder-adapter.js +14 -0
- package/dist/structured-session-manager.d.ts +13 -18
- package/dist/structured-session-manager.js +147 -517
- package/dist/system-employee.d.ts +1 -1
- package/dist/system-employee.js +1 -1
- package/dist/task-archive-policy.js +1 -1
- package/dist/task-read-model.d.ts +27 -0
- package/dist/task-read-model.js +47 -0
- package/dist/task-retention.d.ts +1 -1
- package/dist/task-retention.js +1 -1
- package/dist/task-title-service.d.ts +30 -0
- package/dist/task-title-service.js +122 -0
- package/dist/task-title.d.ts +1 -1
- package/dist/task-title.js +1 -1
- package/dist/terminal-daemon-client.js +1 -18
- package/dist/user-memory.js +1 -1
- package/dist/web-ui/asset-runtime.d.ts +12 -0
- package/dist/web-ui/asset-runtime.js +6 -0
- package/dist/web-ui/composer-avatar-activity.d.ts +6 -0
- package/dist/web-ui/composer-avatar-activity.js +29 -0
- package/dist/web-ui/content/ai-teams.js +13 -13
- package/dist/web-ui/content/module-manifest.json +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/chat-render-PY6DIXNZ.js +24 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-2YZLCGYN.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-6CFHW3FA.js +111 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-6XQEBIEX.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-DDSO7OX7.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-G3CXNMA2.js +20 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-LGVAPHRS.js +36 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-NURCNMRU.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-PCMFGZKX.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-QRNA4D7E.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-TMRPHNJO.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-WC7RK7LP.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-YOXXNXOX.js +2 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-YX475LXO.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/employee-card-MDWKU4V3.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-56WPSON2.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-5QOHKHNC.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-AWIJ5L2K.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-BOZLGIT5.js +69 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-EZV22Q7Y.js +4 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-IW43Z5ZF.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-JT7NIUQL.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-JWEIDB55.js +3 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-OU5TRYH6.js +2 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-PRSTCPVE.js +5 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-TWPJI3HE.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-WZGJSNML.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-X6KQVFD6.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-YKCQFMQR.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-bridge-2LZX6UUZ.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/library-bridge-MQFJGTGY.js +4 -0
- package/dist/web-ui/content/modules/5478be410b112154/page-T3VUCU2W.js +3 -0
- package/dist/web-ui/content/modules/5478be410b112154/panel-HJBGBBPY.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/runtime.js +920 -0
- package/dist/web-ui/content/modules/5478be410b112154/sha256-ANXV72UC.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/task-board-host-AVMDIHIV.js +2 -0
- package/dist/web-ui/content/modules/5478be410b112154/workspace-agent-picker-KI7QVSK3.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/workspace-window-L542FGP2.js +1 -0
- package/dist/web-ui/content/plush-avatar.js +177 -177
- package/dist/web-ui/content/scripts.js +1 -994
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +32 -0
- package/dist/web-ui/embedded-assets.js +10 -8
- package/dist/web-ui/markdown.d.ts +3 -2
- package/dist/web-ui/markdown.js +61 -48
- package/dist/web-ui/message-file-target.d.ts +13 -0
- package/dist/web-ui/message-file-target.js +85 -0
- package/dist/web-ui/provider-identity.d.ts +1 -1
- package/dist/web-ui/provider-identity.js +3 -4
- package/dist/web-ui/react/local-preview/controller.d.ts +2 -0
- package/dist/web-ui/react/local-preview/controller.js +12 -0
- package/dist/web-ui/scripts.d.ts +10 -0
- package/dist/web-ui/scripts.js +42 -5
- package/package.json +6 -3
- package/browser-extension/icons/wand-128.png +0 -0
- package/browser-extension/icons/wand-16.png +0 -0
- package/browser-extension/icons/wand-32.png +0 -0
- package/browser-extension/icons/wand-48.png +0 -0
- package/dist/web-ui/content/licenses/codex-LICENSE.txt +0 -21
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import { reconcileScheduledRun, scheduledExecutionStatus } from "./scheduled-task-state.js";
|
|
4
|
+
import { requireTeamEmployee } from "./ai-team-employee-binding.js";
|
|
5
|
+
import { employeeConversationId } from "./conversation-types.js";
|
|
6
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
7
|
+
import { nextScheduledTime, parseTaskSchedule } from "./scheduled-task-time.js";
|
|
8
|
+
import { bodyObject } from "./server-request.js";
|
|
9
|
+
import { resolveSessionCwd } from "./session-cwd.js";
|
|
10
|
+
import { selectEmployeeCandidate } from "./silicon-employee-dispatch.js";
|
|
11
|
+
import { provisionalTaskTitleFromDescription } from "./task-title.js";
|
|
12
|
+
import { isGlobalWorkspace } from "./workspace-binding.js";
|
|
13
|
+
export class ScheduledTaskConflictError extends Error {
|
|
14
|
+
}
|
|
15
|
+
export class ScheduledTaskNotFoundError extends Error {
|
|
16
|
+
}
|
|
17
|
+
function requiredText(value, label, max, preserve = false) {
|
|
18
|
+
if (typeof value !== "string" || !value.trim() || value.length > max) {
|
|
19
|
+
throw new Error(`${label}不能为空且不能超过 ${max} 个字符。`);
|
|
20
|
+
}
|
|
21
|
+
return preserve ? value : value.trim();
|
|
22
|
+
}
|
|
23
|
+
function titleOf(value, prompt) {
|
|
24
|
+
if (value !== undefined && typeof value !== "string")
|
|
25
|
+
throw new Error("任务名称必须是文字。");
|
|
26
|
+
const title = value?.trim() || provisionalTaskTitleFromDescription(prompt) || "定时任务";
|
|
27
|
+
if (title.length > 200)
|
|
28
|
+
throw new Error("任务名称不能超过 200 个字符。");
|
|
29
|
+
return title;
|
|
30
|
+
}
|
|
31
|
+
function digest(value) { return createHash("sha256").update(value).digest("hex"); }
|
|
32
|
+
function workspaceIdOf(value) {
|
|
33
|
+
if (value === undefined || value === null)
|
|
34
|
+
return null;
|
|
35
|
+
return requiredText(value, "执行项目", 160);
|
|
36
|
+
}
|
|
37
|
+
/** Scheduling owns durable occurrences; employee execution and retries remain in the normal conversation protocol. */
|
|
38
|
+
export class ScheduledTaskService {
|
|
39
|
+
deps;
|
|
40
|
+
timer = null;
|
|
41
|
+
pendingTick = null;
|
|
42
|
+
disposed = false;
|
|
43
|
+
liveAcceptedRuns = new Set();
|
|
44
|
+
constructor(deps) {
|
|
45
|
+
this.deps = deps;
|
|
46
|
+
}
|
|
47
|
+
now() { return this.deps.now?.() ?? new Date(); }
|
|
48
|
+
requireEmployee(id) {
|
|
49
|
+
selectEmployeeCandidate(requireTeamEmployee(this.deps.storage, id));
|
|
50
|
+
}
|
|
51
|
+
requireWorkspaceCwd(id) {
|
|
52
|
+
const workspace = this.deps.storage.getWorkspace(id);
|
|
53
|
+
if (!workspace || isGlobalWorkspace(workspace))
|
|
54
|
+
throw new Error("执行项目不存在或不可用,请重新选择项目。");
|
|
55
|
+
const cwd = workspace.cwd?.trim();
|
|
56
|
+
if (!cwd || !isAbsolute(cwd))
|
|
57
|
+
throw new Error("执行项目需要有效的绝对工作目录,请先设置项目目录。");
|
|
58
|
+
return resolveSessionCwd(cwd, undefined);
|
|
59
|
+
}
|
|
60
|
+
executionDirectory(task) {
|
|
61
|
+
const workspace = task.workspaceId ? this.deps.storage.getWorkspace(task.workspaceId) : null;
|
|
62
|
+
const raw = task.workspaceId ? workspace?.cwd?.trim() : this.deps.defaultCwd?.()?.trim();
|
|
63
|
+
const cwd = raw && isAbsolute(raw) ? raw : null;
|
|
64
|
+
try {
|
|
65
|
+
return { cwd: task.workspaceId ? this.requireWorkspaceCwd(task.workspaceId)
|
|
66
|
+
: raw ? resolveSessionCwd(raw, undefined) : null, error: null };
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
return { cwd, error: getErrorMessage(error) };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
requireTask(id) {
|
|
73
|
+
const task = this.deps.storage.getScheduledTask(id);
|
|
74
|
+
if (!task)
|
|
75
|
+
throw new ScheduledTaskNotFoundError("定时任务不存在。");
|
|
76
|
+
return task;
|
|
77
|
+
}
|
|
78
|
+
runView(run) {
|
|
79
|
+
const receipt = run.executionFinishedAt ? run.receipt : this.deps.conversations.receipt(run.requestId) ?? run.receipt;
|
|
80
|
+
const sessionId = receipt?.sessionId ?? null;
|
|
81
|
+
const session = sessionId ? this.deps.getSession(sessionId) : null;
|
|
82
|
+
const executionStatus = scheduledExecutionStatus(run, receipt, session);
|
|
83
|
+
return { ...run, receipt, executionStatus, sessionId };
|
|
84
|
+
}
|
|
85
|
+
summary(task) {
|
|
86
|
+
const employee = this.deps.storage.getSiliconEmployee(task.employeeId);
|
|
87
|
+
let employeeUnavailableReason = null;
|
|
88
|
+
try {
|
|
89
|
+
this.requireEmployee(task.employeeId);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
employeeUnavailableReason = getErrorMessage(error);
|
|
93
|
+
}
|
|
94
|
+
const workspace = task.workspaceId ? this.deps.storage.getWorkspace(task.workspaceId) : null;
|
|
95
|
+
const directory = this.executionDirectory(task);
|
|
96
|
+
const lastRun = this.deps.storage.listScheduledTaskRuns(task.id, 1)[0];
|
|
97
|
+
return { ...task, employeeName: employee?.name ?? "员工已不可用", employeeUnavailableReason,
|
|
98
|
+
workspaceName: task.workspaceId ? workspace?.name ?? "项目已不可用" : "服务默认目录",
|
|
99
|
+
workspaceCwd: directory.cwd, workspaceUnavailableReason: directory.error,
|
|
100
|
+
lastRun: lastRun ? this.runView(lastRun) : null };
|
|
101
|
+
}
|
|
102
|
+
list(includeCancelled = false) {
|
|
103
|
+
return this.deps.storage.listScheduledTasks(includeCancelled).map(task => this.summary(task));
|
|
104
|
+
}
|
|
105
|
+
get(id) {
|
|
106
|
+
return { task: this.summary(this.requireTask(id)), runs: this.deps.storage.listScheduledTaskRuns(id).map(run => this.runView(run)) };
|
|
107
|
+
}
|
|
108
|
+
create(value) {
|
|
109
|
+
const input = bodyObject(value);
|
|
110
|
+
if (typeof input.requestId !== "string" || !/^[\w:-]{8,80}$/.test(input.requestId))
|
|
111
|
+
throw new Error("需要有效的 requestId。");
|
|
112
|
+
const employeeId = requiredText(input.employeeId, "执行员工", 160);
|
|
113
|
+
const prompt = requiredText(input.prompt, "执行提示词", 256_000, true);
|
|
114
|
+
const workspaceId = workspaceIdOf(input.workspaceId);
|
|
115
|
+
const schedule = parseTaskSchedule(input.schedule);
|
|
116
|
+
const title = titleOf(input.title, prompt);
|
|
117
|
+
const id = `sched_${digest(input.requestId).slice(0, 32)}`;
|
|
118
|
+
const creationFingerprint = digest(JSON.stringify({ employeeId, prompt, schedule, title,
|
|
119
|
+
...(workspaceId ? { workspaceId } : {}) }));
|
|
120
|
+
const previous = this.deps.storage.getScheduledTask(id);
|
|
121
|
+
if (previous) {
|
|
122
|
+
if (previous.creationFingerprint !== creationFingerprint)
|
|
123
|
+
throw new ScheduledTaskConflictError("requestId 已用于另一项定时任务,请核对记录。");
|
|
124
|
+
return this.summary(previous);
|
|
125
|
+
}
|
|
126
|
+
this.requireEmployee(employeeId);
|
|
127
|
+
if (workspaceId)
|
|
128
|
+
this.requireWorkspaceCwd(workspaceId);
|
|
129
|
+
const now = this.now();
|
|
130
|
+
const nextRunAt = nextScheduledTime(schedule, now);
|
|
131
|
+
if (!nextRunAt)
|
|
132
|
+
throw new Error("单次执行时间必须晚于当前时间。");
|
|
133
|
+
const task = { id, creationFingerprint, title, employeeId, workspaceId, prompt, schedule, enabled: true,
|
|
134
|
+
nextRunAt, revision: 1, createdAt: now.toISOString(), updatedAt: now.toISOString(), cancelledAt: null };
|
|
135
|
+
if (!this.deps.storage.createScheduledTask(task)) {
|
|
136
|
+
const current = this.requireTask(id);
|
|
137
|
+
if (current.creationFingerprint !== creationFingerprint)
|
|
138
|
+
throw new ScheduledTaskConflictError("requestId 已用于另一项定时任务,请核对记录。");
|
|
139
|
+
return this.summary(current);
|
|
140
|
+
}
|
|
141
|
+
return this.summary(task);
|
|
142
|
+
}
|
|
143
|
+
update(id, value) {
|
|
144
|
+
const input = bodyObject(value);
|
|
145
|
+
const current = this.requireTask(id);
|
|
146
|
+
if (!Number.isInteger(input.revision) || Number(input.revision) !== current.revision) {
|
|
147
|
+
throw new ScheduledTaskConflictError("定时任务已更新,请刷新后重试。");
|
|
148
|
+
}
|
|
149
|
+
if (current.cancelledAt)
|
|
150
|
+
throw new ScheduledTaskConflictError("定时任务已取消,历史记录仍保留。");
|
|
151
|
+
if (input.enabled !== undefined && typeof input.enabled !== "boolean")
|
|
152
|
+
throw new Error("启用状态必须是布尔值。");
|
|
153
|
+
if (input.cancelled !== undefined && input.cancelled !== true)
|
|
154
|
+
throw new Error("取消任务必须传 cancelled=true。");
|
|
155
|
+
if (!["title", "employeeId", "workspaceId", "prompt", "schedule", "enabled", "cancelled"].some(key => input[key] !== undefined)) {
|
|
156
|
+
throw new Error("请提供需要更新的定时任务设置。");
|
|
157
|
+
}
|
|
158
|
+
const now = this.now();
|
|
159
|
+
const prompt = input.prompt === undefined ? current.prompt : requiredText(input.prompt, "执行提示词", 256_000, true);
|
|
160
|
+
const employeeId = input.employeeId === undefined ? current.employeeId : requiredText(input.employeeId, "执行员工", 160);
|
|
161
|
+
const workspaceId = input.workspaceId === undefined ? current.workspaceId ?? null : workspaceIdOf(input.workspaceId);
|
|
162
|
+
const schedule = input.schedule === undefined ? current.schedule : parseTaskSchedule(input.schedule);
|
|
163
|
+
const enabled = input.cancelled === true ? false : input.enabled === undefined ? current.enabled : input.enabled;
|
|
164
|
+
let nextRunAt = current.nextRunAt;
|
|
165
|
+
if (input.cancelled === true)
|
|
166
|
+
nextRunAt = null;
|
|
167
|
+
else if (input.schedule !== undefined || (enabled && !current.enabled)) {
|
|
168
|
+
nextRunAt = nextScheduledTime(schedule, now);
|
|
169
|
+
if (!nextRunAt)
|
|
170
|
+
throw new Error("单次执行时间已过去,请设置新的执行时间。");
|
|
171
|
+
}
|
|
172
|
+
if (input.employeeId !== undefined || (enabled && !current.enabled))
|
|
173
|
+
this.requireEmployee(employeeId);
|
|
174
|
+
if (workspaceId && input.cancelled !== true && input.enabled !== false)
|
|
175
|
+
this.requireWorkspaceCwd(workspaceId);
|
|
176
|
+
const updated = { ...current, title: input.title === undefined ? current.title : titleOf(input.title, prompt),
|
|
177
|
+
prompt, employeeId, workspaceId, schedule, enabled, nextRunAt, revision: current.revision + 1,
|
|
178
|
+
updatedAt: now.toISOString(), cancelledAt: input.cancelled === true ? now.toISOString() : null };
|
|
179
|
+
if (!this.deps.storage.updateScheduledTask(updated, current.revision))
|
|
180
|
+
throw new ScheduledTaskConflictError("定时任务已更新,请刷新后重试。");
|
|
181
|
+
return this.summary(updated);
|
|
182
|
+
}
|
|
183
|
+
reconcile(run, now, recoverInactive = true) {
|
|
184
|
+
if (run.executionFinishedAt) {
|
|
185
|
+
this.liveAcceptedRuns.delete(run.id);
|
|
186
|
+
return run;
|
|
187
|
+
}
|
|
188
|
+
const receipt = this.deps.conversations.receipt(run.requestId) ?? run.receipt;
|
|
189
|
+
const session = receipt?.sessionId ? this.deps.getSession(receipt.sessionId) : null;
|
|
190
|
+
const updated = reconcileScheduledRun(run, receipt, session, now, {
|
|
191
|
+
recoverInactive, liveAccepted: this.liveAcceptedRuns.has(run.id),
|
|
192
|
+
});
|
|
193
|
+
if (updated.executionFinishedAt)
|
|
194
|
+
this.liveAcceptedRuns.delete(run.id);
|
|
195
|
+
if (updated.state !== run.state || updated.error !== run.error || updated.executionOutcome !== run.executionOutcome
|
|
196
|
+
|| updated.executionFinishedAt !== run.executionFinishedAt || JSON.stringify(updated.receipt) !== JSON.stringify(run.receipt)) {
|
|
197
|
+
this.deps.storage.saveScheduledTaskRun(updated);
|
|
198
|
+
}
|
|
199
|
+
return updated;
|
|
200
|
+
}
|
|
201
|
+
/** Capture the original turn's terminal fact before a user can start another turn in the same session. */
|
|
202
|
+
ingestSessionEvent(event) {
|
|
203
|
+
if (this.disposed || (event.type !== "status" && event.type !== "ended"))
|
|
204
|
+
return;
|
|
205
|
+
const runs = this.deps.storage.getOpenScheduledTaskRunsForSession(event.sessionId);
|
|
206
|
+
if (!runs.length)
|
|
207
|
+
return;
|
|
208
|
+
const now = this.now().toISOString();
|
|
209
|
+
for (const run of runs)
|
|
210
|
+
this.reconcile(run, now, false);
|
|
211
|
+
}
|
|
212
|
+
tick() {
|
|
213
|
+
if (this.disposed)
|
|
214
|
+
return Promise.resolve();
|
|
215
|
+
if (this.pendingTick)
|
|
216
|
+
return this.pendingTick;
|
|
217
|
+
const pending = this.runTick();
|
|
218
|
+
this.pendingTick = pending;
|
|
219
|
+
void pending.finally(() => { if (this.pendingTick === pending)
|
|
220
|
+
this.pendingTick = null; }).catch(() => { });
|
|
221
|
+
return pending;
|
|
222
|
+
}
|
|
223
|
+
async runTick() {
|
|
224
|
+
const now = this.now();
|
|
225
|
+
const nowIso = now.toISOString();
|
|
226
|
+
const openRuns = this.deps.storage.listOpenScheduledTaskRuns().map(run => this.reconcile(run, nowIso));
|
|
227
|
+
for (const task of this.deps.storage.listDueScheduledTasks(nowIso)) {
|
|
228
|
+
if (this.disposed)
|
|
229
|
+
return;
|
|
230
|
+
if (!task.enabled || task.cancelledAt || !task.nextRunAt || Date.parse(task.nextRunAt) > now.getTime())
|
|
231
|
+
continue;
|
|
232
|
+
const overlap = openRuns.some(run => {
|
|
233
|
+
if (run.taskId !== task.id)
|
|
234
|
+
return false;
|
|
235
|
+
const current = this.deps.storage.getScheduledTaskRun(run.id) ?? run;
|
|
236
|
+
if (current.executionFinishedAt)
|
|
237
|
+
return false;
|
|
238
|
+
const view = this.runView(current);
|
|
239
|
+
return view.executionStatus === "running" || (view.executionStatus === "pending" && Boolean(view.sessionId));
|
|
240
|
+
});
|
|
241
|
+
const id = `schedrun_${digest(`${task.id}:${task.nextRunAt}`).slice(0, 32)}`;
|
|
242
|
+
const directory = this.executionDirectory(task);
|
|
243
|
+
let run = { id, taskId: task.id, requestId: `scheduled:${id}`, scheduledAt: task.nextRunAt,
|
|
244
|
+
claimedAt: nowIso, employeeId: task.employeeId, workspaceId: task.workspaceId ?? null, cwd: directory.cwd, prompt: task.prompt,
|
|
245
|
+
state: overlap ? "skipped" : directory.error ? "rejected" : "claimed", receipt: null,
|
|
246
|
+
error: overlap ? "上次执行尚未结束,本次已跳过。" : directory.error,
|
|
247
|
+
executionFinishedAt: overlap || directory.error ? nowIso : null,
|
|
248
|
+
...(directory.error && !overlap ? { executionOutcome: "failed" } : {}) };
|
|
249
|
+
// One atomic claim advances from now, including after downtime: never fan out missed recurring occurrences.
|
|
250
|
+
if (!this.deps.storage.claimScheduledTaskRun(task, run, nextScheduledTime(task.schedule, now)))
|
|
251
|
+
continue;
|
|
252
|
+
if (overlap || directory.error)
|
|
253
|
+
continue;
|
|
254
|
+
try {
|
|
255
|
+
const receipt = await this.deps.conversations.send(employeeConversationId(run.employeeId), run.requestId, run.prompt, null, run.cwd ?? undefined, true, { workspaceId: run.workspaceId ?? undefined, title: task.title, scheduled: true });
|
|
256
|
+
run = { ...run, receipt, state: receipt.state === "accepted" ? "accepted" : receipt.state === "rejected" ? "rejected" : "unknown",
|
|
257
|
+
cwd: run.cwd ?? (receipt.sessionId ? this.deps.getSession(receipt.sessionId)?.cwd ?? null : null),
|
|
258
|
+
error: receipt.error ?? (receipt.state === "pending" ? "本次执行的接收状态无法确认;不会自动重发。" : null),
|
|
259
|
+
executionFinishedAt: receipt.state === "rejected" ? nowIso : null,
|
|
260
|
+
...(receipt.state === "rejected" ? { executionOutcome: "failed" } : {}) };
|
|
261
|
+
if (receipt.state === "accepted")
|
|
262
|
+
this.liveAcceptedRuns.add(run.id);
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
const receipt = this.deps.conversations.receipt(run.requestId);
|
|
266
|
+
run = { ...run, receipt, state: receipt?.state === "accepted" ? "accepted" : receipt?.state === "rejected" ? "rejected" : "unknown",
|
|
267
|
+
error: getErrorMessage(error), executionFinishedAt: receipt?.state === "rejected" || !receipt?.sessionId ? nowIso : null,
|
|
268
|
+
...(receipt?.state === "rejected" || !receipt?.sessionId
|
|
269
|
+
? { executionOutcome: receipt?.state === "rejected" ? "failed" : "unknown" } : {}) };
|
|
270
|
+
if (receipt?.state === "accepted")
|
|
271
|
+
this.liveAcceptedRuns.add(run.id);
|
|
272
|
+
}
|
|
273
|
+
this.deps.storage.saveScheduledTaskRun(run);
|
|
274
|
+
openRuns.push(this.reconcile(run, nowIso));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
start(intervalMs = 15_000) {
|
|
278
|
+
if (this.disposed || this.timer)
|
|
279
|
+
return;
|
|
280
|
+
const scan = () => { void this.tick().catch(error => console.error("[ScheduledTasks] 扫描失败:", getErrorMessage(error))); };
|
|
281
|
+
scan();
|
|
282
|
+
this.timer = setInterval(scan, intervalMs);
|
|
283
|
+
this.timer.unref?.();
|
|
284
|
+
}
|
|
285
|
+
async dispose() {
|
|
286
|
+
this.disposed = true;
|
|
287
|
+
if (this.timer) {
|
|
288
|
+
clearInterval(this.timer);
|
|
289
|
+
this.timer = null;
|
|
290
|
+
}
|
|
291
|
+
// Cleanup waits for the scan to settle; a scan failure must not prevent the server from closing its resources.
|
|
292
|
+
await this.pendingTick?.catch(() => { });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ConversationReceipt } from "./conversation-types.js";
|
|
2
|
+
import type { ScheduledTaskRun, ScheduledTaskRunView } from "./scheduled-task-types.js";
|
|
3
|
+
import type { SessionSnapshot } from "./types.js";
|
|
4
|
+
export declare function sessionActive(session: SessionSnapshot): boolean;
|
|
5
|
+
/** Pure read projection; it never settles or replays a scheduled occurrence. */
|
|
6
|
+
export declare function scheduledExecutionStatus(run: ScheduledTaskRun, receipt: ConversationReceipt | null, session: SessionSnapshot | null): ScheduledTaskRunView["executionStatus"];
|
|
7
|
+
/** Settles only observed facts; missing/unknown acceptance never authorizes replay. */
|
|
8
|
+
export declare function reconcileScheduledRun(run: ScheduledTaskRun, receipt: ConversationReceipt | null, session: SessionSnapshot | null, now: string, options: {
|
|
9
|
+
recoverInactive: boolean;
|
|
10
|
+
liveAccepted: boolean;
|
|
11
|
+
}): ScheduledTaskRun;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export function sessionActive(session) {
|
|
2
|
+
return session.status === "running" || Boolean(session.structuredState?.inFlight)
|
|
3
|
+
|| Boolean(session.pendingEscalation || session.permissionBlocked) || (session.queuedMessages?.length ?? 0) > 0;
|
|
4
|
+
}
|
|
5
|
+
/** Pure read projection; it never settles or replays a scheduled occurrence. */
|
|
6
|
+
export function scheduledExecutionStatus(run, receipt, session) {
|
|
7
|
+
let executionStatus;
|
|
8
|
+
if (run.state === "skipped")
|
|
9
|
+
executionStatus = "skipped";
|
|
10
|
+
else if (run.executionOutcome)
|
|
11
|
+
executionStatus = run.executionOutcome;
|
|
12
|
+
// Legacy settled records lack an immutable outcome; a later turn cannot establish their original result.
|
|
13
|
+
else if (run.executionFinishedAt)
|
|
14
|
+
executionStatus = run.state === "rejected" || receipt?.startup === "failed" ? "failed" : "unknown";
|
|
15
|
+
else if (receipt?.state === "rejected" || run.state === "rejected")
|
|
16
|
+
executionStatus = "failed";
|
|
17
|
+
else if (session && sessionActive(session))
|
|
18
|
+
executionStatus = "running";
|
|
19
|
+
else if (session?.status === "failed" || session?.structuredState?.lastError || receipt?.startup === "failed")
|
|
20
|
+
executionStatus = "failed";
|
|
21
|
+
else if (session?.endedAt)
|
|
22
|
+
executionStatus = session.status === "stopped" ? "failed" : "completed";
|
|
23
|
+
else if (run.state === "unknown" || receipt?.state === "pending" || (receipt?.state === "accepted" && !session))
|
|
24
|
+
executionStatus = "unknown";
|
|
25
|
+
else
|
|
26
|
+
executionStatus = "pending";
|
|
27
|
+
return executionStatus;
|
|
28
|
+
}
|
|
29
|
+
/** Settles only observed facts; missing/unknown acceptance never authorizes replay. */
|
|
30
|
+
export function reconcileScheduledRun(run, receipt, session, now, options) {
|
|
31
|
+
if (run.executionFinishedAt)
|
|
32
|
+
return run;
|
|
33
|
+
let updated = { ...run, receipt };
|
|
34
|
+
if (run.state === "claimed") {
|
|
35
|
+
updated = { ...updated, state: receipt?.state === "accepted" ? "accepted" : receipt?.state === "rejected" ? "rejected" : "unknown",
|
|
36
|
+
error: receipt?.error ?? (receipt?.state === "accepted" ? null : "本次执行的接收状态无法确认,请核对执行记录;不会自动重发。") };
|
|
37
|
+
}
|
|
38
|
+
if (session && session.endedAt && session.status !== "running" && !session.structuredState?.inFlight) {
|
|
39
|
+
// A user may already have queued a follow-up. This ended turn still settles the scheduled occurrence.
|
|
40
|
+
const failed = session.status === "failed" || session.status === "stopped" || Boolean(session.structuredState?.lastError)
|
|
41
|
+
|| receipt?.startup === "failed" || (session.exitCode !== null && session.exitCode !== 0);
|
|
42
|
+
updated.executionOutcome = failed ? "failed" : "completed";
|
|
43
|
+
updated.executionFinishedAt = session.endedAt;
|
|
44
|
+
if (failed)
|
|
45
|
+
updated.error = receipt?.error ?? session.structuredState?.lastError ?? updated.error;
|
|
46
|
+
}
|
|
47
|
+
else if (session && !sessionActive(session) && (session.status === "failed" || session.status === "stopped"
|
|
48
|
+
|| session.structuredState?.lastError)) {
|
|
49
|
+
updated.error = receipt?.error ?? session.structuredState?.lastError ?? updated.error;
|
|
50
|
+
updated.executionOutcome = "failed";
|
|
51
|
+
updated.executionFinishedAt = now;
|
|
52
|
+
}
|
|
53
|
+
else if (updated.state === "rejected" || (!session && (updated.state === "unknown" || receipt?.state === "accepted"))
|
|
54
|
+
|| (receipt?.startup === "failed" && (!session || !sessionActive(session)))) {
|
|
55
|
+
updated.executionOutcome = updated.state === "rejected" || receipt?.startup === "failed" ? "failed" : "unknown";
|
|
56
|
+
updated.executionFinishedAt = now;
|
|
57
|
+
}
|
|
58
|
+
else if (options.recoverInactive && session && !sessionActive(session) && !session.endedAt && !options.liveAccepted
|
|
59
|
+
&& (updated.state === "accepted" || updated.state === "unknown")) {
|
|
60
|
+
// After restart an initial idle snapshot proves neither dispatch nor completion. Preserve its receipt without replay.
|
|
61
|
+
updated = { ...updated, state: "unknown", executionFinishedAt: now, executionOutcome: "unknown",
|
|
62
|
+
error: updated.error ?? "本次执行尚未确认启动或结束,请核对会话;不会自动重发。" };
|
|
63
|
+
}
|
|
64
|
+
return updated;
|
|
65
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { TaskSchedule } from "./scheduled-task-types.js";
|
|
2
|
+
export declare function parseTaskSchedule(value: unknown): TaskSchedule;
|
|
3
|
+
/** Calendar time in the saved zone. A missing DST time is skipped; a repeated time runs once. */
|
|
4
|
+
export declare function nextScheduledTime(schedule: TaskSchedule, after: Date): string | null;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const formatters = new Map();
|
|
2
|
+
function formatter(timeZone) {
|
|
3
|
+
let value = formatters.get(timeZone);
|
|
4
|
+
if (!value) {
|
|
5
|
+
value = new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit",
|
|
6
|
+
hour: "2-digit", minute: "2-digit", second: "2-digit", hourCycle: "h23" });
|
|
7
|
+
// Validation can see arbitrary HTTP input; keep the cache bounded.
|
|
8
|
+
if (formatters.size >= 100)
|
|
9
|
+
formatters.delete(formatters.keys().next().value);
|
|
10
|
+
formatters.set(timeZone, value);
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
function localParts(timestamp, timeZone) {
|
|
15
|
+
const parts = formatter(timeZone).formatToParts(timestamp);
|
|
16
|
+
return ["year", "month", "day", "hour", "minute", "second"].map(key => Number(parts.find(part => part.type === key).value));
|
|
17
|
+
}
|
|
18
|
+
function asUtc(parts) {
|
|
19
|
+
return Date.UTC(parts[0], parts[1] - 1, parts[2], parts[3] ?? 0, parts[4] ?? 0, parts[5] ?? 0);
|
|
20
|
+
}
|
|
21
|
+
export function parseTaskSchedule(value) {
|
|
22
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
23
|
+
throw new Error("请选择执行计划。");
|
|
24
|
+
const input = value;
|
|
25
|
+
if (input.kind === "once") {
|
|
26
|
+
if (typeof input.runAt !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/.test(input.runAt)
|
|
27
|
+
|| !Number.isFinite(Date.parse(input.runAt))
|
|
28
|
+
|| new Date(`${input.runAt.slice(0, 19)}Z`).toISOString().slice(0, 19) !== input.runAt.slice(0, 19)) {
|
|
29
|
+
throw new Error("请选择有效的执行日期和时间。");
|
|
30
|
+
}
|
|
31
|
+
return { kind: "once", runAt: new Date(input.runAt).toISOString() };
|
|
32
|
+
}
|
|
33
|
+
if (input.kind !== "daily" && input.kind !== "weekly")
|
|
34
|
+
throw new Error("执行计划仅支持单次、每天或每周。");
|
|
35
|
+
if (typeof input.time !== "string" || !/^(?:[01]\d|2[0-3]):[0-5]\d$/.test(input.time))
|
|
36
|
+
throw new Error("执行时间须为 HH:mm。");
|
|
37
|
+
if (typeof input.timeZone !== "string" || input.timeZone.length > 100)
|
|
38
|
+
throw new Error("请选择有效的时区。");
|
|
39
|
+
try {
|
|
40
|
+
formatter(input.timeZone);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new Error("请选择有效的时区。");
|
|
44
|
+
}
|
|
45
|
+
if (input.kind === "weekly") {
|
|
46
|
+
if (!Number.isInteger(input.weekday) || Number(input.weekday) < 0 || Number(input.weekday) > 6)
|
|
47
|
+
throw new Error("请选择星期几执行。");
|
|
48
|
+
return { kind: "weekly", time: input.time, timeZone: input.timeZone, weekday: Number(input.weekday) };
|
|
49
|
+
}
|
|
50
|
+
return { kind: "daily", time: input.time, timeZone: input.timeZone };
|
|
51
|
+
}
|
|
52
|
+
/** Calendar time in the saved zone. A missing DST time is skipped; a repeated time runs once. */
|
|
53
|
+
export function nextScheduledTime(schedule, after) {
|
|
54
|
+
const timestamp = after.getTime();
|
|
55
|
+
if (!Number.isFinite(timestamp))
|
|
56
|
+
throw new Error("当前时间无效。");
|
|
57
|
+
if (schedule.kind === "once")
|
|
58
|
+
return Date.parse(schedule.runAt) > timestamp ? schedule.runAt : null;
|
|
59
|
+
const current = localParts(timestamp, schedule.timeZone);
|
|
60
|
+
const [hour, minute] = schedule.time.split(":").map(Number);
|
|
61
|
+
const today = Date.UTC(current[0], current[1] - 1, current[2]);
|
|
62
|
+
// Two weeks includes the next weekly occurrence after a skipped spring-forward time.
|
|
63
|
+
for (let day = 0; day <= 14; day++) {
|
|
64
|
+
const date = new Date(today + day * 86_400_000);
|
|
65
|
+
if (schedule.kind === "weekly" && date.getUTCDay() !== schedule.weekday)
|
|
66
|
+
continue;
|
|
67
|
+
const desired = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), hour, minute);
|
|
68
|
+
// The offsets surrounding this local date cover transitions, including 30-minute DST.
|
|
69
|
+
const offsets = new Set();
|
|
70
|
+
for (const delta of [-36, -12, 0, 12, 36]) {
|
|
71
|
+
const probe = desired + delta * 3_600_000;
|
|
72
|
+
offsets.add(asUtc(localParts(probe, schedule.timeZone)) - probe);
|
|
73
|
+
}
|
|
74
|
+
const matches = [...offsets].map(offset => desired - offset)
|
|
75
|
+
.filter(candidate => asUtc(localParts(candidate, schedule.timeZone)) === desired).sort((a, b) => a - b);
|
|
76
|
+
// Always choose the first occurrence, even if after is between the two DST copies.
|
|
77
|
+
const candidate = matches[0];
|
|
78
|
+
if (candidate !== undefined && candidate > timestamp)
|
|
79
|
+
return new Date(candidate).toISOString();
|
|
80
|
+
}
|
|
81
|
+
throw new Error("无法计算下次执行时间,请检查计划和时区。");
|
|
82
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ConversationReceipt } from "./conversation-types.js";
|
|
2
|
+
export type TaskSchedule = {
|
|
3
|
+
kind: "once";
|
|
4
|
+
runAt: string;
|
|
5
|
+
} | {
|
|
6
|
+
kind: "daily";
|
|
7
|
+
time: string;
|
|
8
|
+
timeZone: string;
|
|
9
|
+
} | {
|
|
10
|
+
kind: "weekly";
|
|
11
|
+
time: string;
|
|
12
|
+
timeZone: string;
|
|
13
|
+
weekday: number;
|
|
14
|
+
};
|
|
15
|
+
export interface ScheduledTask {
|
|
16
|
+
id: string;
|
|
17
|
+
creationFingerprint: string;
|
|
18
|
+
title: string;
|
|
19
|
+
employeeId: string;
|
|
20
|
+
/** Omitted or null on legacy plans means the service default directory. */
|
|
21
|
+
workspaceId?: string | null;
|
|
22
|
+
prompt: string;
|
|
23
|
+
schedule: TaskSchedule;
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
nextRunAt: string | null;
|
|
26
|
+
revision: number;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
updatedAt: string;
|
|
29
|
+
cancelledAt: string | null;
|
|
30
|
+
}
|
|
31
|
+
export interface ScheduledTaskInput {
|
|
32
|
+
requestId: string;
|
|
33
|
+
title?: string;
|
|
34
|
+
employeeId: string;
|
|
35
|
+
workspaceId?: string | null;
|
|
36
|
+
prompt: string;
|
|
37
|
+
schedule: TaskSchedule;
|
|
38
|
+
}
|
|
39
|
+
export interface ScheduledTaskRun {
|
|
40
|
+
id: string;
|
|
41
|
+
taskId: string;
|
|
42
|
+
requestId: string;
|
|
43
|
+
scheduledAt: string;
|
|
44
|
+
claimedAt: string;
|
|
45
|
+
executionFinishedAt: string | null;
|
|
46
|
+
executionOutcome?: "completed" | "failed" | "unknown";
|
|
47
|
+
employeeId: string;
|
|
48
|
+
/** Immutable execution target, captured for this occurrence before dispatch. */
|
|
49
|
+
workspaceId?: string | null;
|
|
50
|
+
cwd?: string | null;
|
|
51
|
+
prompt: string;
|
|
52
|
+
state: "claimed" | "accepted" | "rejected" | "unknown" | "skipped";
|
|
53
|
+
receipt: ConversationReceipt | null;
|
|
54
|
+
error: string | null;
|
|
55
|
+
}
|
|
56
|
+
export interface ScheduledTaskRunView extends ScheduledTaskRun {
|
|
57
|
+
executionStatus: "pending" | "running" | "completed" | "failed" | "unknown" | "skipped";
|
|
58
|
+
sessionId: string | null;
|
|
59
|
+
}
|
|
60
|
+
export interface ScheduledTaskSummary extends ScheduledTask {
|
|
61
|
+
employeeName: string;
|
|
62
|
+
employeeUnavailableReason: string | null;
|
|
63
|
+
workspaceName: string;
|
|
64
|
+
workspaceCwd: string | null;
|
|
65
|
+
workspaceUnavailableReason: string | null;
|
|
66
|
+
lastRun: ScheduledTaskRunView | null;
|
|
67
|
+
}
|
|
68
|
+
export interface ScheduledTaskDetail {
|
|
69
|
+
task: ScheduledTaskSummary;
|
|
70
|
+
runs: ScheduledTaskRunView[];
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -5,7 +5,8 @@ import { requireTeamEmployee } from "./ai-team-employee-binding.js";
|
|
|
5
5
|
import { asyncRoute } from "./express-async.js";
|
|
6
6
|
import { getErrorMessage } from "./error-utils.js";
|
|
7
7
|
import { defaultMilestoneIdForWrite, scopedMilestoneId } from "./milestone-scope.js";
|
|
8
|
-
import {
|
|
8
|
+
import { parseStoredAvatar } from "./avatar-input.js";
|
|
9
|
+
import { bodyObject, boundedText, sendRouteError, text } from "./server-request.js";
|
|
9
10
|
import { parseTaskAgent } from "./server-task-routes.js";
|
|
10
11
|
import { provisionalTaskTitleFromDescription } from "./task-title.js";
|
|
11
12
|
const NOT_FOUND = /不存在/;
|
|
@@ -20,23 +21,8 @@ function sendTeamError(res, error) {
|
|
|
20
21
|
const status = error instanceof AiTeamConflictError ? 409 : NOT_FOUND.test(message) ? 404 : 400;
|
|
21
22
|
sendRouteError(res, error, "团队操作失败。", status);
|
|
22
23
|
}
|
|
23
|
-
function boundedText(value, label, min, max) {
|
|
24
|
-
const result = text(value);
|
|
25
|
-
if (result.length < min)
|
|
26
|
-
throw new Error(`${label}不能为空。`);
|
|
27
|
-
if (result.length > max)
|
|
28
|
-
throw new Error(`${label}不能超过 ${max} 个字符。`);
|
|
29
|
-
return result;
|
|
30
|
-
}
|
|
31
24
|
function parseAvatar(value, name) {
|
|
32
|
-
|
|
33
|
-
if (!avatar || /^cat:\d{1,2}$/.test(avatar))
|
|
34
|
-
return avatar;
|
|
35
|
-
if (!/^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/=]+$/.test(avatar))
|
|
36
|
-
throw new Error(`成员「${name}」的头像格式无效。`);
|
|
37
|
-
if (avatar.length > AI_TEAM_AVATAR_MAX_CHARS)
|
|
38
|
-
throw new Error(`成员「${name}」的头像太大。`);
|
|
39
|
-
return avatar;
|
|
25
|
+
return parseStoredAvatar(value, `成员「${name}」`, AI_TEAM_AVATAR_MAX_CHARS);
|
|
40
26
|
}
|
|
41
27
|
function parseMembers(value, existing, source) {
|
|
42
28
|
if (!Array.isArray(value))
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { Express } from "express";
|
|
2
2
|
import { type SiliconEmployee, type SiliconEmployeeDraft } from "./ai-team-types.js";
|
|
3
3
|
import type { OpenRouterFreeModelsService } from "./openrouter-free-models.js";
|
|
4
|
+
import type { QuickCommitAiOptions } from "./ai-text-runner.js";
|
|
4
5
|
import type { WandStorage } from "./storage.js";
|
|
5
6
|
import type { WandTaskAgent } from "./task-types.js";
|
|
6
7
|
import type { WandConfig } from "./types.js";
|
|
8
|
+
/** Recruitment uses HR’s own ordered candidates and persona, never the ops chain. */
|
|
9
|
+
export declare function employeeDraftAiOptions(storage: WandStorage, config?: WandConfig, free?: Pick<OpenRouterFreeModelsService, "resolveForCall">): QuickCommitAiOptions;
|
|
7
10
|
export interface EmployeeDraftRequest {
|
|
8
11
|
expectation: string;
|
|
9
12
|
existingNames: string[];
|