@baique/pi-web-sky 0.1.35 → 0.1.36
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +16 -16
- package/.next/build-manifest.json +3 -3
- package/.next/react-loadable-manifest.json +1 -1
- package/.next/required-server-files.js +1 -1
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +3 -3
- package/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/agent/[id]/bash-output/route.js +1 -1
- package/.next/server/app/api/agent/[id]/events/route.js +1 -1
- package/.next/server/app/api/agent/[id]/route.js +1 -1
- package/.next/server/app/api/app-update/route.js +1 -1
- package/.next/server/app/api/auth/login/[provider]/route.js +1 -1
- package/.next/server/app/api/cwd/validate/route.js +1 -1
- package/.next/server/app/api/default-cwd/route.js +1 -1
- package/.next/server/app/api/file-index/route.js +1 -1
- package/.next/server/app/api/files/[...path]/route.js +1 -1
- package/.next/server/app/api/mcp/route.js +1 -1
- package/.next/server/app/api/models/route.js +1 -1
- package/.next/server/app/api/plugins/route.js +1 -1
- package/.next/server/app/api/project-trust/route.js +1 -1
- package/.next/server/app/api/search/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/auto-name/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/context/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/state/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/todos/route.js +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/skills/check/route.js +1 -1
- package/.next/server/app/api/skills/check/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/install/route.js +1 -1
- package/.next/server/app/api/skills/route.js +2 -31
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/update/route.js +1 -1
- package/.next/server/app/api/skills/update/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +5 -5
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/index.segments/_full.segment.rsc +5 -5
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/page.js +2 -2
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +16 -16
- package/.next/server/chunks/2404.js +1 -1
- package/.next/server/chunks/3500.js +30 -0
- package/.next/server/chunks/4226.js +2 -2
- package/.next/server/chunks/542.js +10 -5
- package/.next/server/chunks/5794.js +1 -1
- package/.next/server/chunks/6429.js +1 -1
- package/.next/server/chunks/898.js +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/static/chunks/5315.7e72d26a3ad3bd9a.js +4 -0
- package/.next/static/chunks/app/{layout-1d9a7a44fbc92d2e.js → layout-ebbc0d287267b07b.js} +1 -1
- package/.next/static/chunks/app/{page-6d6779a81a0a7639.js → page-e568623a44c67606.js} +2 -2
- package/.next/static/chunks/{webpack-fa4f40085b1ec1ac.js → webpack-2e4e348867a555cd.js} +1 -1
- package/.next/static/css/422c7dc0faa04d89.css +1 -0
- package/.next/trace +8 -9
- package/.next/trace-build +1 -1
- package/bin/pi-web-sky.js +15 -20
- package/lib/agent-client.test.mjs +64 -0
- package/lib/agent-client.ts +53 -0
- package/lib/agent-event-connection.test.mjs +193 -0
- package/lib/agent-event-connection.ts +202 -0
- package/lib/agent-event-stream.test.mjs +155 -0
- package/lib/agent-event-stream.ts +132 -0
- package/lib/agent-event-wire.test.mjs +278 -0
- package/lib/agent-event-wire.ts +107 -0
- package/lib/agent-phase.ts +24 -0
- package/lib/allowed-roots.ts +31 -0
- package/lib/ansi.test.mjs +52 -0
- package/lib/ansi.ts +211 -0
- package/lib/api-types.ts +139 -0
- package/lib/app-update.test.mjs +27 -0
- package/lib/app-update.ts +28 -0
- package/lib/atomic-file.test.mjs +37 -0
- package/lib/atomic-file.ts +34 -0
- package/lib/audit-session.ts +235 -0
- package/lib/bash-output.test.mjs +63 -0
- package/lib/bash-output.ts +53 -0
- package/lib/bg-image.ts +129 -0
- package/lib/board-events.ts +83 -0
- package/lib/board-purge.test.mjs +66 -0
- package/lib/board-purge.ts +65 -0
- package/lib/board-search.test.mjs +110 -0
- package/lib/board-search.ts +63 -0
- package/lib/board-store.test.mjs +542 -0
- package/lib/board-store.ts +794 -0
- package/lib/board-types.ts +96 -0
- package/lib/board-utils.ts +17 -0
- package/lib/bounded-form-data.test.mjs +54 -0
- package/lib/bounded-form-data.ts +49 -0
- package/lib/browser-notifications.test.mjs +167 -0
- package/lib/browser-notifications.ts +104 -0
- package/lib/chat-lazy-load.test.mjs +140 -0
- package/lib/chat-lazy-load.ts +64 -0
- package/lib/clipboard-paths.test.mjs +140 -0
- package/lib/clipboard-paths.ts +192 -0
- package/lib/clipboard.ts +18 -0
- package/lib/compaction-summary.test.mjs +44 -0
- package/lib/compaction-summary.ts +34 -0
- package/lib/custom-ui-terminal.test.mjs +32 -0
- package/lib/custom-ui-terminal.ts +27 -0
- package/lib/deepseek-pricing.test.mjs +120 -0
- package/lib/deepseek-pricing.ts +71 -0
- package/lib/directory-browser.test.mjs +60 -0
- package/lib/directory-browser.ts +83 -0
- package/lib/draft-stash.test.mjs +91 -0
- package/lib/draft-stash.ts +102 -0
- package/lib/draft-store.ts +105 -0
- package/lib/dropdown-direction.ts +27 -0
- package/lib/extension-command.test.mjs +63 -0
- package/lib/extension-command.ts +81 -0
- package/lib/file-access.test.mjs +31 -0
- package/lib/file-access.ts +59 -0
- package/lib/file-dirent.test.mjs +56 -0
- package/lib/file-dirent.ts +17 -0
- package/lib/file-explorer-state.test.mjs +63 -0
- package/lib/file-explorer-state.ts +36 -0
- package/lib/file-fuzzy.test.mjs +29 -0
- package/lib/file-fuzzy.ts +189 -0
- package/lib/file-links.test.mjs +92 -0
- package/lib/file-links.ts +148 -0
- package/lib/file-paths.ts +43 -0
- package/lib/file-types.test.mjs +34 -0
- package/lib/file-types.ts +73 -0
- package/lib/file-upload.test.mjs +70 -0
- package/lib/file-upload.ts +59 -0
- package/lib/file-viewer-state.test.mjs +20 -0
- package/lib/file-viewer-state.ts +15 -0
- package/lib/frontmatter.test.mjs +55 -0
- package/lib/frontmatter.ts +87 -0
- package/lib/git-changes.test.mjs +52 -0
- package/lib/git-changes.ts +232 -0
- package/lib/git-status.ts +47 -0
- package/lib/git-types.ts +29 -0
- package/lib/http-dispatcher.test.mjs +83 -0
- package/lib/http-dispatcher.ts +86 -0
- package/lib/i18n/format.test.mjs +19 -0
- package/lib/i18n/format.ts +60 -0
- package/lib/i18n/messages/en.ts +662 -0
- package/lib/i18n/messages/zh-CN.ts +661 -0
- package/lib/i18n/registry.test.mjs +32 -0
- package/lib/i18n/registry.ts +43 -0
- package/lib/i18n/types.ts +15 -0
- package/lib/image-attachments.test.mjs +27 -0
- package/lib/image-attachments.ts +56 -0
- package/lib/initial-navigation.test.mjs +65 -0
- package/lib/initial-navigation.ts +15 -0
- package/lib/markdown.test.mjs +104 -0
- package/lib/markdown.ts +347 -0
- package/lib/message-display.test.mjs +130 -0
- package/lib/message-display.ts +47 -0
- package/lib/model-catalog.test.mjs +327 -0
- package/lib/model-catalog.ts +414 -0
- package/lib/model-discovery-auth.ts +58 -0
- package/lib/model-discovery.test.mjs +50 -0
- package/lib/model-discovery.ts +75 -0
- package/lib/model-id-preference.ts +46 -0
- package/lib/model-scope.test.mjs +193 -0
- package/lib/model-scope.ts +198 -0
- package/lib/models-cache.test.mjs +122 -0
- package/lib/models-cache.ts +90 -0
- package/lib/models-config-store.test.mjs +200 -0
- package/lib/models-config-store.ts +84 -0
- package/lib/next-config-esm.test.mjs +14 -0
- package/lib/next-config.test.mjs +13 -0
- package/lib/node-version.test.mjs +33 -0
- package/lib/normalize.test.mjs +54 -0
- package/lib/normalize.ts +63 -0
- package/lib/npx.ts +62 -0
- package/lib/panel-layout.test.mjs +56 -0
- package/lib/panel-layout.ts +52 -0
- package/lib/patch.test.mjs +110 -0
- package/lib/patch.ts +134 -0
- package/lib/path-security.ts +42 -0
- package/lib/paths.test.mjs +58 -0
- package/lib/paths.ts +71 -0
- package/lib/pi-types.ts +183 -0
- package/lib/pi-web-options.test.mjs +63 -0
- package/lib/process-lifecycle.test.mjs +83 -0
- package/lib/project-command-env.test.mjs +298 -0
- package/lib/project-command-env.ts +129 -0
- package/lib/project-groups.test.mjs +60 -0
- package/lib/project-groups.ts +53 -0
- package/lib/project-identity.test.mjs +37 -0
- package/lib/project-identity.ts +26 -0
- package/lib/project-tree.test.mjs +115 -0
- package/lib/project-tree.ts +176 -0
- package/lib/project-trust.test.mjs +117 -0
- package/lib/project-trust.ts +48 -0
- package/lib/prompt-recovery.test.mjs +33 -0
- package/lib/prompt-recovery.ts +47 -0
- package/lib/provider-api-key-route.test.mjs +12 -0
- package/lib/provider-credential-store.test.mjs +126 -0
- package/lib/provider-credential-store.ts +114 -0
- package/lib/provider-listing-runtime.ts +37 -0
- package/lib/provider-listing.test.mjs +114 -0
- package/lib/provider-listing.ts +118 -0
- package/lib/provider-services.ts +64 -0
- package/lib/request-security.test.mjs +256 -0
- package/lib/request-security.ts +136 -0
- package/lib/rpc-manager-shutdown.test.mjs +372 -0
- package/lib/rpc-manager-widgets.test.mjs +342 -0
- package/lib/rpc-manager.test.mjs +139 -0
- package/lib/rpc-manager.ts +1703 -0
- package/lib/rpc-session-info.test.mjs +93 -0
- package/lib/session-delete.test.mjs +53 -0
- package/lib/session-delete.ts +86 -0
- package/lib/session-file-references-core.ts +82 -0
- package/lib/session-file-references.test.mjs +91 -0
- package/lib/session-file-references.ts +29 -0
- package/lib/session-path.test.mjs +24 -0
- package/lib/session-path.ts +11 -0
- package/lib/session-reader.pagination.test.mjs +115 -0
- package/lib/session-reader.test.mjs +446 -0
- package/lib/session-reader.ts +548 -0
- package/lib/session-resolve.test.mjs +65 -0
- package/lib/session-row-context-menu.test.mjs +46 -0
- package/lib/session-row-context-menu.ts +29 -0
- package/lib/session-scanner.test.mjs +151 -0
- package/lib/session-scanner.ts +238 -0
- package/lib/session-search.test.mjs +173 -0
- package/lib/session-search.ts +284 -0
- package/lib/session-stats.ts +26 -0
- package/lib/session-timing.test.mjs +107 -0
- package/lib/session-timing.ts +45 -0
- package/lib/session-title.test.mjs +284 -0
- package/lib/session-title.ts +255 -0
- package/lib/skill-frontmatter.test.mjs +75 -0
- package/lib/skill-frontmatter.ts +40 -0
- package/lib/skill-lock.test.mjs +182 -0
- package/lib/skill-lock.ts +146 -0
- package/lib/skill-updates.test.mjs +182 -0
- package/lib/skill-updates.ts +263 -0
- package/lib/skills-service.ts +16 -0
- package/lib/slash-display.test.mjs +122 -0
- package/lib/slash-display.ts +30 -0
- package/lib/sqlite-db.test.mjs +140 -0
- package/lib/sqlite-db.ts +205 -0
- package/lib/startup-preferences.test.mjs +130 -0
- package/lib/startup-preferences.ts +65 -0
- package/lib/streaming-message.test.mjs +212 -0
- package/lib/streaming-message.ts +151 -0
- package/lib/subagent-widget.test.mjs +87 -0
- package/lib/subagent-widget.ts +109 -0
- package/lib/sync-room-server.mjs +137 -0
- package/lib/task-card-store.test.mjs +309 -0
- package/lib/task-card-store.ts +559 -0
- package/lib/task-scheduler.ts +498 -0
- package/lib/task-store.test.mjs +216 -0
- package/lib/task-store.ts +271 -0
- package/lib/terminal-input.test.mjs +46 -0
- package/lib/terminal-input.ts +63 -0
- package/lib/terminal-manager.test.mjs +71 -0
- package/lib/terminal-manager.ts +245 -0
- package/lib/tool-execution-progress.test.mjs +32 -0
- package/lib/tool-execution-progress.ts +29 -0
- package/lib/tool-names.ts +25 -0
- package/lib/tool-preset-preference.test.mjs +72 -0
- package/lib/tool-preset-preference.ts +41 -0
- package/lib/tool-presets.test.mjs +56 -0
- package/lib/tool-presets.ts +42 -0
- package/lib/turn-written-files.test.mjs +137 -0
- package/lib/turn-written-files.ts +60 -0
- package/lib/types.ts +349 -0
- package/lib/wallpaper-settings.ts +191 -0
- package/lib/web-auth.test.mjs +50 -0
- package/lib/web-auth.ts +45 -0
- package/lib/workspace-memory.test.mjs +129 -0
- package/lib/workspace-memory.ts +98 -0
- package/lib/worktree.test.mjs +50 -0
- package/lib/worktree.ts +250 -0
- package/package.json +3 -1
- package/server.mjs +34 -0
- package/.next/server/chunks/7513.js +0 -30
- package/.next/static/chunks/5315.03de3c6abd8b283e.js +0 -4
- package/.next/static/css/cf25164cb7c04d95.css +0 -1
- /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_buildManifest.js +0 -0
- /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import { getBoard } from "./board-store";
|
|
4
|
+
import { startRpcSession, getRunningRpcSessionIds } from "./rpc-manager";
|
|
5
|
+
import { resolveSessionPath } from "./session-reader";
|
|
6
|
+
import { assignSessionToTask } from "./task-store";
|
|
7
|
+
import { PRESET_FULL } from "./tool-presets";
|
|
8
|
+
import {
|
|
9
|
+
countRunningDispatched,
|
|
10
|
+
listDispatchableCards,
|
|
11
|
+
listRunningDispatched,
|
|
12
|
+
listCardsByExecStatus,
|
|
13
|
+
listAnswerableQuestions,
|
|
14
|
+
createQuestion,
|
|
15
|
+
getCard,
|
|
16
|
+
updateCard,
|
|
17
|
+
acquireDispatchToken,
|
|
18
|
+
clearDispatchToken,
|
|
19
|
+
type TaskCard,
|
|
20
|
+
type TaskCardQuestion,
|
|
21
|
+
} from "./task-card-store";
|
|
22
|
+
import {
|
|
23
|
+
readSessionAuditSnapshot,
|
|
24
|
+
runAuditVerdict,
|
|
25
|
+
runBlockCheck,
|
|
26
|
+
} from "./audit-session";
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// 任务卡调度器(S2 happy path)
|
|
30
|
+
// 定时从看板捞「就绪=待办 & 未开始/失败且未超重试 & 无未完成前置」的卡,
|
|
31
|
+
// 派发专属执行会话(FULL 工具),发任务描述 → execStatus=running。
|
|
32
|
+
// 本轮只做正常流程:失败重试 / 巡检 / 审核 / 待回答队列(S3)后续再接。
|
|
33
|
+
// 并发闸门只约束调度器自动派发(默认 1);手动 run 走前台,不占闸门。
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
export const TASK_SCHEDULER_INTERVAL_MS = 10_000;
|
|
37
|
+
export const TASK_SCHEDULER_MAX_CONCURRENCY = 1;
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// S3 巡检/审核参数
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
/** 阻塞检测阈值:最后一条消息超过该时长无进展 → 触发 AI 判定 */
|
|
44
|
+
export const BLOCK_IDLE_MS = 5 * 60 * 1000;
|
|
45
|
+
/** 每卡阻塞判定冷却:冷却期内不重复判同一卡 */
|
|
46
|
+
export const BLOCK_COOLDOWN_MS = 10 * 60 * 1000;
|
|
47
|
+
|
|
48
|
+
/** 阻塞判定冷却表(模块级即可:仅节流,热重载重置无害) */
|
|
49
|
+
const blockCheckAt = new Map<string, number>();
|
|
50
|
+
|
|
51
|
+
/** 审核冷却:review 卡 AI 判定未决(other/失败)时,冷却期内不重复审核(防每 10s 烧一次模型) */
|
|
52
|
+
export const AUDIT_COOLDOWN_MS = 5 * 60 * 1000;
|
|
53
|
+
const reviewAuditAt = new Map<string, number>();
|
|
54
|
+
|
|
55
|
+
/** 会话静默期兜底:running 卡会话不在跑但最后活动距今不足该值时,视为刚结束未及事件订阅(防派发启动竞态) */
|
|
56
|
+
export const SESSION_SETTLE_MS = 5_000;
|
|
57
|
+
|
|
58
|
+
declare global {
|
|
59
|
+
var __piTaskScheduler: {
|
|
60
|
+
timer: ReturnType<typeof setInterval>;
|
|
61
|
+
startedAt: number;
|
|
62
|
+
/** 最近一次调度动作(挂 globalThis 防 instrumentation 与 API 路由加载两份模块实例不互通) */
|
|
63
|
+
lastAction: TaskSchedulerLastAction;
|
|
64
|
+
} | undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 派发 cwd:card.cwd 优先;否则看板所属项目根(project_key 若是存在的目录)。 */
|
|
68
|
+
export function resolveDispatchCwd(card: TaskCard): string | null {
|
|
69
|
+
if (card.cwd) return card.cwd;
|
|
70
|
+
const board = getBoard(card.boardId);
|
|
71
|
+
if (board && existsSync(board.projectKey)) return board.projectKey;
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 拼任务 prompt:任务编号+名称 + 描述(markdown)+ 附件引用。 */
|
|
76
|
+
export function buildTaskPrompt(card: TaskCard): string {
|
|
77
|
+
const lines = [`【任务卡 #${card.number}】${card.name}`];
|
|
78
|
+
if (card.description.trim()) {
|
|
79
|
+
lines.push("", card.description.trim());
|
|
80
|
+
}
|
|
81
|
+
if (card.attachments.length > 0) {
|
|
82
|
+
lines.push("", "附件:");
|
|
83
|
+
for (const a of card.attachments) lines.push(`- ${a}`);
|
|
84
|
+
}
|
|
85
|
+
return lines.join("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 确保拿到执行会话:卡已有 sessionId → 复用(重试场景);否则新建。
|
|
90
|
+
* 新建时 cwd 解析失败返回 null(无法派发)。created 标记是否真正新建
|
|
91
|
+
* (仅新建会话失败时需清理,复用会话归调用方/他处管理,不可销毁)。
|
|
92
|
+
*/
|
|
93
|
+
async function ensureExecutionSession(
|
|
94
|
+
card: TaskCard,
|
|
95
|
+
): Promise<{ session: Awaited<ReturnType<typeof startRpcSession>>["session"]; realSessionId: string; created: boolean } | null> {
|
|
96
|
+
if (card.sessionId) {
|
|
97
|
+
try {
|
|
98
|
+
const filePath = await resolveSessionPath(card.sessionId);
|
|
99
|
+
if (filePath) {
|
|
100
|
+
const reused = await startRpcSession(card.sessionId, filePath, undefined);
|
|
101
|
+
return { ...reused, created: false };
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
// 会话文件失效 → 走新建
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const cwd = resolveDispatchCwd(card);
|
|
108
|
+
if (!cwd) return null;
|
|
109
|
+
// 指定会话 ID:调度器创建的执行会话 ID 在发起时即确定,绑定无需等待 realSessionId。
|
|
110
|
+
const fresh = await startRpcSession(randomUUID(), "", cwd, { toolNames: PRESET_FULL });
|
|
111
|
+
return { ...fresh, created: true };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 派发单卡:建/复执行会话 → 命名 #N 标题 → 任务看板归属 → node 绑 session → 发 prompt → running。
|
|
115
|
+
* 跨进程互斥:派发前先原子抢 dispatch_token(谁抢到谁执行),防多实例共库双会话。 */
|
|
116
|
+
export async function dispatchCard(card: TaskCard): Promise<boolean> {
|
|
117
|
+
// 抢派发锁:同卡并发(另一调度器实例)时只有一个能抢到;抢不到直接放弃。
|
|
118
|
+
const token = randomUUID();
|
|
119
|
+
if (!acquireDispatchToken(card.id, token)) {
|
|
120
|
+
console.log(`[task-scheduler] #${card.number} ${card.name} 派发锁被占用,跳过`);
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
let ensured: { session: Awaited<ReturnType<typeof startRpcSession>>["session"]; realSessionId: string; created: boolean } | null = null;
|
|
124
|
+
try {
|
|
125
|
+
ensured = await ensureExecutionSession(card);
|
|
126
|
+
if (!ensured) {
|
|
127
|
+
console.warn(`[task-scheduler] #${card.number} ${card.name} 无可用 cwd,跳过派发`);
|
|
128
|
+
clearDispatchToken(card.id, token);
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
const session = ensured;
|
|
132
|
+
|
|
133
|
+
await session.session.send({ type: "set_session_name", name: `#${card.number} ${card.name}` });
|
|
134
|
+
|
|
135
|
+
const board = getBoard(card.boardId);
|
|
136
|
+
if (board?.taskId) {
|
|
137
|
+
assignSessionToTask(session.realSessionId, board.taskId);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 会话正忙(已在流式/处理中):说明上一轮已发过 prompt,直接标 running,不重复发
|
|
141
|
+
if (session.session.isRunning()) {
|
|
142
|
+
updateCard(card.id, { sessionId: session.realSessionId, execStatus: "running" });
|
|
143
|
+
watchForAgentEnd(card, session.session);
|
|
144
|
+
clearDispatchToken(card.id, token);
|
|
145
|
+
console.log(`[task-scheduler] #${card.number} ${card.name} 会话忙(复用中),标 running 不重发`);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
await session.session.send({ type: "prompt", message: buildTaskPrompt(card) });
|
|
150
|
+
|
|
151
|
+
updateCard(card.id, { sessionId: session.realSessionId, execStatus: "running" });
|
|
152
|
+
watchForAgentEnd(card, session.session);
|
|
153
|
+
clearDispatchToken(card.id, token);
|
|
154
|
+
console.log(
|
|
155
|
+
`[task-scheduler] 派发 #${card.number} ${card.name} → session ${session.realSessionId.slice(0, 8)}`,
|
|
156
|
+
);
|
|
157
|
+
return true;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
// 派发失败(命名/prompt 抛错等):仅销毁本轮新建的会话,防反复失败泄漏;复用会话保留。
|
|
160
|
+
if (ensured?.created) {
|
|
161
|
+
try {
|
|
162
|
+
ensured.session.destroy();
|
|
163
|
+
} catch {
|
|
164
|
+
// 销毁失败不影响派发结果上报
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
clearDispatchToken(card.id, token);
|
|
168
|
+
console.error(
|
|
169
|
+
`[task-scheduler] 派发 #${card.number} ${card.name} 失败:`,
|
|
170
|
+
error instanceof Error ? error.message : error,
|
|
171
|
+
);
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 订阅执行会话 agent_end:会话跑完立即把卡转 review(待审核),不等下一轮 reconcile。
|
|
178
|
+
* 事件驱动实现「会话执行完立即进入待审核」。回调只处理卡仍为 running 的情况,避免覆盖后续流转。
|
|
179
|
+
*/
|
|
180
|
+
function watchForAgentEnd(card: TaskCard, session: Awaited<ReturnType<typeof startRpcSession>>["session"]): void {
|
|
181
|
+
if (typeof session.onEvent !== "function") return;
|
|
182
|
+
const unsub = session.onEvent((event) => {
|
|
183
|
+
if (event.type !== "agent_end") return;
|
|
184
|
+
unsub();
|
|
185
|
+
const fresh = getCard(card.id);
|
|
186
|
+
if (!fresh || fresh.execStatus !== "running") return;
|
|
187
|
+
updateCard(card.id, { execStatus: "review" });
|
|
188
|
+
console.log(`[task-scheduler] #${card.number} ${card.name} 执行会话结束(agent_end) → review(待审核)`);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 巡检:running 卡若执行会话已不在真实运行(AI 会话结束/销毁)→ 转 review(会话结束待审核)。
|
|
194
|
+
* 以真实 pi 会话执行状态为准,防止卡状态 stale 锁死并发闸门。
|
|
195
|
+
* 兜底:事件订阅(agent_end)之外的场景(进程重启/订阅丢失)靠本巡检,静默期防派发启动竞态。
|
|
196
|
+
* 返回本次流转数。
|
|
197
|
+
*/
|
|
198
|
+
export async function reconcileEndedRunningCards(): Promise<number> {
|
|
199
|
+
const runningIds = new Set(getRunningRpcSessionIds());
|
|
200
|
+
const now = Date.now();
|
|
201
|
+
let flipped = 0;
|
|
202
|
+
for (const card of listRunningDispatched()) {
|
|
203
|
+
if (!card.sessionId) continue;
|
|
204
|
+
// 会话真实在跑 → 保持 running
|
|
205
|
+
if (runningIds.has(card.sessionId)) continue;
|
|
206
|
+
// 会话不在跑:确认已静默一段时间(防回合间隙误判),才转 review
|
|
207
|
+
const snapshot = await readSessionAuditSnapshot(card.sessionId);
|
|
208
|
+
if (!snapshot || snapshot.lastActivityMs === 0) continue;
|
|
209
|
+
if (now - snapshot.lastActivityMs < SESSION_SETTLE_MS) continue;
|
|
210
|
+
updateCard(card.id, { execStatus: "review" });
|
|
211
|
+
console.log(`[task-scheduler] #${card.number} ${card.name} 执行会话已结束 → review(待审核)`);
|
|
212
|
+
flipped += 1;
|
|
213
|
+
}
|
|
214
|
+
return flipped;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** 单轮调度:并发闸门未满才派发;最多补满到全局上限。返回本轮派发数。 */
|
|
218
|
+
export async function runSchedulerTick(): Promise<number> {
|
|
219
|
+
// 先巡检:会话已结束的 running 卡流转走(review),释放闸门(以真实会话状态为准)
|
|
220
|
+
await reconcileEndedRunningCards();
|
|
221
|
+
// S3:回复队列优先 → 审核 review 卡 → 巡检阻塞
|
|
222
|
+
await processReplyQueue();
|
|
223
|
+
await processReviewCards();
|
|
224
|
+
await checkRunningCardsBlocked();
|
|
225
|
+
|
|
226
|
+
let dispatched = 0;
|
|
227
|
+
const running = countRunningDispatched();
|
|
228
|
+
const slots = Math.max(0, TASK_SCHEDULER_MAX_CONCURRENCY - running);
|
|
229
|
+
if (slots <= 0) {
|
|
230
|
+
setLastAction({ type: "skipped_gate", at: Date.now() });
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const cards = listDispatchableCards().slice(0, slots);
|
|
235
|
+
for (const card of cards) {
|
|
236
|
+
// 并发闸门按 running 数实时判断(上一张派发后 running 可能已满)
|
|
237
|
+
if (countRunningDispatched() >= TASK_SCHEDULER_MAX_CONCURRENCY) break;
|
|
238
|
+
const ok = await dispatchCard(card);
|
|
239
|
+
if (ok) {
|
|
240
|
+
dispatched += 1;
|
|
241
|
+
setLastAction({ type: "dispatch", cardNumber: card.number, cardName: card.name, at: Date.now() });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return dispatched;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ============================================================================
|
|
248
|
+
// S3:回复队列 + 审核(review 卡)+ 阻塞巡检(running 卡)
|
|
249
|
+
// ============================================================================
|
|
250
|
+
|
|
251
|
+
/** 回复队列:answered 且卡仍在 waiting_reply → 发回复续会话 → running。 */
|
|
252
|
+
export async function processReplyQueue(): Promise<number> {
|
|
253
|
+
let resumed = 0;
|
|
254
|
+
for (const q of listAnswerableQuestions()) {
|
|
255
|
+
const card = getCard(q.cardId);
|
|
256
|
+
if (!card || card.execStatus !== "waiting_reply" || !card.sessionId) continue;
|
|
257
|
+
if (countRunningDispatched() >= TASK_SCHEDULER_MAX_CONCURRENCY) break;
|
|
258
|
+
const ok = await resumeWithAnswer(card, q);
|
|
259
|
+
if (ok) {
|
|
260
|
+
resumed += 1;
|
|
261
|
+
setLastAction({ type: "resume", cardNumber: card.number, cardName: card.name, at: Date.now() });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return resumed;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function resumeWithAnswer(card: TaskCard, q: TaskCardQuestion): Promise<boolean> {
|
|
268
|
+
try {
|
|
269
|
+
const filePath = await resolveSessionPath(card.sessionId!);
|
|
270
|
+
if (!filePath) return false;
|
|
271
|
+
const { session } = await startRpcSession(card.sessionId!, filePath, undefined);
|
|
272
|
+
await session.send({
|
|
273
|
+
type: "prompt",
|
|
274
|
+
message: `【用户回答了你的提问】\n${q.answer}\n\n请据此继续完成任务。`,
|
|
275
|
+
});
|
|
276
|
+
updateCard(card.id, { execStatus: "running" });
|
|
277
|
+
console.log(`[task-scheduler] 回复续会话 #${card.number} ${card.name}`);
|
|
278
|
+
return true;
|
|
279
|
+
} catch (error) {
|
|
280
|
+
console.error(
|
|
281
|
+
`[task-scheduler] 回复续会话失败 #${card.number}:`,
|
|
282
|
+
error instanceof Error ? error.message : error,
|
|
283
|
+
);
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** 失败记次:retry+1;未超上限 → not_started(等下轮调度重试),否则 failed。 */
|
|
289
|
+
function markFailedOrRetry(card: TaskCard): void {
|
|
290
|
+
const next = card.retryCount + 1;
|
|
291
|
+
if (next < card.maxRetries) {
|
|
292
|
+
updateCard(card.id, { execStatus: "not_started", retryCount: next });
|
|
293
|
+
console.log(`[task-scheduler] #${card.number} 失败(${next}/${card.maxRetries})→ 待重试`);
|
|
294
|
+
} else {
|
|
295
|
+
updateCard(card.id, { execStatus: "failed", retryCount: next });
|
|
296
|
+
console.log(`[task-scheduler] #${card.number} 失败(${next}/${card.maxRetries})→ failed`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** 进入等回复:卡 → waiting_reply + 提问入待回答队列。 */
|
|
301
|
+
function enterWaitingReply(card: TaskCard, questionHint: string): void {
|
|
302
|
+
updateCard(card.id, { execStatus: "waiting_reply" });
|
|
303
|
+
if (card.sessionId) {
|
|
304
|
+
createQuestion(card.id, card.sessionId, `AI 需要你回答后继续任务。\n${questionHint.slice(0, 500)}`);
|
|
305
|
+
}
|
|
306
|
+
console.log(`[task-scheduler] #${card.number} → waiting_reply(提问入队)`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** 审核 review 卡:程序检测失败→failed;否则 AI 审核 → done/failed/waiting_reply/other。 */
|
|
310
|
+
export async function processReviewCards(): Promise<number> {
|
|
311
|
+
const now = Date.now();
|
|
312
|
+
let processed = 0;
|
|
313
|
+
for (const card of listCardsByExecStatus(["review"])) {
|
|
314
|
+
const snapshot = card.sessionId ? await readSessionAuditSnapshot(card.sessionId) : null;
|
|
315
|
+
|
|
316
|
+
// 1. 程序检测:最后消息有失败迹象 → 直接 failed(计次重试)
|
|
317
|
+
if (snapshot?.failure) {
|
|
318
|
+
markFailedOrRetry(card);
|
|
319
|
+
processed += 1;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
// 2. 无会话 → 视为完成(空执行)
|
|
323
|
+
if (!card.sessionId || !snapshot) {
|
|
324
|
+
updateCard(card.id, { execStatus: "done" });
|
|
325
|
+
console.log(`[task-scheduler] #${card.number} 无执行内容 → done`);
|
|
326
|
+
processed += 1;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
// 3. AI 独立审核(冷却:未决时冷却期内不重复烧模型)
|
|
330
|
+
const lastAudit = reviewAuditAt.get(card.id) ?? 0;
|
|
331
|
+
if (now - lastAudit < AUDIT_COOLDOWN_MS) continue;
|
|
332
|
+
reviewAuditAt.set(card.id, now);
|
|
333
|
+
const cwd = resolveDispatchCwd(card);
|
|
334
|
+
if (!cwd) continue;
|
|
335
|
+
const result = await runAuditVerdict({
|
|
336
|
+
cwd,
|
|
337
|
+
cardNumber: card.number,
|
|
338
|
+
cardName: card.name,
|
|
339
|
+
taskDescription: card.description,
|
|
340
|
+
recentMessages: snapshot.recentText,
|
|
341
|
+
});
|
|
342
|
+
if (result?.verdict === "done") {
|
|
343
|
+
updateCard(card.id, { execStatus: "done" });
|
|
344
|
+
console.log(`[task-scheduler] #${card.number} 审核通过 → done(${result.reason})`);
|
|
345
|
+
processed += 1;
|
|
346
|
+
} else if (result?.verdict === "failed") {
|
|
347
|
+
markFailedOrRetry(card);
|
|
348
|
+
processed += 1;
|
|
349
|
+
} else if (result?.verdict === "waiting_reply") {
|
|
350
|
+
enterWaitingReply(card, snapshot.recentText);
|
|
351
|
+
processed += 1;
|
|
352
|
+
} else {
|
|
353
|
+
console.log(
|
|
354
|
+
`[task-scheduler] #${card.number} 审核未决:${result ? result.reason : "AI 判定失败"}(保持 review)`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return processed;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** running 卡阻塞巡检:最后消息 > BLOCK_IDLE_MS 无进展 → AI 判定阻塞类型 → 处置。 */
|
|
362
|
+
export async function checkRunningCardsBlocked(): Promise<number> {
|
|
363
|
+
const now = Date.now();
|
|
364
|
+
let handled = 0;
|
|
365
|
+
for (const card of listCardsByExecStatus(["running"])) {
|
|
366
|
+
if (!card.sessionId) continue;
|
|
367
|
+
// 冷却:同一卡冷却期内不重复判定
|
|
368
|
+
const last = blockCheckAt.get(card.id) ?? 0;
|
|
369
|
+
if (now - last < BLOCK_COOLDOWN_MS) continue;
|
|
370
|
+
const snapshot = await readSessionAuditSnapshot(card.sessionId);
|
|
371
|
+
if (!snapshot || snapshot.lastActivityMs === 0) continue;
|
|
372
|
+
// 5min 内还有进展 → 继续观察
|
|
373
|
+
if (now - snapshot.lastActivityMs < BLOCK_IDLE_MS) continue;
|
|
374
|
+
blockCheckAt.set(card.id, now);
|
|
375
|
+
const cwd = resolveDispatchCwd(card);
|
|
376
|
+
if (!cwd) continue;
|
|
377
|
+
const result = await runBlockCheck({
|
|
378
|
+
cwd,
|
|
379
|
+
cardNumber: card.number,
|
|
380
|
+
cardName: card.name,
|
|
381
|
+
taskDescription: card.description,
|
|
382
|
+
recentMessages: snapshot.recentText,
|
|
383
|
+
});
|
|
384
|
+
if (!result) continue;
|
|
385
|
+
switch (result.kind) {
|
|
386
|
+
case "sync_server":
|
|
387
|
+
case "infinite_loop":
|
|
388
|
+
await abortAndReguide(card);
|
|
389
|
+
handled += 1;
|
|
390
|
+
break;
|
|
391
|
+
case "rate_limit":
|
|
392
|
+
console.log(`[task-scheduler] #${card.number} 限流退避,继续观察`);
|
|
393
|
+
break;
|
|
394
|
+
case "asking":
|
|
395
|
+
enterWaitingReply(card, snapshot.recentText);
|
|
396
|
+
handled += 1;
|
|
397
|
+
break;
|
|
398
|
+
case "error":
|
|
399
|
+
// 转 review 由审核路径判定失败/重试
|
|
400
|
+
updateCard(card.id, { execStatus: "review" });
|
|
401
|
+
handled += 1;
|
|
402
|
+
break;
|
|
403
|
+
case "normal":
|
|
404
|
+
// 长任务,继续观察
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return handled;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** 阻塞处置:abort 会话 + 重发 tmux 引导 → 保持 running。 */
|
|
412
|
+
async function abortAndReguide(card: TaskCard): Promise<void> {
|
|
413
|
+
try {
|
|
414
|
+
if (card.sessionId) {
|
|
415
|
+
const filePath = await resolveSessionPath(card.sessionId);
|
|
416
|
+
if (filePath) {
|
|
417
|
+
const { session } = await startRpcSession(card.sessionId, filePath, undefined);
|
|
418
|
+
await session.send({ type: "abort" });
|
|
419
|
+
await session.send({
|
|
420
|
+
type: "prompt",
|
|
421
|
+
message:
|
|
422
|
+
"检测到任务卡执行异常(可能是同步开启服务或死循环)。\n" +
|
|
423
|
+
"请改用 tmux 后台启动服务:tmux new-session -d '命令',不要在前台阻塞等待日志;" +
|
|
424
|
+
"如为死循环请重新规划步骤后继续。",
|
|
425
|
+
});
|
|
426
|
+
watchForAgentEnd(card, session);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
updateCard(card.id, { execStatus: "running" });
|
|
430
|
+
console.log(`[task-scheduler] #${card.number} 阻塞处置:abort+tmux 引导,继续运行`);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
console.error(
|
|
433
|
+
`[task-scheduler] 阻塞处置失败 #${card.number}:`,
|
|
434
|
+
error instanceof Error ? error.message : error,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** 启动调度器(instrumentation 注册;globalThis 防热重载重复启动)。 */
|
|
440
|
+
export function startTaskScheduler(): void {
|
|
441
|
+
if (globalThis.__piTaskScheduler) return;
|
|
442
|
+
let tickInFlight = false;
|
|
443
|
+
const timer = setInterval(() => {
|
|
444
|
+
// 防 tick 重叠:上一轮未跑完(AI 审核耗时可能超过周期)则跳过本轮
|
|
445
|
+
if (tickInFlight) return;
|
|
446
|
+
tickInFlight = true;
|
|
447
|
+
void runSchedulerTick()
|
|
448
|
+
.catch((error) => {
|
|
449
|
+
console.error("[task-scheduler] tick error:", error instanceof Error ? error.message : error);
|
|
450
|
+
})
|
|
451
|
+
.finally(() => {
|
|
452
|
+
tickInFlight = false;
|
|
453
|
+
});
|
|
454
|
+
}, TASK_SCHEDULER_INTERVAL_MS);
|
|
455
|
+
timer.unref?.();
|
|
456
|
+
const sched = { timer, startedAt: Date.now(), lastAction: { type: "none" as const, at: 0 } };
|
|
457
|
+
globalThis.__piTaskScheduler = sched;
|
|
458
|
+
console.log(`[pi-web] task scheduler started (interval ${TASK_SCHEDULER_INTERVAL_MS}ms, max ${TASK_SCHEDULER_MAX_CONCURRENCY})`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ============================================================================
|
|
462
|
+
// 调度状态(看板展示:调度器在执行什么)
|
|
463
|
+
// ============================================================================
|
|
464
|
+
|
|
465
|
+
export interface TaskSchedulerLastAction {
|
|
466
|
+
/** dispatch=派发了任务;resume=回复续会话;skipped_gate=并发闸门满跳过;none=尚未动作 */
|
|
467
|
+
type: "dispatch" | "resume" | "skipped_gate" | "none";
|
|
468
|
+
cardNumber?: number;
|
|
469
|
+
cardName?: string;
|
|
470
|
+
at: number;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export interface TaskSchedulerStatus {
|
|
474
|
+
/** 调度器是否已启动(in-process interval 活着) */
|
|
475
|
+
started: boolean;
|
|
476
|
+
/** 调度器派发且正在运行的卡(列表可能为空=当前无调度执行) */
|
|
477
|
+
running: TaskCard[];
|
|
478
|
+
/** 最近一次调度动作 */
|
|
479
|
+
lastAction: TaskSchedulerLastAction;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function getLastAction(): TaskSchedulerLastAction {
|
|
483
|
+
return globalThis.__piTaskScheduler?.lastAction ?? { type: "none", at: 0 };
|
|
484
|
+
}
|
|
485
|
+
function setLastAction(action: TaskSchedulerLastAction): void {
|
|
486
|
+
if (globalThis.__piTaskScheduler) globalThis.__piTaskScheduler.lastAction = action;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** 当前调度器状态:真实运行中的卡 + 最近动作。UI 轮询展示「正在运行 xxx任务」。
|
|
490
|
+
* running 只列执行会话真实在跑的卡(以 getRunningRpcSessionIds 为准,不读过期卡状态)。 */
|
|
491
|
+
export function getSchedulerStatus(): TaskSchedulerStatus {
|
|
492
|
+
const runningIds = new Set(getRunningRpcSessionIds());
|
|
493
|
+
return {
|
|
494
|
+
started: Boolean(globalThis.__piTaskScheduler),
|
|
495
|
+
running: listRunningDispatched().filter((c) => c.sessionId && runningIds.has(c.sessionId)),
|
|
496
|
+
lastAction: getLastAction(),
|
|
497
|
+
};
|
|
498
|
+
}
|