codexmate 0.1.8 → 0.1.10
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/cli.js +63 -2696
- package/package.json +1 -1
- package/web-ui/app.js +27 -56
- package/web-ui/index.html +0 -1
- package/web-ui/modules/app.computed.dashboard.mjs +0 -3
- package/web-ui/modules/app.computed.main-tabs.mjs +0 -474
- package/web-ui/modules/app.methods.index.mjs +1 -3
- package/web-ui/modules/app.methods.navigation.mjs +1 -2
- package/web-ui/modules/app.methods.session-actions.mjs +146 -21
- package/web-ui/modules/app.methods.session-browser.mjs +5 -0
- package/web-ui/modules/app.methods.web-ui-preferences.mjs +20 -9
- package/web-ui/modules/i18n/locales/en.mjs +14 -265
- package/web-ui/modules/i18n/locales/ja.mjs +14 -265
- package/web-ui/modules/i18n/locales/vi.mjs +14 -266
- package/web-ui/modules/i18n/locales/zh-tw.mjs +14 -265
- package/web-ui/modules/i18n/locales/zh.mjs +14 -265
- package/web-ui/partials/index/layout-header.html +0 -48
- package/web-ui/partials/index/panel-dashboard.html +0 -21
- package/web-ui/partials/index/panel-sessions.html +30 -2
- package/web-ui/res/web-ui-render.precompiled.js +105 -1193
- package/web-ui/session-helpers.mjs +3 -16
- package/web-ui/styles/sessions-list.css +96 -0
- package/web-ui/styles.css +0 -1
- package/lib/automation.js +0 -404
- package/lib/task-orchestrator.js +0 -938
- package/lib/task-workspace-chat.js +0 -292
- package/web-ui/modules/app.methods.task-orchestration.mjs +0 -895
- package/web-ui/partials/index/panel-orchestration.html +0 -598
- package/web-ui/styles/task-orchestration.css +0 -2979
package/cli.js
CHANGED
|
@@ -69,27 +69,6 @@ const {
|
|
|
69
69
|
validateWorkflowDefinition,
|
|
70
70
|
executeWorkflowDefinition
|
|
71
71
|
} = require('./lib/workflow-engine');
|
|
72
|
-
const {
|
|
73
|
-
truncateText: truncateTaskText,
|
|
74
|
-
buildTaskPlan,
|
|
75
|
-
validateTaskPlan,
|
|
76
|
-
executeTaskPlan,
|
|
77
|
-
computePlanWaves
|
|
78
|
-
} = require('./lib/task-orchestrator');
|
|
79
|
-
const {
|
|
80
|
-
buildWorkspaceChatContext,
|
|
81
|
-
loadWorkspaceChatThread,
|
|
82
|
-
appendWorkspaceChatThread,
|
|
83
|
-
applyWorkspaceFileOperations
|
|
84
|
-
} = require('./lib/task-workspace-chat');
|
|
85
|
-
const {
|
|
86
|
-
readAutomationConfig,
|
|
87
|
-
matchAutomationRule,
|
|
88
|
-
buildAutomationEventKey,
|
|
89
|
-
isCronMatch,
|
|
90
|
-
dispatchAutomationNotifiers,
|
|
91
|
-
formatTaskRunNotificationPayload
|
|
92
|
-
} = require('./lib/automation');
|
|
93
72
|
const {
|
|
94
73
|
ALLOWED_EVENTS: WEBHOOK_ALLOWED_EVENTS,
|
|
95
74
|
defaultConfigPath: defaultWebhookConfigPath,
|
|
@@ -268,15 +247,8 @@ const PI_SESSIONS_DIR = path.join(PI_DIR, 'agent', 'sessions');
|
|
|
268
247
|
const RECENT_CONFIGS_FILE = path.join(CONFIG_DIR, 'recent-configs.json');
|
|
269
248
|
const WORKFLOW_DEFINITIONS_FILE = path.join(CONFIG_DIR, 'codexmate-workflows.json');
|
|
270
249
|
const WORKFLOW_RUNS_FILE = path.join(CONFIG_DIR, 'codexmate-workflow-runs.jsonl');
|
|
271
|
-
const TASK_QUEUE_FILE = path.join(CONFIG_DIR, 'codexmate-task-queue.json');
|
|
272
|
-
const TASK_RUNS_FILE = path.join(CONFIG_DIR, 'codexmate-task-runs.jsonl');
|
|
273
|
-
const TASK_RUN_DETAILS_DIR = path.join(CONFIG_DIR, 'codexmate-task-runs');
|
|
274
|
-
const TASK_QUEUE_WORKER_FILE = path.join(CONFIG_DIR, 'codexmate-task-queue-worker.json');
|
|
275
|
-
const TASK_ARTIFACTS_DIR = path.join(CONFIG_DIR, 'codexmate-task-artifacts');
|
|
276
|
-
const TASK_WORKSPACE_CHAT_THREADS_DIR = path.join(CONFIG_DIR, 'codexmate-task-chat-threads');
|
|
277
250
|
const TASK_OPENAI_CHAT_TIMEOUT_MS = 180000;
|
|
278
251
|
const TASK_OPENAI_CHAT_MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
279
|
-
const AUTOMATION_CONFIG_FILE = path.join(CONFIG_DIR, 'codexmate-automation.json');
|
|
280
252
|
const DEFAULT_CLAUDE_MODEL = 'glm-4.7';
|
|
281
253
|
const DEFAULT_MODEL_CONTEXT_WINDOW = 190000;
|
|
282
254
|
const DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT = 185000;
|
|
@@ -998,7 +970,7 @@ function normalizeSettingsTabPreference(value) {
|
|
|
998
970
|
|
|
999
971
|
function normalizeMainTabPreference(value) {
|
|
1000
972
|
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
1001
|
-
const allowed = new Set(['dashboard', 'config', 'sessions', 'usage', '
|
|
973
|
+
const allowed = new Set(['dashboard', 'config', 'sessions', 'usage', 'market', 'plugins', 'docs', 'settings', 'trash', 'prompts']);
|
|
1002
974
|
return allowed.has(normalized) ? normalized : 'dashboard';
|
|
1003
975
|
}
|
|
1004
976
|
|
|
@@ -1015,7 +987,19 @@ function normalizeUsageTimeRangePreference(value) {
|
|
|
1015
987
|
|
|
1016
988
|
function normalizePromptsSubTabPreference(value) {
|
|
1017
989
|
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
1018
|
-
|
|
990
|
+
if (normalized === 'claude-project') return normalized;
|
|
991
|
+
if (normalized === 'system') return normalized;
|
|
992
|
+
return 'codex';
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function normalizeSysPromptScopePreference(value) {
|
|
996
|
+
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
997
|
+
return normalized === 'project' ? 'project' : 'global';
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function normalizeSysPromptModePreference(value) {
|
|
1001
|
+
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
1002
|
+
return normalized === 'append' ? 'append' : 'system';
|
|
1019
1003
|
}
|
|
1020
1004
|
|
|
1021
1005
|
function normalizeSidebarCollapsedPreference(value) {
|
|
@@ -1088,10 +1072,11 @@ function normalizeWebUiPreferences(value = {}) {
|
|
|
1088
1072
|
configTemplateDiffConfirmEnabled: normalizeBooleanPreference(source.configTemplateDiffConfirmEnabled, true),
|
|
1089
1073
|
sessionsUsageTimeRange: normalizeUsageTimeRangePreference(source.sessionsUsageTimeRange),
|
|
1090
1074
|
promptsSubTab: normalizePromptsSubTabPreference(source.promptsSubTab),
|
|
1075
|
+
sysPromptScope: normalizeSysPromptScopePreference(source.sysPromptScope),
|
|
1076
|
+
sysPromptMode: normalizeSysPromptModePreference(source.sysPromptMode),
|
|
1091
1077
|
projectClaudeMdPath: typeof source.projectClaudeMdPath === 'string' ? source.projectClaudeMdPath : '',
|
|
1092
1078
|
sidebarCollapsed: normalizeSidebarCollapsedPreference(source.sidebarCollapsed),
|
|
1093
1079
|
starPrompted: normalizeBooleanPreference(source.starPrompted, false),
|
|
1094
|
-
taskOrchestrationTabEnabled: normalizeBooleanPreference(source.taskOrchestrationTabEnabled, true),
|
|
1095
1080
|
sessionLoadNativeDialog: normalizeBooleanPreference(source.sessionLoadNativeDialog, false),
|
|
1096
1081
|
language: typeof source.language === 'string' ? source.language : '',
|
|
1097
1082
|
sessionFilters: normalizeSessionFiltersPreference(source.sessionFilters),
|
|
@@ -9984,7 +9969,6 @@ async function cmdDoctor(argv = []) {
|
|
|
9984
9969
|
buildInstallStatusReport,
|
|
9985
9970
|
buildConfigHealthReport,
|
|
9986
9971
|
listSessionUsage,
|
|
9987
|
-
buildTaskOverviewPayload,
|
|
9988
9972
|
listSkills
|
|
9989
9973
|
});
|
|
9990
9974
|
const format = options.format === 'md' ? 'md' : 'json';
|
|
@@ -12689,110 +12673,6 @@ function verifyGithubWebhookSignature(secret, signatureHeader, rawBuffer) {
|
|
|
12689
12673
|
return safeTimingEqual(signature, expected);
|
|
12690
12674
|
}
|
|
12691
12675
|
|
|
12692
|
-
async function handleAutomationHook(req, res, source) {
|
|
12693
|
-
const method = (req.method || 'GET').toUpperCase();
|
|
12694
|
-
if (method !== 'POST') {
|
|
12695
|
-
writeJsonResponse(res, 405, { error: 'Method Not Allowed' });
|
|
12696
|
-
return;
|
|
12697
|
-
}
|
|
12698
|
-
const deliveryId = typeof (req.headers || {})['x-github-delivery'] === 'string'
|
|
12699
|
-
? String(req.headers['x-github-delivery'] || '')
|
|
12700
|
-
: (typeof (req.headers || {})['x-gitlab-event-uuid'] === 'string' ? String(req.headers['x-gitlab-event-uuid'] || '') : '');
|
|
12701
|
-
const remember = rememberWebhookDeliveryId(deliveryId);
|
|
12702
|
-
if (remember.seen) {
|
|
12703
|
-
writeJsonResponse(res, 200, { ok: true, deduped: true });
|
|
12704
|
-
return;
|
|
12705
|
-
}
|
|
12706
|
-
const parsedBody = await readJsonRequestBody(req, res);
|
|
12707
|
-
if (!parsedBody.ok) {
|
|
12708
|
-
if (parsedBody.error !== 'payload-too-large') {
|
|
12709
|
-
writeJsonResponse(res, 400, { error: parsedBody.error || 'invalid request body' });
|
|
12710
|
-
}
|
|
12711
|
-
return;
|
|
12712
|
-
}
|
|
12713
|
-
const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
|
|
12714
|
-
const isLoopback = !remoteAddr || isLoopbackRemoteAddress(remoteAddr);
|
|
12715
|
-
const normalizedSource = typeof source === 'string' ? source.trim().toLowerCase() : '';
|
|
12716
|
-
if (normalizedSource === 'github') {
|
|
12717
|
-
const secret = typeof process.env.CODEXMATE_GITHUB_WEBHOOK_SECRET === 'string'
|
|
12718
|
-
? process.env.CODEXMATE_GITHUB_WEBHOOK_SECRET
|
|
12719
|
-
: '';
|
|
12720
|
-
if (!secret && !isLoopback) {
|
|
12721
|
-
writeJsonResponse(res, 403, { error: 'Remote GitHub webhook is disabled (set CODEXMATE_GITHUB_WEBHOOK_SECRET)' });
|
|
12722
|
-
return;
|
|
12723
|
-
}
|
|
12724
|
-
if (secret) {
|
|
12725
|
-
const signature = (req.headers || {})['x-hub-signature-256'];
|
|
12726
|
-
if (!verifyGithubWebhookSignature(secret, signature, parsedBody.rawBuffer)) {
|
|
12727
|
-
writeJsonResponse(res, 401, { error: 'Invalid webhook signature' });
|
|
12728
|
-
return;
|
|
12729
|
-
}
|
|
12730
|
-
}
|
|
12731
|
-
} else if (normalizedSource === 'gitlab') {
|
|
12732
|
-
const secret = typeof process.env.CODEXMATE_GITLAB_WEBHOOK_SECRET === 'string'
|
|
12733
|
-
? process.env.CODEXMATE_GITLAB_WEBHOOK_SECRET.trim()
|
|
12734
|
-
: '';
|
|
12735
|
-
if (!secret && !isLoopback) {
|
|
12736
|
-
writeJsonResponse(res, 403, { error: 'Remote GitLab webhook is disabled (set CODEXMATE_GITLAB_WEBHOOK_SECRET)' });
|
|
12737
|
-
return;
|
|
12738
|
-
}
|
|
12739
|
-
if (secret) {
|
|
12740
|
-
const tokenHeader = typeof (req.headers || {})['x-gitlab-token'] === 'string'
|
|
12741
|
-
? String(req.headers['x-gitlab-token']).trim()
|
|
12742
|
-
: '';
|
|
12743
|
-
if (!tokenHeader || tokenHeader !== secret) {
|
|
12744
|
-
writeJsonResponse(res, 401, { error: 'Invalid webhook token' });
|
|
12745
|
-
return;
|
|
12746
|
-
}
|
|
12747
|
-
}
|
|
12748
|
-
}
|
|
12749
|
-
const payload = parsedBody.body && typeof parsedBody.body === 'object' ? parsedBody.body : {};
|
|
12750
|
-
const eventKey = buildAutomationEventKey(source, req.headers || {}, payload);
|
|
12751
|
-
if (!eventKey) {
|
|
12752
|
-
writeJsonResponse(res, 400, { error: 'unknown event' });
|
|
12753
|
-
return;
|
|
12754
|
-
}
|
|
12755
|
-
const cfg = readAutomationConfig(AUTOMATION_CONFIG_FILE, { env: process.env });
|
|
12756
|
-
if (!cfg.ok) {
|
|
12757
|
-
writeJsonResponse(res, 500, { error: cfg.error || 'failed to load automation config' });
|
|
12758
|
-
return;
|
|
12759
|
-
}
|
|
12760
|
-
const rule = matchAutomationRule(cfg.config, { source, event: eventKey });
|
|
12761
|
-
if (!rule) {
|
|
12762
|
-
writeJsonResponse(res, 404, { error: 'no matching rule', source, event: eventKey });
|
|
12763
|
-
return;
|
|
12764
|
-
}
|
|
12765
|
-
const action = rule.action && typeof rule.action === 'object' ? rule.action : {};
|
|
12766
|
-
const actionType = typeof action.type === 'string' ? action.type.trim().toLowerCase() : '';
|
|
12767
|
-
if (actionType !== 'task.queue.add') {
|
|
12768
|
-
writeJsonResponse(res, 400, { error: 'unsupported rule action', action: actionType || '' });
|
|
12769
|
-
return;
|
|
12770
|
-
}
|
|
12771
|
-
const taskPayload = action.task && typeof action.task === 'object' ? action.task : {};
|
|
12772
|
-
const enqueue = addTaskToQueue(taskPayload);
|
|
12773
|
-
if (enqueue.error) {
|
|
12774
|
-
writeJsonResponse(res, 400, { error: enqueue.error, issues: enqueue.issues || [], warnings: enqueue.warnings || [] });
|
|
12775
|
-
return;
|
|
12776
|
-
}
|
|
12777
|
-
const taskId = enqueue.task && enqueue.task.taskId ? enqueue.task.taskId : '';
|
|
12778
|
-
const shouldStart = action.startQueue === true;
|
|
12779
|
-
const queueResult = shouldStart
|
|
12780
|
-
? await startTaskQueueProcessing({ taskId: '', detach: true })
|
|
12781
|
-
: { ok: true, started: false };
|
|
12782
|
-
writeJsonResponse(res, 200, {
|
|
12783
|
-
ok: true,
|
|
12784
|
-
ruleId: rule.id,
|
|
12785
|
-
source,
|
|
12786
|
-
event: eventKey,
|
|
12787
|
-
taskId,
|
|
12788
|
-
queue: {
|
|
12789
|
-
started: !!queueResult.started,
|
|
12790
|
-
alreadyRunning: !!queueResult.alreadyRunning,
|
|
12791
|
-
detached: !!queueResult.detached
|
|
12792
|
-
}
|
|
12793
|
-
});
|
|
12794
|
-
}
|
|
12795
|
-
|
|
12796
12676
|
function streamZipDownloadResponse(res, filePath, options = {}) {
|
|
12797
12677
|
if (!filePath || !fs.existsSync(filePath)) {
|
|
12798
12678
|
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
@@ -13158,7 +13038,6 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
13158
13038
|
if (
|
|
13159
13039
|
requestPath === '/api'
|
|
13160
13040
|
|| requestPath.startsWith('/api/import-')
|
|
13161
|
-
|| requestPath.startsWith('/hooks/')
|
|
13162
13041
|
|| requestPath.startsWith('/download/')
|
|
13163
13042
|
) {
|
|
13164
13043
|
const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
|
|
@@ -13183,12 +13062,6 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
13183
13062
|
void handleImportSkillsZipUpload(req, res, { targetApp: 'codex' });
|
|
13184
13063
|
return;
|
|
13185
13064
|
}
|
|
13186
|
-
if (requestPath.startsWith('/hooks/')) {
|
|
13187
|
-
const segments = requestPath.split('/').filter(Boolean);
|
|
13188
|
-
const source = segments[1] ? String(segments[1]) : '';
|
|
13189
|
-
void handleAutomationHook(req, res, source);
|
|
13190
|
-
return;
|
|
13191
|
-
}
|
|
13192
13065
|
if (requestPath === '/api') {
|
|
13193
13066
|
const method = (req.method ? String(req.method) : 'POST').toUpperCase();
|
|
13194
13067
|
if (method !== 'POST') {
|
|
@@ -13414,7 +13287,6 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
13414
13287
|
buildInstallStatusReport,
|
|
13415
13288
|
buildConfigHealthReport,
|
|
13416
13289
|
listSessionUsage,
|
|
13417
|
-
buildTaskOverviewPayload,
|
|
13418
13290
|
listSkills
|
|
13419
13291
|
});
|
|
13420
13292
|
result = buildDoctorLegacyPayload(report);
|
|
@@ -13889,113 +13761,6 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
13889
13761
|
};
|
|
13890
13762
|
}
|
|
13891
13763
|
break;
|
|
13892
|
-
case 'task-overview':
|
|
13893
|
-
result = buildTaskOverviewPayload(params || {});
|
|
13894
|
-
break;
|
|
13895
|
-
case 'task-plan':
|
|
13896
|
-
{
|
|
13897
|
-
const plan = coerceTaskPlanPayload(params || {});
|
|
13898
|
-
const validation = validatePreparedTaskPlan(plan);
|
|
13899
|
-
result = {
|
|
13900
|
-
ok: validation.ok,
|
|
13901
|
-
plan,
|
|
13902
|
-
issues: validation.issues || [],
|
|
13903
|
-
warnings: validation.warnings || []
|
|
13904
|
-
};
|
|
13905
|
-
if (!validation.ok) {
|
|
13906
|
-
result.error = validation.error || 'task plan validation failed';
|
|
13907
|
-
}
|
|
13908
|
-
}
|
|
13909
|
-
break;
|
|
13910
|
-
case 'task-run':
|
|
13911
|
-
{
|
|
13912
|
-
const detach = !!(params && params.detach);
|
|
13913
|
-
if (detach) {
|
|
13914
|
-
const plan = coerceTaskPlanPayload(params || {});
|
|
13915
|
-
const validation = validatePreparedTaskPlan(plan);
|
|
13916
|
-
if (!validation.ok) {
|
|
13917
|
-
result = {
|
|
13918
|
-
ok: false,
|
|
13919
|
-
error: validation.error || 'task plan validation failed',
|
|
13920
|
-
issues: validation.issues || [],
|
|
13921
|
-
warnings: validation.warnings || []
|
|
13922
|
-
};
|
|
13923
|
-
break;
|
|
13924
|
-
}
|
|
13925
|
-
const taskId = typeof params.taskId === 'string' && params.taskId.trim() ? params.taskId.trim() : createTaskId();
|
|
13926
|
-
const runId = createTaskRunId();
|
|
13927
|
-
runTaskPlanInternal(plan, { taskId, runId }).catch(() => { });
|
|
13928
|
-
result = {
|
|
13929
|
-
ok: true,
|
|
13930
|
-
started: true,
|
|
13931
|
-
detached: true,
|
|
13932
|
-
taskId,
|
|
13933
|
-
runId,
|
|
13934
|
-
threadId: plan.threadId || '',
|
|
13935
|
-
cwd: plan.cwd || '',
|
|
13936
|
-
warnings: validation.warnings || []
|
|
13937
|
-
};
|
|
13938
|
-
} else {
|
|
13939
|
-
result = await runTaskNow(params || {});
|
|
13940
|
-
}
|
|
13941
|
-
}
|
|
13942
|
-
break;
|
|
13943
|
-
case 'task-runs':
|
|
13944
|
-
{
|
|
13945
|
-
const rawLimit = params && Number.isFinite(params.limit) ? params.limit : parseInt(params && params.limit, 10);
|
|
13946
|
-
const limit = Number.isFinite(rawLimit) ? Math.max(1, Math.floor(rawLimit)) : 20;
|
|
13947
|
-
result = {
|
|
13948
|
-
runs: listTaskRunRecords(limit),
|
|
13949
|
-
limit
|
|
13950
|
-
};
|
|
13951
|
-
}
|
|
13952
|
-
break;
|
|
13953
|
-
case 'task-run-detail':
|
|
13954
|
-
{
|
|
13955
|
-
const runIdValidation = validateTaskRunId(params && typeof params.runId === 'string' ? params.runId : '');
|
|
13956
|
-
if (!runIdValidation.ok) {
|
|
13957
|
-
result = { error: runIdValidation.error };
|
|
13958
|
-
break;
|
|
13959
|
-
}
|
|
13960
|
-
const detail = readTaskRunDetail(runIdValidation.runId);
|
|
13961
|
-
result = detail || { error: `task run not found: ${runIdValidation.runId}` };
|
|
13962
|
-
}
|
|
13963
|
-
break;
|
|
13964
|
-
case 'task-queue-add':
|
|
13965
|
-
result = addTaskToQueue(params || {});
|
|
13966
|
-
break;
|
|
13967
|
-
case 'task-queue-list':
|
|
13968
|
-
{
|
|
13969
|
-
const rawLimit = params && Number.isFinite(params.limit) ? params.limit : parseInt(params && params.limit, 10);
|
|
13970
|
-
const limit = Number.isFinite(rawLimit) ? Math.max(1, Math.floor(rawLimit)) : 50;
|
|
13971
|
-
result = {
|
|
13972
|
-
tasks: listTaskQueueItems({ limit, status: params && params.status }),
|
|
13973
|
-
limit
|
|
13974
|
-
};
|
|
13975
|
-
}
|
|
13976
|
-
break;
|
|
13977
|
-
case 'task-queue-show':
|
|
13978
|
-
{
|
|
13979
|
-
const taskId = params && typeof params.taskId === 'string' ? params.taskId.trim() : '';
|
|
13980
|
-
if (!taskId) {
|
|
13981
|
-
result = { error: 'taskId is required' };
|
|
13982
|
-
break;
|
|
13983
|
-
}
|
|
13984
|
-
result = getTaskQueueItem(taskId) || { error: `task not found: ${taskId}` };
|
|
13985
|
-
}
|
|
13986
|
-
break;
|
|
13987
|
-
case 'task-queue-start':
|
|
13988
|
-
result = await startTaskQueueProcessing(params || {});
|
|
13989
|
-
break;
|
|
13990
|
-
case 'task-retry':
|
|
13991
|
-
result = await retryTaskRun(params || {});
|
|
13992
|
-
break;
|
|
13993
|
-
case 'task-cancel':
|
|
13994
|
-
result = cancelTaskRunOrQueue(params || {});
|
|
13995
|
-
break;
|
|
13996
|
-
case 'task-logs':
|
|
13997
|
-
result = getTaskLogs(params || {});
|
|
13998
|
-
break;
|
|
13999
13764
|
default:
|
|
14000
13765
|
result = { error: '未知操作' };
|
|
14001
13766
|
}
|
|
@@ -14360,15 +14125,12 @@ async function cmdStart(options = {}) {
|
|
|
14360
14125
|
openBrowser: shouldOpenBrowser
|
|
14361
14126
|
});
|
|
14362
14127
|
|
|
14363
|
-
const stopAutomationScheduler = startAutomationScheduler();
|
|
14364
|
-
|
|
14365
14128
|
// 禁止前端变更侦测与自动重启:避免终端输出噪音与访问时短暂 Connection Refused。
|
|
14366
14129
|
// 如需热重启,请由开发者自行使用外部 watcher / nodemon 等工具。
|
|
14367
14130
|
const stopWatch = () => { };
|
|
14368
14131
|
|
|
14369
14132
|
const handleExit = () => {
|
|
14370
14133
|
stopWatch();
|
|
14371
|
-
stopAutomationScheduler();
|
|
14372
14134
|
Promise.allSettled([
|
|
14373
14135
|
serverHandle.stop(),
|
|
14374
14136
|
stopBuiltinProxyRuntime(),
|
|
@@ -14754,713 +14516,75 @@ async function cmdWorkflow(args = []) {
|
|
|
14754
14516
|
throw new Error(`未知 workflow 子命令: ${subcommand}`);
|
|
14755
14517
|
}
|
|
14756
14518
|
|
|
14757
|
-
|
|
14758
|
-
|
|
14759
|
-
console.log(' codexmate task plan --target "实现任务编排 Tab" --follow-up "继续处理 review"');
|
|
14760
|
-
console.log(' codexmate task run --target "修复失败测试" --allow-write --concurrency 2');
|
|
14761
|
-
console.log(' codexmate task run --target "检查请求链路" --dry-run --plan-only');
|
|
14762
|
-
console.log(' codexmate task runs --limit 20');
|
|
14763
|
-
console.log(' codexmate task queue add --target "整理 workflow 入口" --allow-write');
|
|
14764
|
-
console.log(' codexmate task queue list');
|
|
14765
|
-
console.log(' codexmate task queue show <taskId>');
|
|
14766
|
-
console.log(' codexmate task queue start [<taskId>] [--detach]');
|
|
14767
|
-
console.log(' codexmate task retry <runId>');
|
|
14768
|
-
console.log(' codexmate task cancel <taskId|runId>');
|
|
14769
|
-
console.log(' codexmate task logs <runId>');
|
|
14770
|
-
console.log('参数:');
|
|
14771
|
-
console.log(' --target <文本> 任务目标文本');
|
|
14772
|
-
console.log(' --title <文本> 任务标题');
|
|
14773
|
-
console.log(' --notes <文本> 附加说明');
|
|
14774
|
-
console.log(' --plan <JSON|@file> 直接提供任务计划对象');
|
|
14775
|
-
console.log(' --workflow-id <ID> 复用现有 workflow(可重复)');
|
|
14776
|
-
console.log(' --follow-up <文本> 追加 follow-up(可重复)');
|
|
14777
|
-
console.log(' --allow-write 允许写入工作区');
|
|
14778
|
-
console.log(' --dry-run 仅计划/预演,不执行写入');
|
|
14779
|
-
console.log(' --plan-only 仅输出计划,不执行');
|
|
14780
|
-
console.log(' --engine <openai-chat|workflow> 选择编排引擎');
|
|
14781
|
-
console.log(' --cwd <路径> 指定任务工作区路径');
|
|
14782
|
-
console.log(' --thread-id <ID> 指定任务线程 ID');
|
|
14783
|
-
console.log(' --conversation-id <ID> 指定任务线程 ID(兼容别名)');
|
|
14784
|
-
console.log(' --session-id <ID> 指定任务线程 ID(兼容别名)');
|
|
14785
|
-
console.log(' --concurrency <N> 并发度');
|
|
14786
|
-
console.log(' --auto-fix-rounds <N> 自动修复回合数');
|
|
14787
|
-
console.log(' --limit <N> runs/queue list 数量');
|
|
14788
|
-
console.log(' --task-id <ID> 指定任务 ID');
|
|
14789
|
-
console.log(' --run-id <ID> 指定运行 ID');
|
|
14790
|
-
console.log(' --status <状态> queue list 状态过滤');
|
|
14791
|
-
console.log(' --detach 后台启动任务或队列');
|
|
14792
|
-
console.log(' --json 以 JSON 输出');
|
|
14793
|
-
console.log();
|
|
14794
|
-
}
|
|
14795
|
-
|
|
14796
|
-
function parseTaskCliOptions(args = []) {
|
|
14519
|
+
// #region parseCodexProxyOptions
|
|
14520
|
+
function parseCodexProxyOptions(args = []) {
|
|
14797
14521
|
const options = {
|
|
14798
|
-
|
|
14799
|
-
|
|
14800
|
-
notes: '',
|
|
14801
|
-
planRaw: '',
|
|
14802
|
-
workflowIds: [],
|
|
14803
|
-
followUps: [],
|
|
14804
|
-
allowWrite: false,
|
|
14805
|
-
dryRun: false,
|
|
14806
|
-
planOnly: false,
|
|
14807
|
-
engine: 'openai-chat',
|
|
14808
|
-
concurrency: 2,
|
|
14809
|
-
autoFixRounds: 1,
|
|
14810
|
-
limit: 20,
|
|
14811
|
-
taskId: '',
|
|
14812
|
-
runId: '',
|
|
14813
|
-
status: '',
|
|
14814
|
-
detach: false,
|
|
14815
|
-
json: false,
|
|
14816
|
-
explicit: {}
|
|
14522
|
+
passthroughArgs: [],
|
|
14523
|
+
queuedFollowUps: []
|
|
14817
14524
|
};
|
|
14818
|
-
const
|
|
14819
|
-
|
|
14820
|
-
|
|
14821
|
-
|
|
14525
|
+
const argv = Array.isArray(args) ? args : [];
|
|
14526
|
+
|
|
14527
|
+
const pushFollowUp = (value, optionName) => {
|
|
14528
|
+
const raw = value === undefined || value === null ? '' : String(value);
|
|
14529
|
+
if (!raw.trim()) {
|
|
14822
14530
|
throw new Error(`${optionName} 需要提供非空内容`);
|
|
14823
14531
|
}
|
|
14824
|
-
options
|
|
14532
|
+
options.queuedFollowUps.push(raw);
|
|
14825
14533
|
};
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
|
|
14829
|
-
if (arg ===
|
|
14830
|
-
options.allowWrite = true;
|
|
14831
|
-
options.explicit.allowWrite = true;
|
|
14832
|
-
continue;
|
|
14833
|
-
}
|
|
14834
|
-
if (arg === '--dry-run') {
|
|
14835
|
-
options.dryRun = true;
|
|
14836
|
-
options.explicit.dryRun = true;
|
|
14837
|
-
continue;
|
|
14838
|
-
}
|
|
14839
|
-
if (arg === '--plan-only') {
|
|
14840
|
-
options.planOnly = true;
|
|
14841
|
-
continue;
|
|
14842
|
-
}
|
|
14843
|
-
if (arg === '--detach') {
|
|
14844
|
-
options.detach = true;
|
|
14845
|
-
continue;
|
|
14846
|
-
}
|
|
14847
|
-
if (arg === '--json') {
|
|
14848
|
-
options.json = true;
|
|
14849
|
-
continue;
|
|
14850
|
-
}
|
|
14851
|
-
if (arg === '--title') {
|
|
14852
|
-
options.title = String(args[i + 1] || '').trim();
|
|
14853
|
-
options.explicit.title = true;
|
|
14854
|
-
i += 1;
|
|
14855
|
-
continue;
|
|
14856
|
-
}
|
|
14857
|
-
if (arg.startsWith('--title=')) {
|
|
14858
|
-
options.title = arg.slice('--title='.length).trim();
|
|
14859
|
-
options.explicit.title = true;
|
|
14860
|
-
continue;
|
|
14861
|
-
}
|
|
14862
|
-
if (arg === '--target') {
|
|
14863
|
-
options.target = String(args[i + 1] || '').trim();
|
|
14864
|
-
options.explicit.target = true;
|
|
14865
|
-
i += 1;
|
|
14866
|
-
continue;
|
|
14867
|
-
}
|
|
14868
|
-
if (arg.startsWith('--target=')) {
|
|
14869
|
-
options.target = arg.slice('--target='.length).trim();
|
|
14870
|
-
options.explicit.target = true;
|
|
14871
|
-
continue;
|
|
14872
|
-
}
|
|
14873
|
-
if (arg === '--notes') {
|
|
14874
|
-
options.notes = String(args[i + 1] || '').trim();
|
|
14875
|
-
options.explicit.notes = true;
|
|
14876
|
-
i += 1;
|
|
14877
|
-
continue;
|
|
14878
|
-
}
|
|
14879
|
-
if (arg.startsWith('--notes=')) {
|
|
14880
|
-
options.notes = arg.slice('--notes='.length).trim();
|
|
14881
|
-
options.explicit.notes = true;
|
|
14882
|
-
continue;
|
|
14883
|
-
}
|
|
14884
|
-
if (arg === '--plan') {
|
|
14885
|
-
options.planRaw = String(args[i + 1] || '').trim();
|
|
14886
|
-
options.explicit.planRaw = true;
|
|
14887
|
-
i += 1;
|
|
14888
|
-
continue;
|
|
14889
|
-
}
|
|
14890
|
-
if (arg.startsWith('--plan=')) {
|
|
14891
|
-
options.planRaw = arg.slice('--plan='.length).trim();
|
|
14892
|
-
options.explicit.planRaw = true;
|
|
14893
|
-
continue;
|
|
14894
|
-
}
|
|
14895
|
-
if (arg === '--workflow-id') {
|
|
14896
|
-
pushValue('workflowIds', args[i + 1], '--workflow-id');
|
|
14897
|
-
options.explicit.workflowIds = true;
|
|
14898
|
-
i += 1;
|
|
14899
|
-
continue;
|
|
14900
|
-
}
|
|
14901
|
-
if (arg.startsWith('--workflow-id=')) {
|
|
14902
|
-
pushValue('workflowIds', arg.slice('--workflow-id='.length), '--workflow-id');
|
|
14903
|
-
options.explicit.workflowIds = true;
|
|
14904
|
-
continue;
|
|
14905
|
-
}
|
|
14906
|
-
if (arg === '--follow-up') {
|
|
14907
|
-
pushValue('followUps', args[i + 1], '--follow-up');
|
|
14908
|
-
options.explicit.followUps = true;
|
|
14909
|
-
i += 1;
|
|
14910
|
-
continue;
|
|
14911
|
-
}
|
|
14912
|
-
if (arg.startsWith('--follow-up=')) {
|
|
14913
|
-
pushValue('followUps', arg.slice('--follow-up='.length), '--follow-up');
|
|
14914
|
-
options.explicit.followUps = true;
|
|
14915
|
-
continue;
|
|
14916
|
-
}
|
|
14917
|
-
if (arg === '--engine') {
|
|
14918
|
-
options.engine = normalizeTaskEngine(args[i + 1]);
|
|
14919
|
-
options.explicit.engine = true;
|
|
14920
|
-
i += 1;
|
|
14921
|
-
continue;
|
|
14922
|
-
}
|
|
14923
|
-
if (arg.startsWith('--engine=')) {
|
|
14924
|
-
options.engine = normalizeTaskEngine(arg.slice('--engine='.length));
|
|
14925
|
-
options.explicit.engine = true;
|
|
14926
|
-
continue;
|
|
14927
|
-
}
|
|
14928
|
-
if (arg === '--concurrency') {
|
|
14929
|
-
const value = parseInt(args[i + 1], 10);
|
|
14930
|
-
if (Number.isFinite(value)) options.concurrency = value;
|
|
14931
|
-
options.explicit.concurrency = true;
|
|
14932
|
-
i += 1;
|
|
14933
|
-
continue;
|
|
14934
|
-
}
|
|
14935
|
-
if (arg.startsWith('--concurrency=')) {
|
|
14936
|
-
const value = parseInt(arg.slice('--concurrency='.length), 10);
|
|
14937
|
-
if (Number.isFinite(value)) options.concurrency = value;
|
|
14938
|
-
options.explicit.concurrency = true;
|
|
14939
|
-
continue;
|
|
14940
|
-
}
|
|
14941
|
-
if (arg === '--auto-fix-rounds') {
|
|
14942
|
-
const value = parseInt(args[i + 1], 10);
|
|
14943
|
-
if (Number.isFinite(value)) options.autoFixRounds = value;
|
|
14944
|
-
options.explicit.autoFixRounds = true;
|
|
14945
|
-
i += 1;
|
|
14946
|
-
continue;
|
|
14947
|
-
}
|
|
14948
|
-
if (arg.startsWith('--auto-fix-rounds=')) {
|
|
14949
|
-
const value = parseInt(arg.slice('--auto-fix-rounds='.length), 10);
|
|
14950
|
-
if (Number.isFinite(value)) options.autoFixRounds = value;
|
|
14951
|
-
options.explicit.autoFixRounds = true;
|
|
14952
|
-
continue;
|
|
14953
|
-
}
|
|
14954
|
-
if (arg === '--cwd') {
|
|
14955
|
-
options.cwd = String(args[i + 1] || '').trim();
|
|
14956
|
-
options.explicit.cwd = true;
|
|
14957
|
-
i += 1;
|
|
14958
|
-
continue;
|
|
14959
|
-
}
|
|
14960
|
-
if (arg.startsWith('--cwd=')) {
|
|
14961
|
-
options.cwd = arg.slice('--cwd='.length).trim();
|
|
14962
|
-
options.explicit.cwd = true;
|
|
14963
|
-
continue;
|
|
14964
|
-
}
|
|
14965
|
-
if (arg === '--thread-id' || arg === '--conversation-id' || arg === '--session-id') {
|
|
14966
|
-
options.threadId = String(args[i + 1] || '').trim();
|
|
14967
|
-
options.explicit.threadId = true;
|
|
14968
|
-
i += 1;
|
|
14969
|
-
continue;
|
|
14970
|
-
}
|
|
14971
|
-
if (arg.startsWith('--thread-id=')) {
|
|
14972
|
-
options.threadId = arg.slice('--thread-id='.length).trim();
|
|
14973
|
-
options.explicit.threadId = true;
|
|
14974
|
-
continue;
|
|
14975
|
-
}
|
|
14976
|
-
if (arg.startsWith('--conversation-id=')) {
|
|
14977
|
-
options.threadId = arg.slice('--conversation-id='.length).trim();
|
|
14978
|
-
options.explicit.threadId = true;
|
|
14979
|
-
continue;
|
|
14980
|
-
}
|
|
14981
|
-
if (arg.startsWith('--session-id=')) {
|
|
14982
|
-
options.threadId = arg.slice('--session-id='.length).trim();
|
|
14983
|
-
options.explicit.threadId = true;
|
|
14984
|
-
continue;
|
|
14985
|
-
}
|
|
14986
|
-
if (arg === '--limit') {
|
|
14987
|
-
const value = parseInt(args[i + 1], 10);
|
|
14988
|
-
if (Number.isFinite(value)) options.limit = value;
|
|
14989
|
-
i += 1;
|
|
14990
|
-
continue;
|
|
14991
|
-
}
|
|
14992
|
-
if (arg.startsWith('--limit=')) {
|
|
14993
|
-
const value = parseInt(arg.slice('--limit='.length), 10);
|
|
14994
|
-
if (Number.isFinite(value)) options.limit = value;
|
|
14995
|
-
continue;
|
|
14996
|
-
}
|
|
14997
|
-
if (arg === '--task-id') {
|
|
14998
|
-
options.taskId = String(args[i + 1] || '').trim();
|
|
14999
|
-
options.explicit.taskId = true;
|
|
15000
|
-
i += 1;
|
|
14534
|
+
|
|
14535
|
+
for (let i = 0; i < argv.length; i++) {
|
|
14536
|
+
const arg = argv[i];
|
|
14537
|
+
if (arg === undefined || arg === null) {
|
|
15001
14538
|
continue;
|
|
15002
14539
|
}
|
|
15003
|
-
|
|
15004
|
-
|
|
15005
|
-
options.
|
|
15006
|
-
|
|
14540
|
+
const text = String(arg);
|
|
14541
|
+
if (text === '--') {
|
|
14542
|
+
options.passthroughArgs.push(...argv.slice(i).map((item) => String(item)));
|
|
14543
|
+
break;
|
|
15007
14544
|
}
|
|
15008
|
-
if (
|
|
15009
|
-
|
|
15010
|
-
|
|
14545
|
+
if (text === '--queued-follow-up' || text === '--follow-up') {
|
|
14546
|
+
const next = argv[i + 1];
|
|
14547
|
+
if (next === undefined) {
|
|
14548
|
+
throw new Error(`${text} 需要提供内容`);
|
|
14549
|
+
}
|
|
14550
|
+
pushFollowUp(next, text);
|
|
15011
14551
|
i += 1;
|
|
15012
14552
|
continue;
|
|
15013
14553
|
}
|
|
15014
|
-
if (
|
|
15015
|
-
|
|
15016
|
-
options.explicit.runId = true;
|
|
15017
|
-
continue;
|
|
15018
|
-
}
|
|
15019
|
-
if (arg === '--status') {
|
|
15020
|
-
options.status = String(args[i + 1] || '').trim().toLowerCase();
|
|
15021
|
-
i += 1;
|
|
14554
|
+
if (text.startsWith('--queued-follow-up=')) {
|
|
14555
|
+
pushFollowUp(text.slice('--queued-follow-up='.length), '--queued-follow-up');
|
|
15022
14556
|
continue;
|
|
15023
14557
|
}
|
|
15024
|
-
if (
|
|
15025
|
-
|
|
14558
|
+
if (text.startsWith('--follow-up=')) {
|
|
14559
|
+
pushFollowUp(text.slice('--follow-up='.length), '--follow-up');
|
|
15026
14560
|
continue;
|
|
15027
14561
|
}
|
|
15028
|
-
|
|
14562
|
+
options.passthroughArgs.push(text);
|
|
15029
14563
|
}
|
|
15030
|
-
|
|
14564
|
+
|
|
14565
|
+
return options;
|
|
15031
14566
|
}
|
|
14567
|
+
// #endregion parseCodexProxyOptions
|
|
15032
14568
|
|
|
15033
|
-
function
|
|
15034
|
-
const
|
|
15035
|
-
|
|
15036
|
-
if (explicit.title && options.title) payload.title = options.title;
|
|
15037
|
-
if (explicit.target && options.target) payload.target = options.target;
|
|
15038
|
-
if (explicit.notes && options.notes) payload.notes = options.notes;
|
|
15039
|
-
if (explicit.workflowIds && Array.isArray(options.workflowIds)) payload.workflowIds = options.workflowIds.slice();
|
|
15040
|
-
if (explicit.followUps && Array.isArray(options.followUps)) payload.followUps = options.followUps.slice();
|
|
15041
|
-
if (explicit.allowWrite) payload.allowWrite = options.allowWrite === true;
|
|
15042
|
-
if (explicit.dryRun) payload.dryRun = options.dryRun === true;
|
|
15043
|
-
if (explicit.engine) payload.engine = options.engine || 'openai-chat';
|
|
15044
|
-
if (explicit.concurrency) payload.concurrency = options.concurrency;
|
|
15045
|
-
if (explicit.autoFixRounds) payload.autoFixRounds = options.autoFixRounds;
|
|
15046
|
-
if (explicit.cwd && options.cwd) payload.cwd = options.cwd;
|
|
15047
|
-
if (explicit.threadId && options.threadId) payload.threadId = options.threadId;
|
|
15048
|
-
if (explicit.taskId && options.taskId) payload.taskId = options.taskId;
|
|
15049
|
-
if (explicit.runId && options.runId) payload.runId = options.runId;
|
|
15050
|
-
if (!payload.target && Array.isArray(rest) && rest.length > 0) {
|
|
15051
|
-
payload.target = rest.join(' ').trim();
|
|
15052
|
-
}
|
|
15053
|
-
if (options.planRaw) {
|
|
15054
|
-
payload.plan = parseWorkflowInputArg(options.planRaw);
|
|
15055
|
-
}
|
|
15056
|
-
return payload;
|
|
15057
|
-
}
|
|
15058
|
-
|
|
15059
|
-
function printTaskPlanSummary(plan, warnings = []) {
|
|
15060
|
-
console.log(`\n任务计划: ${plan.title || '(untitled)'}`);
|
|
15061
|
-
console.log(` engine: ${plan.engine || 'openai-chat'}`);
|
|
15062
|
-
console.log(` allowWrite: ${plan.allowWrite === true ? 'yes' : 'no'}`);
|
|
15063
|
-
console.log(` dryRun: ${plan.dryRun === true ? 'yes' : 'no'}`);
|
|
15064
|
-
console.log(` concurrency: ${plan.concurrency || 1}`);
|
|
15065
|
-
if (plan.target) {
|
|
15066
|
-
console.log(` target: ${truncateTaskText(plan.target, 200)}`);
|
|
15067
|
-
}
|
|
15068
|
-
const waves = Array.isArray(plan.waves) ? plan.waves : [];
|
|
15069
|
-
console.log(` waves: ${waves.length}`);
|
|
15070
|
-
for (const wave of waves) {
|
|
15071
|
-
const ids = Array.isArray(wave.nodeIds) ? wave.nodeIds.join(', ') : '';
|
|
15072
|
-
console.log(` - ${wave.label || `Wave ${wave.index + 1}`}: ${ids}`);
|
|
15073
|
-
}
|
|
15074
|
-
const nodes = Array.isArray(plan.nodes) ? plan.nodes : [];
|
|
15075
|
-
for (const node of nodes) {
|
|
15076
|
-
console.log(` - ${node.id} [${node.kind}] ${node.title || ''}`.trim());
|
|
15077
|
-
if (node.workflowId) {
|
|
15078
|
-
console.log(` workflowId: ${node.workflowId}`);
|
|
15079
|
-
}
|
|
15080
|
-
if (Array.isArray(node.dependsOn) && node.dependsOn.length > 0) {
|
|
15081
|
-
console.log(` dependsOn: ${node.dependsOn.join(', ')}`);
|
|
15082
|
-
}
|
|
15083
|
-
}
|
|
15084
|
-
if (Array.isArray(warnings) && warnings.length > 0) {
|
|
15085
|
-
console.log(' warnings:');
|
|
15086
|
-
warnings.forEach((item) => console.log(` - ${item}`));
|
|
15087
|
-
}
|
|
15088
|
-
console.log();
|
|
14569
|
+
function shellEscapePosixArg(value) {
|
|
14570
|
+
const text = value === undefined || value === null ? '' : String(value);
|
|
14571
|
+
return `'${text.replace(/'/g, `'\"'\"'`)}'`;
|
|
15089
14572
|
}
|
|
15090
14573
|
|
|
15091
|
-
|
|
15092
|
-
|
|
15093
|
-
|
|
15094
|
-
|
|
15095
|
-
|
|
15096
|
-
|
|
15097
|
-
console.log(` duration: ${run.durationMs || 0}ms`);
|
|
15098
|
-
if (run.summary) {
|
|
15099
|
-
console.log(` summary: ${run.summary}`);
|
|
14574
|
+
// #region buildScriptCommandArgs
|
|
14575
|
+
function buildScriptCommandArgs(commandLine) {
|
|
14576
|
+
const platform = process.platform;
|
|
14577
|
+
// util-linux script needs -e/--return to propagate child exit code.
|
|
14578
|
+
if (platform === 'linux' || platform === 'android') {
|
|
14579
|
+
return ['-q', '-e', '-c', commandLine, '/dev/null'];
|
|
15100
14580
|
}
|
|
15101
|
-
|
|
15102
|
-
|
|
14581
|
+
// NetBSD supports -e/-c, matching util-linux style contract.
|
|
14582
|
+
if (platform === 'netbsd') {
|
|
14583
|
+
return ['-q', '-e', '-c', commandLine, '/dev/null'];
|
|
15103
14584
|
}
|
|
15104
|
-
|
|
15105
|
-
|
|
15106
|
-
|
|
15107
|
-
if (node.summary) {
|
|
15108
|
-
console.log(` ${node.summary}`);
|
|
15109
|
-
}
|
|
15110
|
-
if (node.error && node.error !== node.summary) {
|
|
15111
|
-
console.log(` error: ${node.error}`);
|
|
15112
|
-
}
|
|
15113
|
-
}
|
|
15114
|
-
console.log();
|
|
15115
|
-
}
|
|
15116
|
-
|
|
15117
|
-
async function cmdTask(args = []) {
|
|
15118
|
-
const argv = Array.isArray(args) ? args : [];
|
|
15119
|
-
if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
|
|
15120
|
-
printTaskHelp();
|
|
15121
|
-
return;
|
|
15122
|
-
}
|
|
15123
|
-
const subcommand = String(argv[0] || '').trim().toLowerCase();
|
|
15124
|
-
const parsed = parseTaskCliOptions(argv.slice(1));
|
|
15125
|
-
const options = parsed.options;
|
|
15126
|
-
const rest = parsed.rest;
|
|
15127
|
-
|
|
15128
|
-
if (subcommand === 'plan') {
|
|
15129
|
-
const payload = buildTaskCliPayload(options, rest);
|
|
15130
|
-
const plan = coerceTaskPlanPayload(payload);
|
|
15131
|
-
const validation = validatePreparedTaskPlan(plan);
|
|
15132
|
-
const result = {
|
|
15133
|
-
ok: validation.ok,
|
|
15134
|
-
plan,
|
|
15135
|
-
issues: validation.issues || [],
|
|
15136
|
-
warnings: validation.warnings || []
|
|
15137
|
-
};
|
|
15138
|
-
if (options.json) {
|
|
15139
|
-
console.log(JSON.stringify(result, null, 2));
|
|
15140
|
-
} else {
|
|
15141
|
-
if (!validation.ok) {
|
|
15142
|
-
throw new Error(validation.error || 'task plan validation failed');
|
|
15143
|
-
}
|
|
15144
|
-
printTaskPlanSummary(plan, validation.warnings || []);
|
|
15145
|
-
}
|
|
15146
|
-
if (!validation.ok) {
|
|
15147
|
-
throw new Error(validation.error || 'task plan validation failed');
|
|
15148
|
-
}
|
|
15149
|
-
return;
|
|
15150
|
-
}
|
|
15151
|
-
|
|
15152
|
-
if (subcommand === 'runs') {
|
|
15153
|
-
const limit = Number.isFinite(options.limit) ? Math.max(1, Math.floor(options.limit)) : 20;
|
|
15154
|
-
const runs = listTaskRunRecords(limit);
|
|
15155
|
-
if (options.json) {
|
|
15156
|
-
console.log(JSON.stringify({ runs, limit }, null, 2));
|
|
15157
|
-
return;
|
|
15158
|
-
}
|
|
15159
|
-
console.log(`\n最近任务运行(${runs.length}/${limit}):`);
|
|
15160
|
-
for (const item of runs) {
|
|
15161
|
-
console.log(` - [${item.status || 'unknown'}] ${item.title || item.taskId || ''} runId=${item.runId || ''} duration=${item.durationMs || 0}ms`);
|
|
15162
|
-
if (item.summary) {
|
|
15163
|
-
console.log(` ${item.summary}`);
|
|
15164
|
-
}
|
|
15165
|
-
if (item.error) {
|
|
15166
|
-
console.log(` error: ${item.error}`);
|
|
15167
|
-
}
|
|
15168
|
-
}
|
|
15169
|
-
console.log();
|
|
15170
|
-
return;
|
|
15171
|
-
}
|
|
15172
|
-
|
|
15173
|
-
if (subcommand === 'queue') {
|
|
15174
|
-
const queueSubcommand = String(rest[0] || '').trim().toLowerCase();
|
|
15175
|
-
const tail = rest.slice(1);
|
|
15176
|
-
if (!queueSubcommand) {
|
|
15177
|
-
throw new Error('queue 子命令不能为空');
|
|
15178
|
-
}
|
|
15179
|
-
if (queueSubcommand === 'add') {
|
|
15180
|
-
const payload = buildTaskCliPayload(options, tail);
|
|
15181
|
-
const result = addTaskToQueue(payload);
|
|
15182
|
-
if (options.json) {
|
|
15183
|
-
console.log(JSON.stringify(result, null, 2));
|
|
15184
|
-
} else {
|
|
15185
|
-
if (result.error) {
|
|
15186
|
-
throw new Error(result.error);
|
|
15187
|
-
}
|
|
15188
|
-
console.log(`✓ 已加入队列: ${result.task.taskId}`);
|
|
15189
|
-
console.log(` ${result.task.title || result.task.target || ''}`);
|
|
15190
|
-
console.log();
|
|
15191
|
-
}
|
|
15192
|
-
if (result.error) {
|
|
15193
|
-
throw new Error(result.error);
|
|
15194
|
-
}
|
|
15195
|
-
return;
|
|
15196
|
-
}
|
|
15197
|
-
if (queueSubcommand === 'list') {
|
|
15198
|
-
const limit = Number.isFinite(options.limit) ? Math.max(1, Math.floor(options.limit)) : 20;
|
|
15199
|
-
const tasks = listTaskQueueItems({ limit, status: options.status || '' });
|
|
15200
|
-
if (options.json) {
|
|
15201
|
-
console.log(JSON.stringify({ tasks, limit }, null, 2));
|
|
15202
|
-
return;
|
|
15203
|
-
}
|
|
15204
|
-
console.log(`\n任务队列(${tasks.length}/${limit}):`);
|
|
15205
|
-
for (const item of tasks) {
|
|
15206
|
-
console.log(` - [${item.status}] ${item.taskId} ${item.title || item.target || ''}`.trim());
|
|
15207
|
-
if (item.lastSummary) {
|
|
15208
|
-
console.log(` ${item.lastSummary}`);
|
|
15209
|
-
}
|
|
15210
|
-
}
|
|
15211
|
-
console.log();
|
|
15212
|
-
return;
|
|
15213
|
-
}
|
|
15214
|
-
if (queueSubcommand === 'show') {
|
|
15215
|
-
const taskId = options.taskId || String(tail[0] || '').trim();
|
|
15216
|
-
if (!taskId) {
|
|
15217
|
-
throw new Error('taskId is required');
|
|
15218
|
-
}
|
|
15219
|
-
const task = getTaskQueueItem(taskId);
|
|
15220
|
-
if (!task) {
|
|
15221
|
-
throw new Error(`task not found: ${taskId}`);
|
|
15222
|
-
}
|
|
15223
|
-
console.log(JSON.stringify(task, null, 2));
|
|
15224
|
-
return;
|
|
15225
|
-
}
|
|
15226
|
-
if (queueSubcommand === 'start') {
|
|
15227
|
-
const taskId = options.taskId || String(tail[0] || '').trim();
|
|
15228
|
-
const result = await startTaskQueueProcessing({ taskId, detach: options.detach });
|
|
15229
|
-
if (options.json) {
|
|
15230
|
-
console.log(JSON.stringify(result, null, 2));
|
|
15231
|
-
} else if (result.error) {
|
|
15232
|
-
throw new Error(result.error);
|
|
15233
|
-
} else if (result.detached) {
|
|
15234
|
-
console.log('✓ 队列处理已在后台启动');
|
|
15235
|
-
console.log();
|
|
15236
|
-
} else if (result.detail) {
|
|
15237
|
-
printTaskRunSummary(result.detail);
|
|
15238
|
-
} else {
|
|
15239
|
-
console.log('队列中暂无可执行任务');
|
|
15240
|
-
console.log();
|
|
15241
|
-
}
|
|
15242
|
-
if (result.error) {
|
|
15243
|
-
throw new Error(result.error);
|
|
15244
|
-
}
|
|
15245
|
-
return;
|
|
15246
|
-
}
|
|
15247
|
-
throw new Error(`未知 queue 子命令: ${queueSubcommand}`);
|
|
15248
|
-
}
|
|
15249
|
-
|
|
15250
|
-
if (subcommand === 'run') {
|
|
15251
|
-
const payload = buildTaskCliPayload(options, rest);
|
|
15252
|
-
if (options.planOnly) {
|
|
15253
|
-
const plan = coerceTaskPlanPayload(payload);
|
|
15254
|
-
const validation = validatePreparedTaskPlan(plan);
|
|
15255
|
-
const result = {
|
|
15256
|
-
ok: validation.ok,
|
|
15257
|
-
plan,
|
|
15258
|
-
issues: validation.issues || [],
|
|
15259
|
-
warnings: validation.warnings || []
|
|
15260
|
-
};
|
|
15261
|
-
if (options.json) {
|
|
15262
|
-
console.log(JSON.stringify(result, null, 2));
|
|
15263
|
-
} else {
|
|
15264
|
-
if (!validation.ok) {
|
|
15265
|
-
throw new Error(validation.error || 'task plan validation failed');
|
|
15266
|
-
}
|
|
15267
|
-
printTaskPlanSummary(plan, validation.warnings || []);
|
|
15268
|
-
}
|
|
15269
|
-
if (!validation.ok) {
|
|
15270
|
-
throw new Error(validation.error || 'task plan validation failed');
|
|
15271
|
-
}
|
|
15272
|
-
return;
|
|
15273
|
-
}
|
|
15274
|
-
if (options.detach) {
|
|
15275
|
-
const plan = coerceTaskPlanPayload(payload);
|
|
15276
|
-
const validation = validatePreparedTaskPlan(plan);
|
|
15277
|
-
if (!validation.ok) {
|
|
15278
|
-
throw new Error(validation.error || 'task plan validation failed');
|
|
15279
|
-
}
|
|
15280
|
-
const taskId = options.taskId || createTaskId();
|
|
15281
|
-
const runId = createTaskRunId();
|
|
15282
|
-
spawnDetachedTaskWorker({
|
|
15283
|
-
type: 'run-plan',
|
|
15284
|
-
plan,
|
|
15285
|
-
taskId,
|
|
15286
|
-
runId
|
|
15287
|
-
});
|
|
15288
|
-
const result = { ok: true, detached: true, taskId, runId, warnings: validation.warnings || [] };
|
|
15289
|
-
if (options.json) {
|
|
15290
|
-
console.log(JSON.stringify(result, null, 2));
|
|
15291
|
-
} else {
|
|
15292
|
-
console.log(`✓ 后台任务已启动: taskId=${taskId} runId=${runId}`);
|
|
15293
|
-
console.log();
|
|
15294
|
-
}
|
|
15295
|
-
return;
|
|
15296
|
-
}
|
|
15297
|
-
const detail = await runTaskNow(payload);
|
|
15298
|
-
if (options.json) {
|
|
15299
|
-
console.log(JSON.stringify(detail, null, 2));
|
|
15300
|
-
} else {
|
|
15301
|
-
printTaskRunSummary(detail);
|
|
15302
|
-
}
|
|
15303
|
-
if (detail.error || (detail.run && detail.run.status && detail.run.status !== 'success')) {
|
|
15304
|
-
throw new Error(detail.error || (detail.run && detail.run.error) || 'task run failed');
|
|
15305
|
-
}
|
|
15306
|
-
return;
|
|
15307
|
-
}
|
|
15308
|
-
|
|
15309
|
-
if (subcommand === 'retry') {
|
|
15310
|
-
const runId = options.runId || String(rest[0] || '').trim();
|
|
15311
|
-
if (options.detach) {
|
|
15312
|
-
const detail = readTaskRunDetail(runId);
|
|
15313
|
-
if (!detail || !detail.plan) {
|
|
15314
|
-
throw new Error(`task run not found: ${runId}`);
|
|
15315
|
-
}
|
|
15316
|
-
const nextRunId = createTaskRunId();
|
|
15317
|
-
spawnDetachedTaskWorker({
|
|
15318
|
-
type: 'run-plan',
|
|
15319
|
-
plan: cloneJson(detail.plan, {}),
|
|
15320
|
-
taskId: detail.taskId || createTaskId(),
|
|
15321
|
-
runId: nextRunId
|
|
15322
|
-
});
|
|
15323
|
-
const result = {
|
|
15324
|
-
ok: true,
|
|
15325
|
-
started: true,
|
|
15326
|
-
detached: true,
|
|
15327
|
-
runId: nextRunId,
|
|
15328
|
-
taskId: detail.taskId || ''
|
|
15329
|
-
};
|
|
15330
|
-
if (options.json) {
|
|
15331
|
-
console.log(JSON.stringify(result, null, 2));
|
|
15332
|
-
} else {
|
|
15333
|
-
console.log(`✓ 已后台重试: runId=${result.runId}`);
|
|
15334
|
-
console.log();
|
|
15335
|
-
}
|
|
15336
|
-
return;
|
|
15337
|
-
}
|
|
15338
|
-
const result = await retryTaskRun({ runId, detach: options.detach });
|
|
15339
|
-
if (options.json) {
|
|
15340
|
-
console.log(JSON.stringify(result, null, 2));
|
|
15341
|
-
} else if (result.detached) {
|
|
15342
|
-
console.log(`✓ 已后台重试: runId=${result.runId}`);
|
|
15343
|
-
console.log();
|
|
15344
|
-
} else {
|
|
15345
|
-
printTaskRunSummary(result);
|
|
15346
|
-
}
|
|
15347
|
-
if (result.error) {
|
|
15348
|
-
throw new Error(result.error);
|
|
15349
|
-
}
|
|
15350
|
-
if (!result.detached && result.run && result.run.status === 'failed') {
|
|
15351
|
-
throw new Error(result.run.error || 'task retry failed');
|
|
15352
|
-
}
|
|
15353
|
-
return;
|
|
15354
|
-
}
|
|
15355
|
-
|
|
15356
|
-
if (subcommand === 'cancel') {
|
|
15357
|
-
const result = cancelTaskRunOrQueue({
|
|
15358
|
-
target: String(rest[0] || '').trim(),
|
|
15359
|
-
taskId: options.taskId,
|
|
15360
|
-
runId: options.runId
|
|
15361
|
-
});
|
|
15362
|
-
if (options.json) {
|
|
15363
|
-
console.log(JSON.stringify(result, null, 2));
|
|
15364
|
-
} else {
|
|
15365
|
-
if (result.error) {
|
|
15366
|
-
throw new Error(result.error);
|
|
15367
|
-
}
|
|
15368
|
-
console.log('✓ 已发出取消请求');
|
|
15369
|
-
console.log();
|
|
15370
|
-
}
|
|
15371
|
-
if (result.error) {
|
|
15372
|
-
throw new Error(result.error);
|
|
15373
|
-
}
|
|
15374
|
-
return;
|
|
15375
|
-
}
|
|
15376
|
-
|
|
15377
|
-
if (subcommand === 'logs') {
|
|
15378
|
-
const runId = options.runId || String(rest[0] || '').trim();
|
|
15379
|
-
const result = getTaskLogs({ runId });
|
|
15380
|
-
if (result.error) {
|
|
15381
|
-
throw new Error(result.error);
|
|
15382
|
-
}
|
|
15383
|
-
if (options.json) {
|
|
15384
|
-
console.log(JSON.stringify(result, null, 2));
|
|
15385
|
-
} else {
|
|
15386
|
-
console.log(result.logs || '(no logs)');
|
|
15387
|
-
console.log();
|
|
15388
|
-
}
|
|
15389
|
-
return;
|
|
15390
|
-
}
|
|
15391
|
-
|
|
15392
|
-
throw new Error(`未知 task 子命令: ${subcommand}`);
|
|
15393
|
-
}
|
|
15394
|
-
|
|
15395
|
-
// #region parseCodexProxyOptions
|
|
15396
|
-
function parseCodexProxyOptions(args = []) {
|
|
15397
|
-
const options = {
|
|
15398
|
-
passthroughArgs: [],
|
|
15399
|
-
queuedFollowUps: []
|
|
15400
|
-
};
|
|
15401
|
-
const argv = Array.isArray(args) ? args : [];
|
|
15402
|
-
|
|
15403
|
-
const pushFollowUp = (value, optionName) => {
|
|
15404
|
-
const raw = value === undefined || value === null ? '' : String(value);
|
|
15405
|
-
if (!raw.trim()) {
|
|
15406
|
-
throw new Error(`${optionName} 需要提供非空内容`);
|
|
15407
|
-
}
|
|
15408
|
-
options.queuedFollowUps.push(raw);
|
|
15409
|
-
};
|
|
15410
|
-
|
|
15411
|
-
for (let i = 0; i < argv.length; i++) {
|
|
15412
|
-
const arg = argv[i];
|
|
15413
|
-
if (arg === undefined || arg === null) {
|
|
15414
|
-
continue;
|
|
15415
|
-
}
|
|
15416
|
-
const text = String(arg);
|
|
15417
|
-
if (text === '--') {
|
|
15418
|
-
options.passthroughArgs.push(...argv.slice(i).map((item) => String(item)));
|
|
15419
|
-
break;
|
|
15420
|
-
}
|
|
15421
|
-
if (text === '--queued-follow-up' || text === '--follow-up') {
|
|
15422
|
-
const next = argv[i + 1];
|
|
15423
|
-
if (next === undefined) {
|
|
15424
|
-
throw new Error(`${text} 需要提供内容`);
|
|
15425
|
-
}
|
|
15426
|
-
pushFollowUp(next, text);
|
|
15427
|
-
i += 1;
|
|
15428
|
-
continue;
|
|
15429
|
-
}
|
|
15430
|
-
if (text.startsWith('--queued-follow-up=')) {
|
|
15431
|
-
pushFollowUp(text.slice('--queued-follow-up='.length), '--queued-follow-up');
|
|
15432
|
-
continue;
|
|
15433
|
-
}
|
|
15434
|
-
if (text.startsWith('--follow-up=')) {
|
|
15435
|
-
pushFollowUp(text.slice('--follow-up='.length), '--follow-up');
|
|
15436
|
-
continue;
|
|
15437
|
-
}
|
|
15438
|
-
options.passthroughArgs.push(text);
|
|
15439
|
-
}
|
|
15440
|
-
|
|
15441
|
-
return options;
|
|
15442
|
-
}
|
|
15443
|
-
// #endregion parseCodexProxyOptions
|
|
15444
|
-
|
|
15445
|
-
function shellEscapePosixArg(value) {
|
|
15446
|
-
const text = value === undefined || value === null ? '' : String(value);
|
|
15447
|
-
return `'${text.replace(/'/g, `'\"'\"'`)}'`;
|
|
15448
|
-
}
|
|
15449
|
-
|
|
15450
|
-
// #region buildScriptCommandArgs
|
|
15451
|
-
function buildScriptCommandArgs(commandLine) {
|
|
15452
|
-
const platform = process.platform;
|
|
15453
|
-
// util-linux script needs -e/--return to propagate child exit code.
|
|
15454
|
-
if (platform === 'linux' || platform === 'android') {
|
|
15455
|
-
return ['-q', '-e', '-c', commandLine, '/dev/null'];
|
|
15456
|
-
}
|
|
15457
|
-
// NetBSD supports -e/-c, matching util-linux style contract.
|
|
15458
|
-
if (platform === 'netbsd') {
|
|
15459
|
-
return ['-q', '-e', '-c', commandLine, '/dev/null'];
|
|
15460
|
-
}
|
|
15461
|
-
// OpenBSD supports "-c <command>" with a trailing output file path.
|
|
15462
|
-
if (platform === 'openbsd') {
|
|
15463
|
-
return ['-c', commandLine, '/dev/null'];
|
|
14585
|
+
// OpenBSD supports "-c <command>" with a trailing output file path.
|
|
14586
|
+
if (platform === 'openbsd') {
|
|
14587
|
+
return ['-c', commandLine, '/dev/null'];
|
|
15464
14588
|
}
|
|
15465
14589
|
// BSD/macOS script does not support util-linux "-c <cmd>" syntax.
|
|
15466
14590
|
if (platform === 'darwin' || platform === 'freebsd') {
|
|
@@ -16506,1760 +15630,6 @@ async function runWorkflowById(workflowId, input = {}, options = {}) {
|
|
|
16506
15630
|
};
|
|
16507
15631
|
}
|
|
16508
15632
|
|
|
16509
|
-
function createTaskId() {
|
|
16510
|
-
return `task-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
|
|
16511
|
-
}
|
|
16512
|
-
|
|
16513
|
-
function createTaskRunId() {
|
|
16514
|
-
return `tr-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
|
|
16515
|
-
}
|
|
16516
|
-
|
|
16517
|
-
function createTaskThreadId() {
|
|
16518
|
-
return `tt-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
|
|
16519
|
-
}
|
|
16520
|
-
|
|
16521
|
-
function validateTaskRunId(value) {
|
|
16522
|
-
const runId = typeof value === 'string' ? value.trim() : '';
|
|
16523
|
-
if (!runId) {
|
|
16524
|
-
return { ok: false, error: 'runId is required', runId: '' };
|
|
16525
|
-
}
|
|
16526
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(runId)) {
|
|
16527
|
-
return { ok: false, error: 'runId contains unsupported characters', runId: '' };
|
|
16528
|
-
}
|
|
16529
|
-
return { ok: true, error: '', runId };
|
|
16530
|
-
}
|
|
16531
|
-
|
|
16532
|
-
function normalizeTaskThreadId(value) {
|
|
16533
|
-
const threadId = typeof value === 'string' ? value.trim() : '';
|
|
16534
|
-
if (!threadId) {
|
|
16535
|
-
return '';
|
|
16536
|
-
}
|
|
16537
|
-
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(threadId) ? threadId : '';
|
|
16538
|
-
}
|
|
16539
|
-
|
|
16540
|
-
function normalizeTaskEngine(value) {
|
|
16541
|
-
const normalized = typeof value === 'string' ? value.trim().toLowerCase().replace(/[\s_/]+/g, '-') : '';
|
|
16542
|
-
if (normalized === 'workflow') return 'workflow';
|
|
16543
|
-
// Legacy task plans used `codex`; new task execution uses the configured OpenAI Chat-compatible provider.
|
|
16544
|
-
return 'openai-chat';
|
|
16545
|
-
}
|
|
16546
|
-
|
|
16547
|
-
function normalizeTaskFollowUps(input = []) {
|
|
16548
|
-
const seen = new Set();
|
|
16549
|
-
const result = [];
|
|
16550
|
-
for (const item of Array.isArray(input) ? input : []) {
|
|
16551
|
-
const text = typeof item === 'string' ? item.trim() : '';
|
|
16552
|
-
if (!text || seen.has(text)) continue;
|
|
16553
|
-
seen.add(text);
|
|
16554
|
-
result.push(text);
|
|
16555
|
-
}
|
|
16556
|
-
return result;
|
|
16557
|
-
}
|
|
16558
|
-
|
|
16559
|
-
function buildTaskWorkflowCatalog() {
|
|
16560
|
-
const listed = listWorkflowDefinitions();
|
|
16561
|
-
return {
|
|
16562
|
-
workflows: Array.isArray(listed.workflows)
|
|
16563
|
-
? listed.workflows.map((item) => ({
|
|
16564
|
-
id: item.id,
|
|
16565
|
-
name: item.name,
|
|
16566
|
-
description: item.description,
|
|
16567
|
-
readOnly: item.readOnly !== false,
|
|
16568
|
-
stepCount: item.stepCount || 0
|
|
16569
|
-
}))
|
|
16570
|
-
: [],
|
|
16571
|
-
warnings: Array.isArray(listed.warnings) ? listed.warnings : []
|
|
16572
|
-
};
|
|
16573
|
-
}
|
|
16574
|
-
|
|
16575
|
-
function normalizeTaskPlanRequest(params = {}) {
|
|
16576
|
-
const source = params && typeof params === 'object' ? params : {};
|
|
16577
|
-
const rawWorkflowIds = Array.isArray(source.workflowIds)
|
|
16578
|
-
? source.workflowIds
|
|
16579
|
-
: (typeof source.workflowId === 'string' && source.workflowId.trim() ? [source.workflowId.trim()] : []);
|
|
16580
|
-
const rawFollowUps = Array.isArray(source.followUps)
|
|
16581
|
-
? source.followUps
|
|
16582
|
-
: (typeof source.followUp === 'string' && source.followUp.trim() ? [source.followUp.trim()] : []);
|
|
16583
|
-
return {
|
|
16584
|
-
id: typeof source.id === 'string' ? source.id.trim() : '',
|
|
16585
|
-
threadId: normalizeTaskThreadId(source.threadId || source.conversationId || source.sessionId),
|
|
16586
|
-
title: typeof source.title === 'string' ? source.title.trim() : '',
|
|
16587
|
-
target: typeof source.target === 'string' ? source.target.trim() : '',
|
|
16588
|
-
notes: typeof source.notes === 'string' ? source.notes.trim() : '',
|
|
16589
|
-
cwd: typeof source.cwd === 'string' ? source.cwd.trim() : process.cwd(),
|
|
16590
|
-
engine: normalizeTaskEngine(source.engine),
|
|
16591
|
-
allowWrite: source.allowWrite === true,
|
|
16592
|
-
dryRun: source.dryRun === true,
|
|
16593
|
-
concurrency: Number.isFinite(source.concurrency) ? source.concurrency : parseInt(source.concurrency, 10),
|
|
16594
|
-
autoFixRounds: Number.isFinite(source.autoFixRounds) ? source.autoFixRounds : parseInt(source.autoFixRounds, 10),
|
|
16595
|
-
previewOnly: source.previewOnly === true,
|
|
16596
|
-
workflowIds: rawWorkflowIds,
|
|
16597
|
-
followUps: normalizeTaskFollowUps(rawFollowUps)
|
|
16598
|
-
};
|
|
16599
|
-
}
|
|
16600
|
-
|
|
16601
|
-
function coerceTaskPlanPayload(params = {}) {
|
|
16602
|
-
if (params && params.plan && typeof params.plan === 'object' && !Array.isArray(params.plan)) {
|
|
16603
|
-
const plan = cloneJson(params.plan, {});
|
|
16604
|
-
const overrideKeys = ['id', 'threadId', 'conversationId', 'sessionId', 'title', 'target', 'notes', 'cwd', 'engine', 'allowWrite', 'dryRun', 'concurrency', 'autoFixRounds', 'previewOnly', 'workflowIds', 'followUps'];
|
|
16605
|
-
for (const key of overrideKeys) {
|
|
16606
|
-
if (Object.prototype.hasOwnProperty.call(params, key) && params[key] !== undefined) {
|
|
16607
|
-
if (key === 'conversationId' || key === 'sessionId') {
|
|
16608
|
-
plan.threadId = cloneJson(params[key], params[key]);
|
|
16609
|
-
} else {
|
|
16610
|
-
plan[key] = cloneJson(params[key], params[key]);
|
|
16611
|
-
}
|
|
16612
|
-
}
|
|
16613
|
-
}
|
|
16614
|
-
plan.cwd = typeof plan.cwd === 'string' && plan.cwd.trim() ? plan.cwd.trim() : process.cwd();
|
|
16615
|
-
plan.threadId = normalizeTaskThreadId(plan.threadId) || createTaskThreadId();
|
|
16616
|
-
plan.engine = normalizeTaskEngine(plan.engine);
|
|
16617
|
-
plan.workflowIds = normalizeTaskFollowUps(plan.workflowIds || []).map((id) => normalizeWorkflowId(id)).filter(Boolean);
|
|
16618
|
-
plan.followUps = normalizeTaskFollowUps(plan.followUps || []);
|
|
16619
|
-
plan.waves = computePlanWaves(Array.isArray(plan.nodes) ? plan.nodes : []);
|
|
16620
|
-
return plan;
|
|
16621
|
-
}
|
|
16622
|
-
const request = normalizeTaskPlanRequest(params || {});
|
|
16623
|
-
const catalog = buildTaskWorkflowCatalog();
|
|
16624
|
-
const plan = buildTaskPlan(request, {
|
|
16625
|
-
workflowCatalog: catalog.workflows,
|
|
16626
|
-
cwd: request.cwd || process.cwd()
|
|
16627
|
-
});
|
|
16628
|
-
return {
|
|
16629
|
-
...plan,
|
|
16630
|
-
threadId: request.threadId || createTaskThreadId(),
|
|
16631
|
-
engine: normalizeTaskEngine(request.engine || plan.engine)
|
|
16632
|
-
};
|
|
16633
|
-
}
|
|
16634
|
-
|
|
16635
|
-
function validatePreparedTaskPlan(plan) {
|
|
16636
|
-
const catalog = buildTaskWorkflowCatalog();
|
|
16637
|
-
const validation = validateTaskPlan(plan, {
|
|
16638
|
-
workflowCatalog: catalog.workflows
|
|
16639
|
-
});
|
|
16640
|
-
return {
|
|
16641
|
-
...validation,
|
|
16642
|
-
warnings: catalog.warnings || []
|
|
16643
|
-
};
|
|
16644
|
-
}
|
|
16645
|
-
|
|
16646
|
-
function normalizeTaskQueueItem(raw = {}) {
|
|
16647
|
-
const plan = raw.plan && typeof raw.plan === 'object' && !Array.isArray(raw.plan)
|
|
16648
|
-
? cloneJson(raw.plan, {})
|
|
16649
|
-
: {};
|
|
16650
|
-
const taskId = typeof raw.taskId === 'string' ? raw.taskId.trim() : '';
|
|
16651
|
-
return {
|
|
16652
|
-
taskId: taskId || createTaskId(),
|
|
16653
|
-
threadId: normalizeTaskThreadId(raw.threadId || plan.threadId) || createTaskThreadId(),
|
|
16654
|
-
title: typeof raw.title === 'string' ? raw.title.trim() : (typeof plan.title === 'string' ? plan.title.trim() : ''),
|
|
16655
|
-
target: typeof raw.target === 'string' ? raw.target.trim() : (typeof plan.target === 'string' ? plan.target.trim() : ''),
|
|
16656
|
-
cwd: typeof raw.cwd === 'string' && raw.cwd.trim() ? raw.cwd.trim() : (typeof plan.cwd === 'string' ? plan.cwd.trim() : ''),
|
|
16657
|
-
status: typeof raw.status === 'string' ? raw.status.trim().toLowerCase() : 'queued',
|
|
16658
|
-
createdAt: toIsoTime(raw.createdAt || Date.now(), ''),
|
|
16659
|
-
updatedAt: toIsoTime(raw.updatedAt || raw.createdAt || Date.now(), ''),
|
|
16660
|
-
engine: normalizeTaskEngine(raw.engine || plan.engine),
|
|
16661
|
-
allowWrite: raw.allowWrite === true || plan.allowWrite === true,
|
|
16662
|
-
dryRun: raw.dryRun === true || plan.dryRun === true,
|
|
16663
|
-
concurrency: Number.isFinite(raw.concurrency) ? raw.concurrency : (Number.isFinite(plan.concurrency) ? plan.concurrency : 2),
|
|
16664
|
-
autoFixRounds: Number.isFinite(raw.autoFixRounds) ? raw.autoFixRounds : (Number.isFinite(plan.autoFixRounds) ? plan.autoFixRounds : 1),
|
|
16665
|
-
lastRunId: typeof raw.lastRunId === 'string' ? raw.lastRunId.trim() : '',
|
|
16666
|
-
lastSummary: typeof raw.lastSummary === 'string' ? raw.lastSummary.trim() : '',
|
|
16667
|
-
plan,
|
|
16668
|
-
runStatus: typeof raw.runStatus === 'string' ? raw.runStatus.trim().toLowerCase() : ''
|
|
16669
|
-
};
|
|
16670
|
-
}
|
|
16671
|
-
|
|
16672
|
-
function readTaskQueueState() {
|
|
16673
|
-
const parsed = readJsonObjectFromFile(TASK_QUEUE_FILE, {});
|
|
16674
|
-
if (!parsed.ok && parsed.exists) {
|
|
16675
|
-
return {
|
|
16676
|
-
tasks: [],
|
|
16677
|
-
error: parsed.error || 'failed to read task queue'
|
|
16678
|
-
};
|
|
16679
|
-
}
|
|
16680
|
-
if (!parsed.ok || !parsed.exists) {
|
|
16681
|
-
return {
|
|
16682
|
-
tasks: [],
|
|
16683
|
-
error: ''
|
|
16684
|
-
};
|
|
16685
|
-
}
|
|
16686
|
-
const source = parsed.data && typeof parsed.data === 'object' ? parsed.data : {};
|
|
16687
|
-
const tasks = Array.isArray(source.tasks) ? source.tasks.map((item) => normalizeTaskQueueItem(item)) : [];
|
|
16688
|
-
return { tasks, error: '' };
|
|
16689
|
-
}
|
|
16690
|
-
|
|
16691
|
-
function writeTaskQueueState(state = {}) {
|
|
16692
|
-
ensureDir(path.dirname(TASK_QUEUE_FILE));
|
|
16693
|
-
writeJsonAtomic(TASK_QUEUE_FILE, {
|
|
16694
|
-
tasks: Array.isArray(state.tasks) ? state.tasks.map((item) => normalizeTaskQueueItem(item)) : []
|
|
16695
|
-
});
|
|
16696
|
-
}
|
|
16697
|
-
|
|
16698
|
-
function withTaskQueueLock(fn) {
|
|
16699
|
-
const lockPath = `${TASK_QUEUE_FILE}.lock`;
|
|
16700
|
-
ensureDir(path.dirname(lockPath));
|
|
16701
|
-
let lockFd = null;
|
|
16702
|
-
try {
|
|
16703
|
-
lockFd = fs.openSync(lockPath, 'wx', 0o600);
|
|
16704
|
-
} catch (error) {
|
|
16705
|
-
const code = error && error.code ? error.code : '';
|
|
16706
|
-
if (code === 'EEXIST') {
|
|
16707
|
-
try {
|
|
16708
|
-
const stat = fs.statSync(lockPath);
|
|
16709
|
-
const ageMs = Date.now() - stat.mtimeMs;
|
|
16710
|
-
if (ageMs > 5000) {
|
|
16711
|
-
try {
|
|
16712
|
-
fs.unlinkSync(lockPath);
|
|
16713
|
-
} catch (_) { }
|
|
16714
|
-
lockFd = fs.openSync(lockPath, 'wx', 0o600);
|
|
16715
|
-
}
|
|
16716
|
-
} catch (_) { }
|
|
16717
|
-
}
|
|
16718
|
-
}
|
|
16719
|
-
if (!lockFd) {
|
|
16720
|
-
return { error: 'task queue is busy' };
|
|
16721
|
-
}
|
|
16722
|
-
try {
|
|
16723
|
-
return fn();
|
|
16724
|
-
} finally {
|
|
16725
|
-
try {
|
|
16726
|
-
fs.closeSync(lockFd);
|
|
16727
|
-
} catch (_) { }
|
|
16728
|
-
try {
|
|
16729
|
-
fs.unlinkSync(lockPath);
|
|
16730
|
-
} catch (_) { }
|
|
16731
|
-
}
|
|
16732
|
-
}
|
|
16733
|
-
|
|
16734
|
-
function upsertTaskQueueItem(item) {
|
|
16735
|
-
return withTaskQueueLock(() => {
|
|
16736
|
-
const state = readTaskQueueState();
|
|
16737
|
-
if (state.error) {
|
|
16738
|
-
return { error: state.error };
|
|
16739
|
-
}
|
|
16740
|
-
const next = normalizeTaskQueueItem(item || {});
|
|
16741
|
-
const index = state.tasks.findIndex((entry) => entry.taskId === next.taskId);
|
|
16742
|
-
if (index >= 0) {
|
|
16743
|
-
state.tasks[index] = next;
|
|
16744
|
-
} else {
|
|
16745
|
-
state.tasks.push(next);
|
|
16746
|
-
}
|
|
16747
|
-
writeTaskQueueState(state);
|
|
16748
|
-
return next;
|
|
16749
|
-
});
|
|
16750
|
-
}
|
|
16751
|
-
|
|
16752
|
-
function getTaskQueueItem(taskId) {
|
|
16753
|
-
const id = typeof taskId === 'string' ? taskId.trim() : '';
|
|
16754
|
-
if (!id) return null;
|
|
16755
|
-
return readTaskQueueState().tasks.find((item) => item.taskId === id) || null;
|
|
16756
|
-
}
|
|
16757
|
-
|
|
16758
|
-
function listTaskQueueItems(options = {}) {
|
|
16759
|
-
const state = readTaskQueueState();
|
|
16760
|
-
const limit = Number.isFinite(options.limit) ? Math.max(1, Math.floor(options.limit)) : 50;
|
|
16761
|
-
const statusFilter = typeof options.status === 'string' ? options.status.trim().toLowerCase() : '';
|
|
16762
|
-
const statusRank = {
|
|
16763
|
-
running: 0,
|
|
16764
|
-
queued: 1,
|
|
16765
|
-
failed: 2,
|
|
16766
|
-
completed: 3,
|
|
16767
|
-
cancelled: 4
|
|
16768
|
-
};
|
|
16769
|
-
return state.tasks
|
|
16770
|
-
.filter((item) => !statusFilter || item.status === statusFilter)
|
|
16771
|
-
.sort((a, b) => {
|
|
16772
|
-
const rankDiff = (statusRank[a.status] ?? 99) - (statusRank[b.status] ?? 99);
|
|
16773
|
-
if (rankDiff !== 0) return rankDiff;
|
|
16774
|
-
return String(b.updatedAt || '').localeCompare(String(a.updatedAt || ''));
|
|
16775
|
-
})
|
|
16776
|
-
.slice(0, limit);
|
|
16777
|
-
}
|
|
16778
|
-
|
|
16779
|
-
function appendTaskRunRecord(record) {
|
|
16780
|
-
ensureDir(path.dirname(TASK_RUNS_FILE));
|
|
16781
|
-
fs.appendFileSync(TASK_RUNS_FILE, `${JSON.stringify(record)}\n`, { encoding: 'utf-8', mode: 0o600 });
|
|
16782
|
-
}
|
|
16783
|
-
|
|
16784
|
-
let g_taskRunRecordsLastParseErrors = 0;
|
|
16785
|
-
|
|
16786
|
-
function listTaskRunRecords(limit = 20) {
|
|
16787
|
-
const max = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 20;
|
|
16788
|
-
if (!fs.existsSync(TASK_RUNS_FILE)) {
|
|
16789
|
-
g_taskRunRecordsLastParseErrors = 0;
|
|
16790
|
-
return [];
|
|
16791
|
-
}
|
|
16792
|
-
let content = '';
|
|
16793
|
-
try {
|
|
16794
|
-
content = fs.readFileSync(TASK_RUNS_FILE, 'utf-8');
|
|
16795
|
-
} catch (_) {
|
|
16796
|
-
g_taskRunRecordsLastParseErrors = 0;
|
|
16797
|
-
return [];
|
|
16798
|
-
}
|
|
16799
|
-
const rows = content
|
|
16800
|
-
.split(/\r?\n/g)
|
|
16801
|
-
.map((line) => line.trim())
|
|
16802
|
-
.filter(Boolean);
|
|
16803
|
-
const parsed = [];
|
|
16804
|
-
let parseErrors = 0;
|
|
16805
|
-
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
16806
|
-
try {
|
|
16807
|
-
parsed.push(JSON.parse(rows[i]));
|
|
16808
|
-
if (parsed.length >= max) {
|
|
16809
|
-
break;
|
|
16810
|
-
}
|
|
16811
|
-
} catch (_) {
|
|
16812
|
-
parseErrors += 1;
|
|
16813
|
-
}
|
|
16814
|
-
}
|
|
16815
|
-
g_taskRunRecordsLastParseErrors = parseErrors;
|
|
16816
|
-
return parsed;
|
|
16817
|
-
}
|
|
16818
|
-
|
|
16819
|
-
function getTaskRunDetailPath(runId) {
|
|
16820
|
-
const validation = validateTaskRunId(runId);
|
|
16821
|
-
if (!validation.ok) {
|
|
16822
|
-
return '';
|
|
16823
|
-
}
|
|
16824
|
-
const baseDir = path.resolve(TASK_RUN_DETAILS_DIR);
|
|
16825
|
-
const detailPath = path.resolve(baseDir, `${validation.runId}.json`);
|
|
16826
|
-
if (!(detailPath === baseDir || detailPath.startsWith(`${baseDir}${path.sep}`))) {
|
|
16827
|
-
return '';
|
|
16828
|
-
}
|
|
16829
|
-
return detailPath;
|
|
16830
|
-
}
|
|
16831
|
-
|
|
16832
|
-
function writeTaskRunDetail(detail = {}) {
|
|
16833
|
-
const detailPath = getTaskRunDetailPath(detail.runId);
|
|
16834
|
-
if (!detailPath) return;
|
|
16835
|
-
ensureDir(path.dirname(detailPath));
|
|
16836
|
-
writeJsonAtomic(detailPath, detail);
|
|
16837
|
-
}
|
|
16838
|
-
|
|
16839
|
-
function readTaskRunDetail(runId) {
|
|
16840
|
-
const detailPath = getTaskRunDetailPath(runId);
|
|
16841
|
-
if (!detailPath) {
|
|
16842
|
-
return null;
|
|
16843
|
-
}
|
|
16844
|
-
const parsed = readJsonObjectFromFile(detailPath, {});
|
|
16845
|
-
if (!parsed.ok || !parsed.exists) {
|
|
16846
|
-
return null;
|
|
16847
|
-
}
|
|
16848
|
-
return parsed.data && typeof parsed.data === 'object' ? parsed.data : null;
|
|
16849
|
-
}
|
|
16850
|
-
|
|
16851
|
-
function collectTaskRunSummary(detail = {}) {
|
|
16852
|
-
const run = detail.run && typeof detail.run === 'object' ? detail.run : {};
|
|
16853
|
-
const nodes = Array.isArray(run.nodes) ? run.nodes : [];
|
|
16854
|
-
return {
|
|
16855
|
-
runId: detail.runId || '',
|
|
16856
|
-
taskId: detail.taskId || '',
|
|
16857
|
-
threadId: detail.threadId || '',
|
|
16858
|
-
title: detail.title || '',
|
|
16859
|
-
target: detail.target || '',
|
|
16860
|
-
cwd: detail.cwd || '',
|
|
16861
|
-
engine: detail.engine || '',
|
|
16862
|
-
allowWrite: detail.allowWrite === true,
|
|
16863
|
-
dryRun: detail.dryRun === true,
|
|
16864
|
-
concurrency: detail.concurrency || 0,
|
|
16865
|
-
status: run.status || detail.status || '',
|
|
16866
|
-
startedAt: run.startedAt || detail.startedAt || '',
|
|
16867
|
-
endedAt: run.endedAt || detail.endedAt || '',
|
|
16868
|
-
durationMs: run.durationMs || 0,
|
|
16869
|
-
summary: run.summary || detail.summary || '',
|
|
16870
|
-
error: run.error || detail.error || '',
|
|
16871
|
-
nodeCount: nodes.length,
|
|
16872
|
-
successCount: nodes.filter((node) => node.status === 'success').length,
|
|
16873
|
-
failedCount: nodes.filter((node) => node.status === 'failed').length,
|
|
16874
|
-
blockedCount: nodes.filter((node) => node.status === 'blocked').length,
|
|
16875
|
-
cancelledCount: nodes.filter((node) => node.status === 'cancelled').length
|
|
16876
|
-
};
|
|
16877
|
-
}
|
|
16878
|
-
|
|
16879
|
-
function writeTaskRunArtifacts(detail = {}) {
|
|
16880
|
-
const validation = validateTaskRunId(detail && typeof detail.runId === 'string' ? detail.runId : '');
|
|
16881
|
-
if (!validation.ok) {
|
|
16882
|
-
return;
|
|
16883
|
-
}
|
|
16884
|
-
const run = detail.run && typeof detail.run === 'object' ? detail.run : {};
|
|
16885
|
-
const nodes = Array.isArray(run.nodes) ? run.nodes : [];
|
|
16886
|
-
const dir = path.join(TASK_ARTIFACTS_DIR, validation.runId);
|
|
16887
|
-
ensureDir(dir);
|
|
16888
|
-
writeJsonAtomic(path.join(dir, 'summary.json'), collectTaskRunSummary(detail));
|
|
16889
|
-
const runLogText = summarizeTaskLogs(run.logs || [], 200);
|
|
16890
|
-
const nodeLogText = nodes
|
|
16891
|
-
.map((node) => {
|
|
16892
|
-
const header = node && node.id ? `\n# ${node.id}\n` : '\n# node\n';
|
|
16893
|
-
const text = summarizeTaskLogs(node && node.logs ? node.logs : [], 200);
|
|
16894
|
-
return `${header}${text}`.trimEnd();
|
|
16895
|
-
})
|
|
16896
|
-
.filter(Boolean)
|
|
16897
|
-
.join('\n');
|
|
16898
|
-
const combined = `${runLogText}${nodeLogText ? `\n\n${nodeLogText}` : ''}`.trim();
|
|
16899
|
-
try {
|
|
16900
|
-
fs.writeFileSync(path.join(dir, 'logs.txt'), combined, { encoding: 'utf-8', mode: 0o600 });
|
|
16901
|
-
} catch (_) { }
|
|
16902
|
-
}
|
|
16903
|
-
|
|
16904
|
-
async function notifyAutomationOnTaskRun(detail = {}) {
|
|
16905
|
-
const cfg = readAutomationConfig(AUTOMATION_CONFIG_FILE, { env: process.env });
|
|
16906
|
-
if (!cfg.ok || !cfg.config) {
|
|
16907
|
-
return [];
|
|
16908
|
-
}
|
|
16909
|
-
const payload = formatTaskRunNotificationPayload(detail);
|
|
16910
|
-
const status = String(payload.status || '').toLowerCase();
|
|
16911
|
-
const eventType = status === 'success'
|
|
16912
|
-
? 'task.completed'
|
|
16913
|
-
: (status === 'failed' ? 'task.failed' : 'task.finished');
|
|
16914
|
-
return await dispatchAutomationNotifiers(cfg.config, eventType, payload);
|
|
16915
|
-
}
|
|
16916
|
-
|
|
16917
|
-
function startAutomationScheduler() {
|
|
16918
|
-
const lastTicks = new Map();
|
|
16919
|
-
let tickInFlight = false;
|
|
16920
|
-
let timer = setInterval(async () => {
|
|
16921
|
-
if (tickInFlight) {
|
|
16922
|
-
return;
|
|
16923
|
-
}
|
|
16924
|
-
tickInFlight = true;
|
|
16925
|
-
try {
|
|
16926
|
-
const cfg = readAutomationConfig(AUTOMATION_CONFIG_FILE, { env: process.env });
|
|
16927
|
-
if (!cfg.ok || !cfg.config) {
|
|
16928
|
-
return;
|
|
16929
|
-
}
|
|
16930
|
-
const schedules = Array.isArray(cfg.config.schedules) ? cfg.config.schedules : [];
|
|
16931
|
-
if (schedules.length === 0) {
|
|
16932
|
-
return;
|
|
16933
|
-
}
|
|
16934
|
-
const now = new Date();
|
|
16935
|
-
const tickKey = now.toISOString().slice(0, 16);
|
|
16936
|
-
for (const schedule of schedules) {
|
|
16937
|
-
if (!schedule || schedule.enabled === false) continue;
|
|
16938
|
-
if (!schedule.id || !schedule.cron) continue;
|
|
16939
|
-
if (!isCronMatch(schedule.cron, now)) continue;
|
|
16940
|
-
if (lastTicks.get(schedule.id) === tickKey) continue;
|
|
16941
|
-
lastTicks.set(schedule.id, tickKey);
|
|
16942
|
-
const action = schedule.action && typeof schedule.action === 'object' ? schedule.action : {};
|
|
16943
|
-
const actionType = typeof action.type === 'string' ? action.type.trim().toLowerCase() : '';
|
|
16944
|
-
if (actionType !== 'task.queue.add') continue;
|
|
16945
|
-
const taskPayload = action.task && typeof action.task === 'object' ? action.task : {};
|
|
16946
|
-
try {
|
|
16947
|
-
const enqueue = addTaskToQueue(taskPayload);
|
|
16948
|
-
if (enqueue && enqueue.error) continue;
|
|
16949
|
-
if (action.startQueue === true) {
|
|
16950
|
-
await startTaskQueueProcessing({ taskId: '', detach: true });
|
|
16951
|
-
}
|
|
16952
|
-
} catch (_) { }
|
|
16953
|
-
}
|
|
16954
|
-
} finally {
|
|
16955
|
-
tickInFlight = false;
|
|
16956
|
-
}
|
|
16957
|
-
}, 30000);
|
|
16958
|
-
if (timer && typeof timer.unref === 'function') {
|
|
16959
|
-
timer.unref();
|
|
16960
|
-
}
|
|
16961
|
-
return () => {
|
|
16962
|
-
if (!timer) return;
|
|
16963
|
-
clearInterval(timer);
|
|
16964
|
-
timer = null;
|
|
16965
|
-
};
|
|
16966
|
-
}
|
|
16967
|
-
|
|
16968
|
-
function buildTaskOverviewPayload(options = {}) {
|
|
16969
|
-
const queueLimit = Number.isFinite(options.queueLimit) ? Math.max(1, Math.floor(options.queueLimit)) : 20;
|
|
16970
|
-
const runLimit = Number.isFinite(options.runLimit) ? Math.max(1, Math.floor(options.runLimit)) : 20;
|
|
16971
|
-
const workflowCatalog = buildTaskWorkflowCatalog();
|
|
16972
|
-
const queueState = readTaskQueueState();
|
|
16973
|
-
const queue = queueState.error ? [] : listTaskQueueItems({ limit: queueLimit });
|
|
16974
|
-
const runs = listTaskRunRecords(runLimit);
|
|
16975
|
-
const warnings = Array.isArray(workflowCatalog.warnings) ? [...workflowCatalog.warnings] : [];
|
|
16976
|
-
if (queueState.error) {
|
|
16977
|
-
warnings.push(`task queue read error: ${queueState.error}`);
|
|
16978
|
-
}
|
|
16979
|
-
if (g_taskRunRecordsLastParseErrors > 0) {
|
|
16980
|
-
warnings.push(`task run history parse errors: ${g_taskRunRecordsLastParseErrors}`);
|
|
16981
|
-
}
|
|
16982
|
-
return {
|
|
16983
|
-
workflows: workflowCatalog.workflows,
|
|
16984
|
-
openAiChatStatus: buildTaskOpenAiChatStatus(),
|
|
16985
|
-
warnings,
|
|
16986
|
-
queue,
|
|
16987
|
-
runs,
|
|
16988
|
-
activeRunIds: Array.from(g_taskRunControllers.keys()),
|
|
16989
|
-
queueError: queueState.error || '',
|
|
16990
|
-
runParseErrors: g_taskRunRecordsLastParseErrors
|
|
16991
|
-
};
|
|
16992
|
-
}
|
|
16993
|
-
|
|
16994
|
-
function summarizeTaskLogs(logs = [], limit = 80) {
|
|
16995
|
-
return (Array.isArray(logs) ? logs : [])
|
|
16996
|
-
.slice(0, limit)
|
|
16997
|
-
.map((item) => {
|
|
16998
|
-
if (!item || typeof item !== 'object') {
|
|
16999
|
-
return String(item || '');
|
|
17000
|
-
}
|
|
17001
|
-
const at = item.at ? `[${item.at}] ` : '';
|
|
17002
|
-
const level = item.level ? `${String(item.level).toUpperCase()} ` : '';
|
|
17003
|
-
const message = item.message ? String(item.message) : '';
|
|
17004
|
-
return `${at}${level}${message}`.trim();
|
|
17005
|
-
})
|
|
17006
|
-
.filter(Boolean)
|
|
17007
|
-
.join('\n');
|
|
17008
|
-
}
|
|
17009
|
-
|
|
17010
|
-
function findCodexSessionId(value, depth = 0) {
|
|
17011
|
-
if (depth > 6 || value === null || value === undefined) {
|
|
17012
|
-
return '';
|
|
17013
|
-
}
|
|
17014
|
-
if (Array.isArray(value)) {
|
|
17015
|
-
for (const item of value) {
|
|
17016
|
-
const found = findCodexSessionId(item, depth + 1);
|
|
17017
|
-
if (found) return found;
|
|
17018
|
-
}
|
|
17019
|
-
return '';
|
|
17020
|
-
}
|
|
17021
|
-
if (typeof value !== 'object') {
|
|
17022
|
-
return '';
|
|
17023
|
-
}
|
|
17024
|
-
const candidateKeys = ['session_id', 'sessionId', 'conversation_id', 'conversationId', 'thread_id', 'threadId'];
|
|
17025
|
-
for (const key of candidateKeys) {
|
|
17026
|
-
const candidate = value[key];
|
|
17027
|
-
if (typeof candidate === 'string' && candidate.trim()) {
|
|
17028
|
-
return candidate.trim();
|
|
17029
|
-
}
|
|
17030
|
-
}
|
|
17031
|
-
for (const item of Object.values(value)) {
|
|
17032
|
-
const found = findCodexSessionId(item, depth + 1);
|
|
17033
|
-
if (found) return found;
|
|
17034
|
-
}
|
|
17035
|
-
return '';
|
|
17036
|
-
}
|
|
17037
|
-
|
|
17038
|
-
function readCodexLastMessageFile(filePath) {
|
|
17039
|
-
if (!filePath) return '';
|
|
17040
|
-
try {
|
|
17041
|
-
return fs.readFileSync(filePath, 'utf-8').trim();
|
|
17042
|
-
} catch (_) {
|
|
17043
|
-
return '';
|
|
17044
|
-
}
|
|
17045
|
-
}
|
|
17046
|
-
|
|
17047
|
-
function buildOpenAiChatEndpointUrl(baseUrl) {
|
|
17048
|
-
const trimmed = normalizeBaseUrl(baseUrl);
|
|
17049
|
-
if (!trimmed) return '';
|
|
17050
|
-
if (/\/v1\/chat\/completions$/i.test(trimmed) || /\/chat\/completions$/i.test(trimmed)) {
|
|
17051
|
-
return trimmed;
|
|
17052
|
-
}
|
|
17053
|
-
return joinApiUrl(trimmed, 'chat/completions');
|
|
17054
|
-
}
|
|
17055
|
-
|
|
17056
|
-
function redactTaskEndpointUrl(endpointUrl = '') {
|
|
17057
|
-
const raw = String(endpointUrl || '').trim();
|
|
17058
|
-
if (!raw) return '';
|
|
17059
|
-
try {
|
|
17060
|
-
const parsed = new URL(raw);
|
|
17061
|
-
if (parsed.username) parsed.username = '***';
|
|
17062
|
-
if (parsed.password) parsed.password = '***';
|
|
17063
|
-
const secretKeyPattern = /(?:^|[_-])(?:key|api[-_]?key|token|access[-_]?token|refresh[-_]?token|secret|password|passwd|pwd|credential|credentials|signature|sig)(?:$|[_-])/i;
|
|
17064
|
-
for (const key of [...parsed.searchParams.keys()]) {
|
|
17065
|
-
if (secretKeyPattern.test(key)) {
|
|
17066
|
-
parsed.searchParams.set(key, '***');
|
|
17067
|
-
}
|
|
17068
|
-
}
|
|
17069
|
-
return parsed.toString();
|
|
17070
|
-
} catch (_) {
|
|
17071
|
-
return raw
|
|
17072
|
-
.replace(/\/\/([^/@:]+):([^/@]+)@/g, '//***:***@')
|
|
17073
|
-
.replace(/([?&][^=&]*(?:key|token|secret|password|passwd|pwd|credential|signature|sig)[^=]*=)[^&]*/ig, '$1***');
|
|
17074
|
-
}
|
|
17075
|
-
}
|
|
17076
|
-
|
|
17077
|
-
function pickTaskProviderModel(providerName, provider, config) {
|
|
17078
|
-
const currentModels = readCurrentModels();
|
|
17079
|
-
const savedModel = currentModels && typeof currentModels[providerName] === 'string'
|
|
17080
|
-
? currentModels[providerName].trim()
|
|
17081
|
-
: '';
|
|
17082
|
-
const activeProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
|
|
17083
|
-
const activeModel = typeof config.model === 'string' ? config.model.trim() : '';
|
|
17084
|
-
const providerModels = Array.isArray(provider && provider.models) ? provider.models : [];
|
|
17085
|
-
const firstProviderModel = providerModels
|
|
17086
|
-
.map((item) => {
|
|
17087
|
-
if (typeof item === 'string') return item.trim();
|
|
17088
|
-
if (item && typeof item === 'object') return String(item.id || item.name || item.model || '').trim();
|
|
17089
|
-
return '';
|
|
17090
|
-
})
|
|
17091
|
-
.find(Boolean) || '';
|
|
17092
|
-
return savedModel || (activeProvider === providerName ? activeModel : '') || firstProviderModel;
|
|
17093
|
-
}
|
|
17094
|
-
|
|
17095
|
-
function pickTaskProviderTemperature(provider) {
|
|
17096
|
-
const raw = provider && Object.prototype.hasOwnProperty.call(provider, 'temperature')
|
|
17097
|
-
? provider.temperature
|
|
17098
|
-
: undefined;
|
|
17099
|
-
if (raw === undefined || raw === null || String(raw).trim() === '') {
|
|
17100
|
-
return 0.2;
|
|
17101
|
-
}
|
|
17102
|
-
const value = Number(raw);
|
|
17103
|
-
if (!Number.isFinite(value) || value < 0 || value > 2) {
|
|
17104
|
-
return 0.2;
|
|
17105
|
-
}
|
|
17106
|
-
return value;
|
|
17107
|
-
}
|
|
17108
|
-
|
|
17109
|
-
function pickTaskOpenAiChatProviderName(config) {
|
|
17110
|
-
const taskProvider = typeof config.task_openai_chat_provider === 'string'
|
|
17111
|
-
? config.task_openai_chat_provider.trim()
|
|
17112
|
-
: '';
|
|
17113
|
-
if (taskProvider) {
|
|
17114
|
-
return taskProvider;
|
|
17115
|
-
}
|
|
17116
|
-
return typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
|
|
17117
|
-
}
|
|
17118
|
-
|
|
17119
|
-
function resolveTaskOpenAiChatConfig() {
|
|
17120
|
-
const configResult = readConfigOrVirtualDefault();
|
|
17121
|
-
const config = configResult && configResult.config && typeof configResult.config === 'object' ? configResult.config : {};
|
|
17122
|
-
const providerName = pickTaskOpenAiChatProviderName(config);
|
|
17123
|
-
if (!providerName) {
|
|
17124
|
-
return { error: '未设置当前 OpenAI Chat 提供商' };
|
|
17125
|
-
}
|
|
17126
|
-
const providers = config.model_providers && typeof config.model_providers === 'object' ? config.model_providers : {};
|
|
17127
|
-
const provider = providers[providerName];
|
|
17128
|
-
if (!provider || typeof provider !== 'object') {
|
|
17129
|
-
return { error: `OpenAI Chat 提供商不存在: ${providerName}` };
|
|
17130
|
-
}
|
|
17131
|
-
|
|
17132
|
-
const bridgeType = typeof provider.codexmate_bridge === 'string' ? provider.codexmate_bridge.trim() : '';
|
|
17133
|
-
const isOpenaiBridgeProvider = bridgeType === 'openai'
|
|
17134
|
-
|| (typeof provider.base_url === 'string' && provider.base_url.includes('/bridge/openai/'));
|
|
17135
|
-
let baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
|
|
17136
|
-
let apiKey = typeof provider.preferred_auth_method === 'string' ? provider.preferred_auth_method.trim() : '';
|
|
17137
|
-
let extraHeaders = {};
|
|
17138
|
-
if (isOpenaiBridgeProvider) {
|
|
17139
|
-
const upstream = resolveOpenaiBridgeUpstream(OPENAI_BRIDGE_SETTINGS_FILE, providerName);
|
|
17140
|
-
if (upstream && !upstream.error) {
|
|
17141
|
-
baseUrl = upstream.baseUrl || baseUrl;
|
|
17142
|
-
apiKey = upstream.apiKey || apiKey;
|
|
17143
|
-
extraHeaders = upstream.headers && typeof upstream.headers === 'object' && !Array.isArray(upstream.headers)
|
|
17144
|
-
? upstream.headers
|
|
17145
|
-
: {};
|
|
17146
|
-
}
|
|
17147
|
-
}
|
|
17148
|
-
|
|
17149
|
-
const model = pickTaskProviderModel(providerName, provider, config);
|
|
17150
|
-
if (!baseUrl) {
|
|
17151
|
-
return { error: `OpenAI Chat 提供商 ${providerName} 缺少 base_url` };
|
|
17152
|
-
}
|
|
17153
|
-
if (!isValidHttpUrl(baseUrl)) {
|
|
17154
|
-
return { error: `OpenAI Chat 提供商 ${providerName} 的 base_url 无效` };
|
|
17155
|
-
}
|
|
17156
|
-
if (!model) {
|
|
17157
|
-
return { error: `OpenAI Chat 提供商 ${providerName} 未设置模型` };
|
|
17158
|
-
}
|
|
17159
|
-
return {
|
|
17160
|
-
providerName,
|
|
17161
|
-
baseUrl,
|
|
17162
|
-
endpointUrl: buildOpenAiChatEndpointUrl(baseUrl),
|
|
17163
|
-
apiKey,
|
|
17164
|
-
extraHeaders,
|
|
17165
|
-
model,
|
|
17166
|
-
temperature: pickTaskProviderTemperature(provider)
|
|
17167
|
-
};
|
|
17168
|
-
}
|
|
17169
|
-
|
|
17170
|
-
function buildTaskOpenAiChatStatus() {
|
|
17171
|
-
const requestConfig = resolveTaskOpenAiChatConfig();
|
|
17172
|
-
if (requestConfig && requestConfig.error) {
|
|
17173
|
-
return {
|
|
17174
|
-
ok: false,
|
|
17175
|
-
ready: false,
|
|
17176
|
-
error: requestConfig.error,
|
|
17177
|
-
providerName: '',
|
|
17178
|
-
model: '',
|
|
17179
|
-
endpoint: '',
|
|
17180
|
-
hasApiKey: false,
|
|
17181
|
-
hasExtraHeaders: false
|
|
17182
|
-
};
|
|
17183
|
-
}
|
|
17184
|
-
const hasApiKey = !!(requestConfig && typeof requestConfig.apiKey === 'string' && requestConfig.apiKey.trim());
|
|
17185
|
-
const hasExtraHeaders = !!(requestConfig
|
|
17186
|
-
&& requestConfig.extraHeaders
|
|
17187
|
-
&& typeof requestConfig.extraHeaders === 'object'
|
|
17188
|
-
&& !Array.isArray(requestConfig.extraHeaders)
|
|
17189
|
-
&& Object.keys(requestConfig.extraHeaders).length > 0);
|
|
17190
|
-
const hasAuthMaterial = hasApiKey || hasExtraHeaders;
|
|
17191
|
-
return {
|
|
17192
|
-
ok: true,
|
|
17193
|
-
ready: hasAuthMaterial,
|
|
17194
|
-
error: hasAuthMaterial ? '' : `OpenAI Chat 提供商 ${requestConfig.providerName || ''} 缺少 API key 或额外 headers`,
|
|
17195
|
-
providerName: requestConfig.providerName || '',
|
|
17196
|
-
model: requestConfig.model || '',
|
|
17197
|
-
endpoint: redactTaskEndpointUrl(requestConfig.endpointUrl || ''),
|
|
17198
|
-
hasApiKey,
|
|
17199
|
-
hasExtraHeaders
|
|
17200
|
-
};
|
|
17201
|
-
}
|
|
17202
|
-
|
|
17203
|
-
function postOpenAiChatCompletion(requestConfig, body, options = {}) {
|
|
17204
|
-
const endpointUrl = requestConfig && requestConfig.endpointUrl ? requestConfig.endpointUrl : '';
|
|
17205
|
-
return new Promise((resolve) => {
|
|
17206
|
-
let parsed;
|
|
17207
|
-
try {
|
|
17208
|
-
parsed = new URL(endpointUrl);
|
|
17209
|
-
} catch (error) {
|
|
17210
|
-
resolve({ ok: false, error: 'OpenAI Chat endpoint URL 无效', status: 0, payload: null, body: '' });
|
|
17211
|
-
return;
|
|
17212
|
-
}
|
|
17213
|
-
const transport = parsed.protocol === 'https:' ? https : http;
|
|
17214
|
-
const agent = parsed.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT;
|
|
17215
|
-
const payloadText = JSON.stringify(body || {});
|
|
17216
|
-
const headers = {
|
|
17217
|
-
'Content-Type': 'application/json',
|
|
17218
|
-
'Accept': 'application/json',
|
|
17219
|
-
'User-Agent': 'codexmate-task-orchestration',
|
|
17220
|
-
'Content-Length': Buffer.byteLength(payloadText, 'utf-8'),
|
|
17221
|
-
...(requestConfig.extraHeaders && typeof requestConfig.extraHeaders === 'object' ? requestConfig.extraHeaders : {})
|
|
17222
|
-
};
|
|
17223
|
-
if (requestConfig.apiKey) {
|
|
17224
|
-
headers.Authorization = `Bearer ${requestConfig.apiKey}`;
|
|
17225
|
-
}
|
|
17226
|
-
let settled = false;
|
|
17227
|
-
let req = null;
|
|
17228
|
-
const finish = (result) => {
|
|
17229
|
-
if (settled) return;
|
|
17230
|
-
settled = true;
|
|
17231
|
-
resolve(result);
|
|
17232
|
-
};
|
|
17233
|
-
req = transport.request(parsed, { method: 'POST', headers, agent }, (res) => {
|
|
17234
|
-
const status = res.statusCode || 0;
|
|
17235
|
-
let raw = '';
|
|
17236
|
-
let receivedBytes = 0;
|
|
17237
|
-
res.on('data', (chunk) => {
|
|
17238
|
-
if (settled) return;
|
|
17239
|
-
receivedBytes += chunk.length || 0;
|
|
17240
|
-
if (receivedBytes > TASK_OPENAI_CHAT_MAX_RESPONSE_BYTES) {
|
|
17241
|
-
finish({ ok: false, status, error: 'OpenAI Chat response too large', payload: null, body: raw });
|
|
17242
|
-
res.destroy();
|
|
17243
|
-
return;
|
|
17244
|
-
}
|
|
17245
|
-
raw += chunk;
|
|
17246
|
-
});
|
|
17247
|
-
res.on('error', (error) => {
|
|
17248
|
-
finish({
|
|
17249
|
-
ok: false,
|
|
17250
|
-
status,
|
|
17251
|
-
error: error && error.message ? error.message : 'OpenAI Chat request failed',
|
|
17252
|
-
payload: null,
|
|
17253
|
-
body: raw
|
|
17254
|
-
});
|
|
17255
|
-
});
|
|
17256
|
-
res.on('end', () => {
|
|
17257
|
-
if (settled) return;
|
|
17258
|
-
let parsedPayload = null;
|
|
17259
|
-
try {
|
|
17260
|
-
parsedPayload = raw ? JSON.parse(raw) : null;
|
|
17261
|
-
} catch (_) {}
|
|
17262
|
-
if (status < 200 || status >= 300) {
|
|
17263
|
-
const message = parsedPayload && parsedPayload.error
|
|
17264
|
-
? (typeof parsedPayload.error === 'string' ? parsedPayload.error : (parsedPayload.error.message || JSON.stringify(parsedPayload.error)))
|
|
17265
|
-
: (raw || `OpenAI Chat request failed: ${status}`);
|
|
17266
|
-
finish({ ok: false, status, error: truncateTaskText(message, 1200), payload: parsedPayload, body: raw });
|
|
17267
|
-
return;
|
|
17268
|
-
}
|
|
17269
|
-
finish({ ok: true, status, error: '', payload: parsedPayload, body: raw });
|
|
17270
|
-
});
|
|
17271
|
-
});
|
|
17272
|
-
if (typeof options.registerAbort === 'function') {
|
|
17273
|
-
options.registerAbort(() => {
|
|
17274
|
-
try {
|
|
17275
|
-
req.destroy(new Error('cancelled'));
|
|
17276
|
-
} catch (_) {}
|
|
17277
|
-
});
|
|
17278
|
-
}
|
|
17279
|
-
req.setTimeout(TASK_OPENAI_CHAT_TIMEOUT_MS, () => {
|
|
17280
|
-
req.destroy(new Error('OpenAI Chat request timeout'));
|
|
17281
|
-
});
|
|
17282
|
-
req.on('error', (error) => {
|
|
17283
|
-
finish({ ok: false, status: 0, error: error && error.message ? error.message : String(error || 'OpenAI Chat request failed'), payload: null, body: '' });
|
|
17284
|
-
});
|
|
17285
|
-
req.write(payloadText);
|
|
17286
|
-
req.end();
|
|
17287
|
-
});
|
|
17288
|
-
}
|
|
17289
|
-
|
|
17290
|
-
const TASK_OPENAI_MATERIALIZED_ARTIFACT_MAX_BYTES = 512 * 1024;
|
|
17291
|
-
const TASK_OPENAI_MATERIALIZED_ARTIFACT_EXTENSIONS = new Set(['.html', '.htm', '.css', '.js', '.mjs', '.json', '.md', '.txt', '.svg']);
|
|
17292
|
-
|
|
17293
|
-
function normalizeTaskMaterializedArtifactPath(rawPath, cwd) {
|
|
17294
|
-
const baseDir = path.resolve(typeof cwd === 'string' && cwd.trim() ? cwd.trim() : process.cwd());
|
|
17295
|
-
const text = typeof rawPath === 'string' ? rawPath.trim() : '';
|
|
17296
|
-
if (!text || text.length > 200) {
|
|
17297
|
-
return { error: 'artifact path is empty or too long' };
|
|
17298
|
-
}
|
|
17299
|
-
if (path.isAbsolute(text) || text.includes('\\')) {
|
|
17300
|
-
return { error: `artifact path must be a safe relative path: ${text}` };
|
|
17301
|
-
}
|
|
17302
|
-
const normalized = path.normalize(text);
|
|
17303
|
-
if (!normalized || normalized === '.' || normalized.startsWith('..') || normalized.split(path.sep).includes('..')) {
|
|
17304
|
-
return { error: `artifact path escapes cwd: ${text}` };
|
|
17305
|
-
}
|
|
17306
|
-
const ext = path.extname(normalized).toLowerCase();
|
|
17307
|
-
if (!TASK_OPENAI_MATERIALIZED_ARTIFACT_EXTENSIONS.has(ext)) {
|
|
17308
|
-
return { error: `artifact extension is not allowed: ${ext || '(none)'}` };
|
|
17309
|
-
}
|
|
17310
|
-
const targetPath = path.resolve(baseDir, normalized);
|
|
17311
|
-
const relative = path.relative(baseDir, targetPath);
|
|
17312
|
-
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
17313
|
-
return { error: `artifact path escapes cwd: ${text}` };
|
|
17314
|
-
}
|
|
17315
|
-
return { path: targetPath, relativePath: normalized, baseDir };
|
|
17316
|
-
}
|
|
17317
|
-
|
|
17318
|
-
function validateTaskMaterializedArtifactParents(targetPath, baseDir) {
|
|
17319
|
-
const resolvedBase = fs.realpathSync.native(baseDir);
|
|
17320
|
-
let current = path.dirname(targetPath);
|
|
17321
|
-
const pending = [];
|
|
17322
|
-
while (true) {
|
|
17323
|
-
if (current === resolvedBase) {
|
|
17324
|
-
return { ok: true };
|
|
17325
|
-
}
|
|
17326
|
-
if (current === path.dirname(current)) {
|
|
17327
|
-
return { ok: false, error: 'artifact parent escapes cwd' };
|
|
17328
|
-
}
|
|
17329
|
-
if (fs.existsSync(current)) {
|
|
17330
|
-
const stats = fs.lstatSync(current);
|
|
17331
|
-
if (stats.isSymbolicLink()) {
|
|
17332
|
-
return { ok: false, error: `artifact parent is a symlink: ${path.relative(baseDir, current) || current}` };
|
|
17333
|
-
}
|
|
17334
|
-
const resolvedCurrent = fs.realpathSync.native(current);
|
|
17335
|
-
const relative = path.relative(resolvedBase, resolvedCurrent);
|
|
17336
|
-
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
17337
|
-
return { ok: false, error: 'artifact parent resolves outside cwd' };
|
|
17338
|
-
}
|
|
17339
|
-
for (const child of pending) {
|
|
17340
|
-
const childRelative = path.relative(resolvedBase, child);
|
|
17341
|
-
if (childRelative.startsWith('..') || path.isAbsolute(childRelative)) {
|
|
17342
|
-
return { ok: false, error: 'artifact parent escapes cwd' };
|
|
17343
|
-
}
|
|
17344
|
-
}
|
|
17345
|
-
return { ok: true };
|
|
17346
|
-
}
|
|
17347
|
-
pending.push(current);
|
|
17348
|
-
current = path.dirname(current);
|
|
17349
|
-
}
|
|
17350
|
-
}
|
|
17351
|
-
|
|
17352
|
-
function inferTaskMaterializedArtifactPath(info, prefix, hints) {
|
|
17353
|
-
const sources = [info, prefix, hints].map((item) => String(item || '')).filter(Boolean);
|
|
17354
|
-
const explicitPatterns = [
|
|
17355
|
-
/(?:file|filename|path)\s*[:=]\s*[`"']?([A-Za-z0-9._/-]+\.[A-Za-z0-9]+)[`"']?/i,
|
|
17356
|
-
/(?:文件|路径|保存为|写入到?|输出到?)\s*[::]?\s*[`"']?([A-Za-z0-9._/-]+\.[A-Za-z0-9]+)[`"']?/i,
|
|
17357
|
-
/[`"']([A-Za-z0-9._/-]+\.[A-Za-z0-9]+)[`"']/i
|
|
17358
|
-
];
|
|
17359
|
-
for (const source of sources) {
|
|
17360
|
-
for (const pattern of explicitPatterns) {
|
|
17361
|
-
const match = source.match(pattern);
|
|
17362
|
-
if (match && match[1]) {
|
|
17363
|
-
return match[1];
|
|
17364
|
-
}
|
|
17365
|
-
}
|
|
17366
|
-
}
|
|
17367
|
-
const normalizedInfo = String(info || '').trim().toLowerCase();
|
|
17368
|
-
const hintText = sources.join('\n').toLowerCase();
|
|
17369
|
-
if ((normalizedInfo === 'html' || normalizedInfo.startsWith('html ')) && hintText.includes('index.html')) {
|
|
17370
|
-
return 'index.html';
|
|
17371
|
-
}
|
|
17372
|
-
return '';
|
|
17373
|
-
}
|
|
17374
|
-
|
|
17375
|
-
function materializeOpenAiChatTaskArtifacts(text, options = {}) {
|
|
17376
|
-
const content = typeof text === 'string' ? text : '';
|
|
17377
|
-
if (!content.trim()) {
|
|
17378
|
-
return { files: [], warnings: [] };
|
|
17379
|
-
}
|
|
17380
|
-
const cwd = typeof options.cwd === 'string' && options.cwd.trim() ? options.cwd.trim() : process.cwd();
|
|
17381
|
-
const hints = [options.target, options.notes, options.prompt]
|
|
17382
|
-
.map((item) => String(item || '').trim())
|
|
17383
|
-
.filter(Boolean)
|
|
17384
|
-
.join('\n');
|
|
17385
|
-
const files = [];
|
|
17386
|
-
const warnings = [];
|
|
17387
|
-
const seen = new Set();
|
|
17388
|
-
const fencePattern = /```([^\n`]*)\n([\s\S]*?)```/g;
|
|
17389
|
-
let match = null;
|
|
17390
|
-
while ((match = fencePattern.exec(content)) !== null) {
|
|
17391
|
-
const info = String(match[1] || '').trim();
|
|
17392
|
-
const body = String(match[2] || '');
|
|
17393
|
-
const prefix = content.slice(Math.max(0, match.index - 240), match.index);
|
|
17394
|
-
const inferredPath = inferTaskMaterializedArtifactPath(info, prefix, hints);
|
|
17395
|
-
if (!inferredPath) {
|
|
17396
|
-
continue;
|
|
17397
|
-
}
|
|
17398
|
-
if (Buffer.byteLength(body, 'utf-8') > TASK_OPENAI_MATERIALIZED_ARTIFACT_MAX_BYTES) {
|
|
17399
|
-
warnings.push(`artifact too large and skipped: ${inferredPath}`);
|
|
17400
|
-
continue;
|
|
17401
|
-
}
|
|
17402
|
-
const normalized = normalizeTaskMaterializedArtifactPath(inferredPath, cwd);
|
|
17403
|
-
if (normalized.error) {
|
|
17404
|
-
warnings.push(normalized.error);
|
|
17405
|
-
continue;
|
|
17406
|
-
}
|
|
17407
|
-
if (seen.has(normalized.path)) {
|
|
17408
|
-
warnings.push(`duplicate artifact skipped: ${normalized.relativePath}`);
|
|
17409
|
-
continue;
|
|
17410
|
-
}
|
|
17411
|
-
seen.add(normalized.path);
|
|
17412
|
-
try {
|
|
17413
|
-
const fileContent = body.trimStart();
|
|
17414
|
-
const parentValidation = validateTaskMaterializedArtifactParents(normalized.path, normalized.baseDir);
|
|
17415
|
-
if (!parentValidation.ok) {
|
|
17416
|
-
warnings.push(parentValidation.error || `artifact parent is unsafe: ${normalized.relativePath}`);
|
|
17417
|
-
continue;
|
|
17418
|
-
}
|
|
17419
|
-
ensureDir(path.dirname(normalized.path));
|
|
17420
|
-
try {
|
|
17421
|
-
if (fs.lstatSync(normalized.path).isSymbolicLink()) {
|
|
17422
|
-
warnings.push(`artifact target is a symlink: ${normalized.relativePath}`);
|
|
17423
|
-
continue;
|
|
17424
|
-
}
|
|
17425
|
-
} catch (error) {
|
|
17426
|
-
if (!error || error.code !== 'ENOENT') {
|
|
17427
|
-
throw error;
|
|
17428
|
-
}
|
|
17429
|
-
}
|
|
17430
|
-
fs.writeFileSync(normalized.path, fileContent, { encoding: 'utf-8', mode: 0o600 });
|
|
17431
|
-
files.push({ path: normalized.path, relativePath: normalized.relativePath, bytes: Buffer.byteLength(fileContent, 'utf-8') });
|
|
17432
|
-
} catch (error) {
|
|
17433
|
-
warnings.push(`failed to write artifact ${normalized.relativePath}: ${error && error.message ? error.message : String(error)}`);
|
|
17434
|
-
}
|
|
17435
|
-
}
|
|
17436
|
-
return { files, warnings };
|
|
17437
|
-
}
|
|
17438
|
-
|
|
17439
|
-
async function runOpenAiChatTaskNode(node, context = {}) {
|
|
17440
|
-
const requestConfig = resolveTaskOpenAiChatConfig();
|
|
17441
|
-
if (requestConfig.error) {
|
|
17442
|
-
return {
|
|
17443
|
-
success: false,
|
|
17444
|
-
error: requestConfig.error,
|
|
17445
|
-
summary: requestConfig.error,
|
|
17446
|
-
output: null,
|
|
17447
|
-
logs: [{ at: toIsoTime(Date.now()), level: 'error', message: requestConfig.error }]
|
|
17448
|
-
};
|
|
17449
|
-
}
|
|
17450
|
-
const hasApiKey = typeof requestConfig.apiKey === 'string' && requestConfig.apiKey.trim();
|
|
17451
|
-
const hasExtraHeaders = requestConfig.extraHeaders
|
|
17452
|
-
&& typeof requestConfig.extraHeaders === 'object'
|
|
17453
|
-
&& !Array.isArray(requestConfig.extraHeaders)
|
|
17454
|
-
&& Object.keys(requestConfig.extraHeaders).length > 0;
|
|
17455
|
-
if (!hasApiKey && !hasExtraHeaders) {
|
|
17456
|
-
const error = `OpenAI Chat 提供商 ${requestConfig.providerName || ''} 缺少 API key 或额外 headers`;
|
|
17457
|
-
return {
|
|
17458
|
-
success: false,
|
|
17459
|
-
error,
|
|
17460
|
-
summary: error,
|
|
17461
|
-
output: {
|
|
17462
|
-
provider: requestConfig.providerName || '',
|
|
17463
|
-
model: requestConfig.model || '',
|
|
17464
|
-
endpoint: redactTaskEndpointUrl(requestConfig.endpointUrl || ''),
|
|
17465
|
-
status: 0,
|
|
17466
|
-
text: '',
|
|
17467
|
-
response: null,
|
|
17468
|
-
durationMs: 0,
|
|
17469
|
-
materializedFiles: []
|
|
17470
|
-
},
|
|
17471
|
-
logs: [{ at: toIsoTime(Date.now()), level: 'error', message: error }]
|
|
17472
|
-
};
|
|
17473
|
-
}
|
|
17474
|
-
const allowWrite = context.allowWrite === true && node.write !== false && context.dryRun !== true;
|
|
17475
|
-
const dependencyResults = Array.isArray(context.dependencyResults) ? context.dependencyResults : [];
|
|
17476
|
-
const dependencyLines = dependencyResults
|
|
17477
|
-
.map((item) => {
|
|
17478
|
-
const summary = item && (item.summary || item.error) ? String(item.summary || item.error) : '';
|
|
17479
|
-
return summary ? `- ${item.id}: ${summary}` : '';
|
|
17480
|
-
})
|
|
17481
|
-
.filter(Boolean);
|
|
17482
|
-
const previousAttempts = Array.isArray(context.previousAttempts) ? context.previousAttempts : [];
|
|
17483
|
-
const lastAttempt = previousAttempts.length > 0 ? previousAttempts[previousAttempts.length - 1] : null;
|
|
17484
|
-
const attempt = Number.isFinite(context.attempt) ? context.attempt : 1;
|
|
17485
|
-
const promptParts = [String(node.prompt || '').trim()];
|
|
17486
|
-
if (dependencyLines.length > 0) {
|
|
17487
|
-
promptParts.push(`前置节点摘要:\n${dependencyLines.join('\n')}`);
|
|
17488
|
-
}
|
|
17489
|
-
if (attempt > 1 && lastAttempt) {
|
|
17490
|
-
promptParts.push(`上一轮失败摘要:\n${String(lastAttempt.error || lastAttempt.summary || '').trim()}`);
|
|
17491
|
-
promptParts.push('请在保持目标不变的前提下修复上一轮失败并继续完成当前节点。');
|
|
17492
|
-
}
|
|
17493
|
-
const finalPrompt = promptParts.filter(Boolean).join('\n\n');
|
|
17494
|
-
const rawThreadId = String(context.threadId || (context.plan && context.plan.threadId) || '').trim();
|
|
17495
|
-
const threadId = /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(rawThreadId) ? rawThreadId : '';
|
|
17496
|
-
const threadStoreDir = typeof TASK_WORKSPACE_CHAT_THREADS_DIR === 'string' ? TASK_WORKSPACE_CHAT_THREADS_DIR : '';
|
|
17497
|
-
const canLoadWorkspaceThread = typeof loadWorkspaceChatThread === 'function' && !!threadStoreDir;
|
|
17498
|
-
const thread = threadId && canLoadWorkspaceThread ? loadWorkspaceChatThread(threadStoreDir, threadId) : { messages: [] };
|
|
17499
|
-
const historyMessages = Array.isArray(thread.messages) ? thread.messages.slice(-12) : [];
|
|
17500
|
-
const workspaceContext = typeof buildWorkspaceChatContext === 'function'
|
|
17501
|
-
? buildWorkspaceChatContext(context.cwd || process.cwd(), finalPrompt, historyMessages)
|
|
17502
|
-
: '';
|
|
17503
|
-
const systemPrompt = [
|
|
17504
|
-
'你是 codexmate 任务编排中的 OpenAI Chat-compatible 执行节点。',
|
|
17505
|
-
'基于用户给出的目标、前置节点摘要、同一 thread 历史和当前工作区快照,输出可执行、可验证、事实谨慎的结果。',
|
|
17506
|
-
allowWrite
|
|
17507
|
-
? '当前任务允许写入;如果需要创建或更新文件,优先使用 fenced code block:```codexmate-file action="write" path="相对路径"。代码块内容会写入任务工作目录内对应文本文件。删除文件请单独写一行 CODEXMATE_DELETE_FILE: 相对路径。只允许安全相对路径。'
|
|
17508
|
-
: '当前任务是只读/验证节点,不要声称修改了文件。',
|
|
17509
|
-
'查询/读取文件时,先依据用户要求和工作区快照回答;如果快照不足,说明缺口,不要编造。',
|
|
17510
|
-
'回答使用中文,避免空泛总结。'
|
|
17511
|
-
].join('\n');
|
|
17512
|
-
const currentUserMessage = [
|
|
17513
|
-
finalPrompt,
|
|
17514
|
-
workspaceContext ? '\n--- codexmate workspace snapshot ---' : '',
|
|
17515
|
-
workspaceContext
|
|
17516
|
-
].filter(Boolean).join('\n');
|
|
17517
|
-
const startedAt = Date.now();
|
|
17518
|
-
const body = {
|
|
17519
|
-
model: requestConfig.model,
|
|
17520
|
-
messages: [
|
|
17521
|
-
{ role: 'system', content: systemPrompt },
|
|
17522
|
-
...historyMessages.map((message) => ({
|
|
17523
|
-
role: message.role === 'assistant' ? 'assistant' : 'user',
|
|
17524
|
-
content: String(message.content || '')
|
|
17525
|
-
})),
|
|
17526
|
-
{ role: 'user', content: currentUserMessage }
|
|
17527
|
-
],
|
|
17528
|
-
temperature: Number.isFinite(requestConfig.temperature) ? requestConfig.temperature : 0.2,
|
|
17529
|
-
stream: false
|
|
17530
|
-
};
|
|
17531
|
-
const result = await postOpenAiChatCompletion(requestConfig, body, {
|
|
17532
|
-
registerAbort: context.registerAbort
|
|
17533
|
-
});
|
|
17534
|
-
const text = result.ok ? extractModelResponseText(result.payload) : '';
|
|
17535
|
-
const success = result.ok && !!text;
|
|
17536
|
-
const errorMessage = success ? '' : (result.error || 'OpenAI Chat response did not contain text');
|
|
17537
|
-
const summary = truncateTaskText(text || errorMessage, 400);
|
|
17538
|
-
const safeEndpoint = redactTaskEndpointUrl(requestConfig.endpointUrl);
|
|
17539
|
-
const materialized = success && allowWrite
|
|
17540
|
-
? materializeOpenAiChatTaskArtifacts(text, {
|
|
17541
|
-
cwd: context.cwd || process.cwd(),
|
|
17542
|
-
target: context.plan && context.plan.target ? context.plan.target : '',
|
|
17543
|
-
notes: context.plan && context.plan.notes ? context.plan.notes : '',
|
|
17544
|
-
prompt: node.prompt || ''
|
|
17545
|
-
})
|
|
17546
|
-
: { files: [], warnings: [] };
|
|
17547
|
-
const workspaceOperations = success && typeof applyWorkspaceFileOperations === 'function'
|
|
17548
|
-
? applyWorkspaceFileOperations(text, context.cwd || process.cwd(), { allowWrite })
|
|
17549
|
-
: { files: [], warnings: [] };
|
|
17550
|
-
const threadAppend = threadId && typeof appendWorkspaceChatThread === 'function' && !!threadStoreDir
|
|
17551
|
-
? appendWorkspaceChatThread(threadStoreDir, threadId, [
|
|
17552
|
-
{ role: 'user', content: finalPrompt },
|
|
17553
|
-
{ role: 'assistant', content: text || errorMessage }
|
|
17554
|
-
], { cwd: context.cwd || process.cwd() })
|
|
17555
|
-
: { ok: false, error: 'thread id is empty' };
|
|
17556
|
-
return {
|
|
17557
|
-
success,
|
|
17558
|
-
error: errorMessage,
|
|
17559
|
-
summary,
|
|
17560
|
-
output: {
|
|
17561
|
-
provider: requestConfig.providerName,
|
|
17562
|
-
model: requestConfig.model,
|
|
17563
|
-
endpoint: safeEndpoint,
|
|
17564
|
-
status: result.status || 0,
|
|
17565
|
-
text,
|
|
17566
|
-
response: result.payload || null,
|
|
17567
|
-
durationMs: Date.now() - startedAt,
|
|
17568
|
-
messageCount: body.messages.length,
|
|
17569
|
-
threadId,
|
|
17570
|
-
threadStore: threadAppend && threadAppend.file ? threadAppend.file : '',
|
|
17571
|
-
materializedFiles: materialized.files,
|
|
17572
|
-
workspaceFiles: workspaceOperations.files
|
|
17573
|
-
},
|
|
17574
|
-
logs: [
|
|
17575
|
-
{ at: toIsoTime(Date.now()), level: 'info', message: `OpenAI Chat request provider=${requestConfig.providerName} model=${requestConfig.model} status=${result.status || 0}` },
|
|
17576
|
-
...(threadId ? [{ at: toIsoTime(Date.now()), level: 'info', message: `workspace chat thread=${threadId} messages=${body.messages.length}` }] : []),
|
|
17577
|
-
...(success ? [{ at: toIsoTime(Date.now()), level: 'info', message: truncateTaskText(text, 1200) }] : [{ at: toIsoTime(Date.now()), level: 'error', message: errorMessage }]),
|
|
17578
|
-
...materialized.files.map((file) => ({ at: toIsoTime(Date.now()), level: 'info', message: `materialized artifact ${file.relativePath} (${file.bytes} bytes)` })),
|
|
17579
|
-
...workspaceOperations.files.map((file) => ({ at: toIsoTime(Date.now()), level: 'info', message: `workspace ${file.operation} ${file.relativePath} (${file.bytes} bytes)` })),
|
|
17580
|
-
...materialized.warnings.map((warning) => ({ at: toIsoTime(Date.now()), level: 'warn', message: warning })),
|
|
17581
|
-
...workspaceOperations.warnings.map((warning) => ({ at: toIsoTime(Date.now()), level: 'warn', message: warning })),
|
|
17582
|
-
...(threadAppend && threadAppend.error ? [{ at: toIsoTime(Date.now()), level: 'warn', message: threadAppend.error }] : [])
|
|
17583
|
-
]
|
|
17584
|
-
};
|
|
17585
|
-
}
|
|
17586
|
-
|
|
17587
|
-
async function executeTaskNodeAdapter(node, context = {}) {
|
|
17588
|
-
if (node.kind === 'workflow') {
|
|
17589
|
-
const input = {
|
|
17590
|
-
...(node.input && typeof node.input === 'object' && !Array.isArray(node.input) ? cloneJson(node.input, {}) : {}),
|
|
17591
|
-
task: {
|
|
17592
|
-
title: context.plan && context.plan.title ? context.plan.title : '',
|
|
17593
|
-
target: context.plan && context.plan.target ? context.plan.target : '',
|
|
17594
|
-
dependencyResults: cloneJson(context.dependencyResults || [], [])
|
|
17595
|
-
}
|
|
17596
|
-
};
|
|
17597
|
-
const result = await runWorkflowById(node.workflowId, input, {
|
|
17598
|
-
allowWrite: context.allowWrite === true,
|
|
17599
|
-
dryRun: context.dryRun === true
|
|
17600
|
-
});
|
|
17601
|
-
return {
|
|
17602
|
-
success: result && result.success === true,
|
|
17603
|
-
error: result && result.error ? result.error : '',
|
|
17604
|
-
summary: truncateTaskText(
|
|
17605
|
-
result && result.error
|
|
17606
|
-
? result.error
|
|
17607
|
-
: `${result && result.workflowName ? result.workflowName : node.workflowId} ${result && result.success === true ? '完成' : '失败'}`,
|
|
17608
|
-
400
|
|
17609
|
-
),
|
|
17610
|
-
output: cloneJson(result, null),
|
|
17611
|
-
logs: Array.isArray(result && result.steps)
|
|
17612
|
-
? result.steps.map((step) => ({
|
|
17613
|
-
at: step.startedAt || toIsoTime(Date.now()),
|
|
17614
|
-
level: step.status === 'failed' ? 'error' : (step.status === 'skipped' ? 'warn' : 'info'),
|
|
17615
|
-
message: `${step.id || step.tool || 'step'}: ${step.status || 'unknown'}${step.error ? ` (${step.error})` : ''}`
|
|
17616
|
-
}))
|
|
17617
|
-
: []
|
|
17618
|
-
};
|
|
17619
|
-
}
|
|
17620
|
-
return runOpenAiChatTaskNode(node, context);
|
|
17621
|
-
}
|
|
17622
|
-
|
|
17623
|
-
async function runTaskPlanInternal(plan, options = {}) {
|
|
17624
|
-
const validation = validatePreparedTaskPlan(plan);
|
|
17625
|
-
if (!validation.ok) {
|
|
17626
|
-
return {
|
|
17627
|
-
error: validation.error || 'task plan validation failed',
|
|
17628
|
-
issues: validation.issues || [],
|
|
17629
|
-
warnings: validation.warnings || []
|
|
17630
|
-
};
|
|
17631
|
-
}
|
|
17632
|
-
const taskId = typeof options.taskId === 'string' && options.taskId.trim() ? options.taskId.trim() : (plan.id || createTaskId());
|
|
17633
|
-
const runId = typeof options.runId === 'string' && options.runId.trim() ? options.runId.trim() : createTaskRunId();
|
|
17634
|
-
const threadId = normalizeTaskThreadId(options.threadId || plan.threadId) || createTaskThreadId();
|
|
17635
|
-
plan.threadId = threadId;
|
|
17636
|
-
const cwd = typeof plan.cwd === 'string' && plan.cwd.trim() ? plan.cwd.trim() : process.cwd();
|
|
17637
|
-
const controller = new AbortController();
|
|
17638
|
-
const baseDetail = {
|
|
17639
|
-
runId,
|
|
17640
|
-
taskId,
|
|
17641
|
-
threadId,
|
|
17642
|
-
workerPid: process.pid,
|
|
17643
|
-
title: plan.title || '',
|
|
17644
|
-
target: plan.target || '',
|
|
17645
|
-
cwd,
|
|
17646
|
-
engine: normalizeTaskEngine(plan.engine),
|
|
17647
|
-
allowWrite: plan.allowWrite === true,
|
|
17648
|
-
dryRun: plan.dryRun === true,
|
|
17649
|
-
concurrency: Number.isFinite(plan.concurrency) ? plan.concurrency : 2,
|
|
17650
|
-
createdAt: toIsoTime(Date.now()),
|
|
17651
|
-
updatedAt: toIsoTime(Date.now()),
|
|
17652
|
-
warnings: validation.warnings || [],
|
|
17653
|
-
plan: cloneJson(plan, {})
|
|
17654
|
-
};
|
|
17655
|
-
writeTaskRunDetail({
|
|
17656
|
-
...baseDetail,
|
|
17657
|
-
status: 'running',
|
|
17658
|
-
run: {
|
|
17659
|
-
status: 'running',
|
|
17660
|
-
startedAt: toIsoTime(Date.now()),
|
|
17661
|
-
endedAt: '',
|
|
17662
|
-
durationMs: 0,
|
|
17663
|
-
nodes: [],
|
|
17664
|
-
logs: []
|
|
17665
|
-
}
|
|
17666
|
-
});
|
|
17667
|
-
g_taskRunControllers.set(runId, {
|
|
17668
|
-
runId,
|
|
17669
|
-
taskId,
|
|
17670
|
-
controller,
|
|
17671
|
-
abort() {
|
|
17672
|
-
try {
|
|
17673
|
-
controller.abort();
|
|
17674
|
-
} catch (_) { }
|
|
17675
|
-
}
|
|
17676
|
-
});
|
|
17677
|
-
if (options.queueItem) {
|
|
17678
|
-
const queued = upsertTaskQueueItem({
|
|
17679
|
-
...options.queueItem,
|
|
17680
|
-
taskId,
|
|
17681
|
-
threadId,
|
|
17682
|
-
cwd,
|
|
17683
|
-
status: 'running',
|
|
17684
|
-
runStatus: 'running',
|
|
17685
|
-
lastRunId: runId,
|
|
17686
|
-
lastSummary: '',
|
|
17687
|
-
updatedAt: toIsoTime(Date.now()),
|
|
17688
|
-
plan
|
|
17689
|
-
});
|
|
17690
|
-
if (queued && queued.error) { }
|
|
17691
|
-
}
|
|
17692
|
-
try {
|
|
17693
|
-
const run = await executeTaskPlan(plan, {
|
|
17694
|
-
concurrency: plan.concurrency,
|
|
17695
|
-
signal: controller.signal,
|
|
17696
|
-
executeNode: async (node, nodeContext) => executeTaskNodeAdapter(node, {
|
|
17697
|
-
...nodeContext,
|
|
17698
|
-
plan,
|
|
17699
|
-
taskId,
|
|
17700
|
-
runId,
|
|
17701
|
-
threadId,
|
|
17702
|
-
allowWrite: plan.allowWrite === true,
|
|
17703
|
-
dryRun: plan.dryRun === true,
|
|
17704
|
-
cwd
|
|
17705
|
-
}),
|
|
17706
|
-
onUpdate: async (snapshot) => {
|
|
17707
|
-
const nextDetail = {
|
|
17708
|
-
...baseDetail,
|
|
17709
|
-
updatedAt: toIsoTime(Date.now()),
|
|
17710
|
-
status: snapshot.status || 'running',
|
|
17711
|
-
run: snapshot
|
|
17712
|
-
};
|
|
17713
|
-
writeTaskRunDetail(nextDetail);
|
|
17714
|
-
if (options.queueItem) {
|
|
17715
|
-
const queued = upsertTaskQueueItem({
|
|
17716
|
-
...options.queueItem,
|
|
17717
|
-
taskId,
|
|
17718
|
-
threadId,
|
|
17719
|
-
cwd,
|
|
17720
|
-
status: snapshot.status === 'success'
|
|
17721
|
-
? 'completed'
|
|
17722
|
-
: (snapshot.status === 'failed' ? 'failed' : (snapshot.status === 'cancelled' ? 'cancelled' : 'running')),
|
|
17723
|
-
runStatus: snapshot.status || 'running',
|
|
17724
|
-
lastRunId: runId,
|
|
17725
|
-
lastSummary: snapshot.summary || '',
|
|
17726
|
-
updatedAt: toIsoTime(Date.now()),
|
|
17727
|
-
plan
|
|
17728
|
-
});
|
|
17729
|
-
if (queued && queued.error) { }
|
|
17730
|
-
}
|
|
17731
|
-
}
|
|
17732
|
-
});
|
|
17733
|
-
const detail = {
|
|
17734
|
-
...baseDetail,
|
|
17735
|
-
updatedAt: toIsoTime(Date.now()),
|
|
17736
|
-
status: run.status || 'failed',
|
|
17737
|
-
run
|
|
17738
|
-
};
|
|
17739
|
-
writeTaskRunDetail(detail);
|
|
17740
|
-
appendTaskRunRecord(collectTaskRunSummary(detail));
|
|
17741
|
-
writeTaskRunArtifacts(detail);
|
|
17742
|
-
try {
|
|
17743
|
-
await notifyAutomationOnTaskRun(detail);
|
|
17744
|
-
} catch (_) { }
|
|
17745
|
-
if (options.queueItem) {
|
|
17746
|
-
const queued = upsertTaskQueueItem({
|
|
17747
|
-
...options.queueItem,
|
|
17748
|
-
taskId,
|
|
17749
|
-
threadId,
|
|
17750
|
-
cwd,
|
|
17751
|
-
status: run.status === 'success'
|
|
17752
|
-
? 'completed'
|
|
17753
|
-
: (run.status === 'cancelled' ? 'cancelled' : 'failed'),
|
|
17754
|
-
runStatus: run.status || '',
|
|
17755
|
-
lastRunId: runId,
|
|
17756
|
-
lastSummary: run.summary || run.error || '',
|
|
17757
|
-
updatedAt: toIsoTime(Date.now()),
|
|
17758
|
-
plan
|
|
17759
|
-
});
|
|
17760
|
-
if (queued && queued.error) { }
|
|
17761
|
-
}
|
|
17762
|
-
return detail;
|
|
17763
|
-
} finally {
|
|
17764
|
-
g_taskRunControllers.delete(runId);
|
|
17765
|
-
}
|
|
17766
|
-
}
|
|
17767
|
-
|
|
17768
|
-
function addTaskToQueue(params = {}) {
|
|
17769
|
-
const plan = coerceTaskPlanPayload(params || {});
|
|
17770
|
-
const validation = validatePreparedTaskPlan(plan);
|
|
17771
|
-
if (!validation.ok) {
|
|
17772
|
-
return {
|
|
17773
|
-
error: validation.error || 'task plan validation failed',
|
|
17774
|
-
issues: validation.issues || [],
|
|
17775
|
-
warnings: validation.warnings || []
|
|
17776
|
-
};
|
|
17777
|
-
}
|
|
17778
|
-
const taskId = typeof params.taskId === 'string' && params.taskId.trim() ? params.taskId.trim() : createTaskId();
|
|
17779
|
-
const item = upsertTaskQueueItem({
|
|
17780
|
-
taskId,
|
|
17781
|
-
threadId: plan.threadId || createTaskThreadId(),
|
|
17782
|
-
title: plan.title,
|
|
17783
|
-
target: plan.target,
|
|
17784
|
-
cwd: plan.cwd || process.cwd(),
|
|
17785
|
-
status: 'queued',
|
|
17786
|
-
createdAt: toIsoTime(Date.now()),
|
|
17787
|
-
updatedAt: toIsoTime(Date.now()),
|
|
17788
|
-
engine: plan.engine,
|
|
17789
|
-
allowWrite: plan.allowWrite === true,
|
|
17790
|
-
dryRun: plan.dryRun === true,
|
|
17791
|
-
concurrency: plan.concurrency || 2,
|
|
17792
|
-
autoFixRounds: plan.autoFixRounds || 1,
|
|
17793
|
-
lastRunId: '',
|
|
17794
|
-
lastSummary: '',
|
|
17795
|
-
runStatus: '',
|
|
17796
|
-
plan
|
|
17797
|
-
});
|
|
17798
|
-
if (item && item.error) {
|
|
17799
|
-
return { error: item.error };
|
|
17800
|
-
}
|
|
17801
|
-
return {
|
|
17802
|
-
ok: true,
|
|
17803
|
-
task: item,
|
|
17804
|
-
warnings: validation.warnings || []
|
|
17805
|
-
};
|
|
17806
|
-
}
|
|
17807
|
-
|
|
17808
|
-
async function runTaskNow(params = {}) {
|
|
17809
|
-
const rawRunId = params && typeof params.runId === 'string' ? params.runId.trim() : '';
|
|
17810
|
-
const runIdValidation = rawRunId
|
|
17811
|
-
? validateTaskRunId(rawRunId)
|
|
17812
|
-
: { ok: true, runId: createTaskRunId(), error: '' };
|
|
17813
|
-
if (!runIdValidation.ok) {
|
|
17814
|
-
return { error: runIdValidation.error };
|
|
17815
|
-
}
|
|
17816
|
-
const plan = coerceTaskPlanPayload(params || {});
|
|
17817
|
-
const detail = await runTaskPlanInternal(plan, {
|
|
17818
|
-
taskId: typeof params.taskId === 'string' && params.taskId.trim() ? params.taskId.trim() : createTaskId(),
|
|
17819
|
-
runId: runIdValidation.runId
|
|
17820
|
-
});
|
|
17821
|
-
return detail;
|
|
17822
|
-
}
|
|
17823
|
-
|
|
17824
|
-
async function runTaskQueueProcessingInternal(options = {}) {
|
|
17825
|
-
const taskId = typeof options.taskId === 'string' ? options.taskId.trim() : '';
|
|
17826
|
-
let latestDetail = null;
|
|
17827
|
-
while (true) {
|
|
17828
|
-
const queue = listTaskQueueItems({ limit: 200, status: 'queued' });
|
|
17829
|
-
const nextItem = taskId
|
|
17830
|
-
? queue.find((item) => item.taskId === taskId)
|
|
17831
|
-
: queue[queue.length - 1];
|
|
17832
|
-
if (!nextItem) {
|
|
17833
|
-
break;
|
|
17834
|
-
}
|
|
17835
|
-
latestDetail = await runTaskPlanInternal(nextItem.plan, {
|
|
17836
|
-
taskId: nextItem.taskId,
|
|
17837
|
-
runId: createTaskRunId(),
|
|
17838
|
-
queueItem: nextItem
|
|
17839
|
-
});
|
|
17840
|
-
if (taskId) {
|
|
17841
|
-
break;
|
|
17842
|
-
}
|
|
17843
|
-
}
|
|
17844
|
-
return latestDetail;
|
|
17845
|
-
}
|
|
17846
|
-
|
|
17847
|
-
async function startTaskQueueProcessing(options = {}) {
|
|
17848
|
-
const taskId = typeof options.taskId === 'string' ? options.taskId.trim() : '';
|
|
17849
|
-
const detach = options.detach === true;
|
|
17850
|
-
const queueItemById = taskId ? getTaskQueueItem(taskId) : null;
|
|
17851
|
-
if (taskId && !queueItemById) {
|
|
17852
|
-
return { error: `task not found: ${taskId}` };
|
|
17853
|
-
}
|
|
17854
|
-
if (queueItemById && queueItemById.status !== 'queued') {
|
|
17855
|
-
return { error: `task is not queued: ${taskId}` };
|
|
17856
|
-
}
|
|
17857
|
-
const detachedWorker = readTaskQueueWorkerState();
|
|
17858
|
-
const runner = async () => runTaskQueueProcessingInternal({ taskId });
|
|
17859
|
-
if (detach) {
|
|
17860
|
-
if (g_taskQueueProcessor || detachedWorker) {
|
|
17861
|
-
return {
|
|
17862
|
-
ok: true,
|
|
17863
|
-
started: false,
|
|
17864
|
-
alreadyRunning: true
|
|
17865
|
-
};
|
|
17866
|
-
}
|
|
17867
|
-
spawnDetachedTaskWorker({
|
|
17868
|
-
type: 'queue-runner',
|
|
17869
|
-
taskId
|
|
17870
|
-
});
|
|
17871
|
-
return {
|
|
17872
|
-
ok: true,
|
|
17873
|
-
started: true,
|
|
17874
|
-
detached: true
|
|
17875
|
-
};
|
|
17876
|
-
}
|
|
17877
|
-
if (detachedWorker) {
|
|
17878
|
-
return {
|
|
17879
|
-
ok: true,
|
|
17880
|
-
started: false,
|
|
17881
|
-
alreadyRunning: true
|
|
17882
|
-
};
|
|
17883
|
-
}
|
|
17884
|
-
if (g_taskQueueProcessor) {
|
|
17885
|
-
const detail = await g_taskQueueProcessor;
|
|
17886
|
-
return {
|
|
17887
|
-
ok: true,
|
|
17888
|
-
started: false,
|
|
17889
|
-
detached: false,
|
|
17890
|
-
detail,
|
|
17891
|
-
alreadyRunning: true
|
|
17892
|
-
};
|
|
17893
|
-
}
|
|
17894
|
-
g_taskQueueProcessor = runner()
|
|
17895
|
-
.catch(() => null)
|
|
17896
|
-
.finally(() => {
|
|
17897
|
-
g_taskQueueProcessor = null;
|
|
17898
|
-
});
|
|
17899
|
-
const detail = await g_taskQueueProcessor;
|
|
17900
|
-
return {
|
|
17901
|
-
ok: true,
|
|
17902
|
-
started: true,
|
|
17903
|
-
detached: false,
|
|
17904
|
-
detail
|
|
17905
|
-
};
|
|
17906
|
-
}
|
|
17907
|
-
|
|
17908
|
-
async function retryTaskRun(params = {}) {
|
|
17909
|
-
const runIdValidation = validateTaskRunId(params && typeof params.runId === 'string' ? params.runId : '');
|
|
17910
|
-
if (!runIdValidation.ok) {
|
|
17911
|
-
return { error: runIdValidation.error };
|
|
17912
|
-
}
|
|
17913
|
-
const detail = readTaskRunDetail(runIdValidation.runId);
|
|
17914
|
-
if (!detail || !detail.plan) {
|
|
17915
|
-
return { error: `task run not found: ${runIdValidation.runId}` };
|
|
17916
|
-
}
|
|
17917
|
-
const plan = cloneJson(detail.plan, {});
|
|
17918
|
-
const detach = params.detach === true;
|
|
17919
|
-
const nextRunId = createTaskRunId();
|
|
17920
|
-
if (detach) {
|
|
17921
|
-
spawnDetachedTaskWorker({
|
|
17922
|
-
type: 'run-plan',
|
|
17923
|
-
plan,
|
|
17924
|
-
taskId: detail.taskId || createTaskId(),
|
|
17925
|
-
runId: nextRunId
|
|
17926
|
-
});
|
|
17927
|
-
return {
|
|
17928
|
-
ok: true,
|
|
17929
|
-
started: true,
|
|
17930
|
-
detached: true,
|
|
17931
|
-
runId: nextRunId,
|
|
17932
|
-
taskId: detail.taskId || ''
|
|
17933
|
-
};
|
|
17934
|
-
}
|
|
17935
|
-
return runTaskPlanInternal(plan, {
|
|
17936
|
-
taskId: detail.taskId || createTaskId(),
|
|
17937
|
-
runId: nextRunId
|
|
17938
|
-
});
|
|
17939
|
-
}
|
|
17940
|
-
|
|
17941
|
-
function cancelTaskRunOrQueue(params = {}) {
|
|
17942
|
-
const rawTarget = typeof params.target === 'string' ? params.target.trim() : '';
|
|
17943
|
-
const runId = typeof params.runId === 'string' ? params.runId.trim() : '';
|
|
17944
|
-
const taskId = typeof params.taskId === 'string' ? params.taskId.trim() : '';
|
|
17945
|
-
const target = rawTarget || runId || taskId;
|
|
17946
|
-
if (!target) {
|
|
17947
|
-
return { error: 'taskId or runId is required' };
|
|
17948
|
-
}
|
|
17949
|
-
const controllerByRun = g_taskRunControllers.get(target)
|
|
17950
|
-
|| Array.from(g_taskRunControllers.values()).find((entry) => entry && entry.taskId === target);
|
|
17951
|
-
if (controllerByRun) {
|
|
17952
|
-
controllerByRun.abort();
|
|
17953
|
-
return {
|
|
17954
|
-
ok: true,
|
|
17955
|
-
cancelled: true,
|
|
17956
|
-
runId: controllerByRun.runId,
|
|
17957
|
-
taskId: controllerByRun.taskId,
|
|
17958
|
-
mode: 'running'
|
|
17959
|
-
};
|
|
17960
|
-
}
|
|
17961
|
-
const queueItem = getTaskQueueItem(target);
|
|
17962
|
-
if (queueItem) {
|
|
17963
|
-
if (queueItem.status === 'queued') {
|
|
17964
|
-
const next = upsertTaskQueueItem({
|
|
17965
|
-
...queueItem,
|
|
17966
|
-
status: 'cancelled',
|
|
17967
|
-
runStatus: 'cancelled',
|
|
17968
|
-
updatedAt: toIsoTime(Date.now()),
|
|
17969
|
-
lastSummary: queueItem.lastSummary || '已取消'
|
|
17970
|
-
});
|
|
17971
|
-
if (next && next.error) {
|
|
17972
|
-
return { error: next.error };
|
|
17973
|
-
}
|
|
17974
|
-
return {
|
|
17975
|
-
ok: true,
|
|
17976
|
-
cancelled: true,
|
|
17977
|
-
task: next,
|
|
17978
|
-
mode: 'queued'
|
|
17979
|
-
};
|
|
17980
|
-
}
|
|
17981
|
-
if (queueItem.lastRunId && g_taskRunControllers.has(queueItem.lastRunId)) {
|
|
17982
|
-
const active = g_taskRunControllers.get(queueItem.lastRunId);
|
|
17983
|
-
active.abort();
|
|
17984
|
-
return {
|
|
17985
|
-
ok: true,
|
|
17986
|
-
cancelled: true,
|
|
17987
|
-
runId: active.runId,
|
|
17988
|
-
taskId: active.taskId,
|
|
17989
|
-
mode: 'running'
|
|
17990
|
-
};
|
|
17991
|
-
}
|
|
17992
|
-
if (queueItem.lastRunId) {
|
|
17993
|
-
const detachedResult = signalDetachedTaskWorker(readTaskRunDetail(queueItem.lastRunId) || {});
|
|
17994
|
-
if (!detachedResult.error) {
|
|
17995
|
-
return detachedResult;
|
|
17996
|
-
}
|
|
17997
|
-
}
|
|
17998
|
-
return {
|
|
17999
|
-
error: `task cannot be cancelled in current status: ${queueItem.status}`
|
|
18000
|
-
};
|
|
18001
|
-
}
|
|
18002
|
-
const detail = readTaskRunDetail(target);
|
|
18003
|
-
if (detail && g_taskRunControllers.has(detail.runId)) {
|
|
18004
|
-
const active = g_taskRunControllers.get(detail.runId);
|
|
18005
|
-
active.abort();
|
|
18006
|
-
return {
|
|
18007
|
-
ok: true,
|
|
18008
|
-
cancelled: true,
|
|
18009
|
-
runId: active.runId,
|
|
18010
|
-
taskId: active.taskId,
|
|
18011
|
-
mode: 'running'
|
|
18012
|
-
};
|
|
18013
|
-
}
|
|
18014
|
-
if (detail) {
|
|
18015
|
-
const detachedResult = signalDetachedTaskWorker(detail);
|
|
18016
|
-
if (!detachedResult.error) {
|
|
18017
|
-
return detachedResult;
|
|
18018
|
-
}
|
|
18019
|
-
}
|
|
18020
|
-
const detailByTaskId = findRunningTaskRunDetailByTaskId(target);
|
|
18021
|
-
if (detailByTaskId) {
|
|
18022
|
-
const detachedResult = signalDetachedTaskWorker(detailByTaskId);
|
|
18023
|
-
if (!detachedResult.error) {
|
|
18024
|
-
return detachedResult;
|
|
18025
|
-
}
|
|
18026
|
-
}
|
|
18027
|
-
return { error: `task/run not found: ${target}` };
|
|
18028
|
-
}
|
|
18029
|
-
|
|
18030
|
-
function getTaskLogs(params = {}) {
|
|
18031
|
-
const runIdValidation = validateTaskRunId(params && typeof params.runId === 'string' ? params.runId : '');
|
|
18032
|
-
if (!runIdValidation.ok) {
|
|
18033
|
-
return { error: runIdValidation.error };
|
|
18034
|
-
}
|
|
18035
|
-
const detail = readTaskRunDetail(runIdValidation.runId);
|
|
18036
|
-
if (!detail) {
|
|
18037
|
-
return { error: `task run not found: ${runIdValidation.runId}` };
|
|
18038
|
-
}
|
|
18039
|
-
const run = detail.run && typeof detail.run === 'object' ? detail.run : {};
|
|
18040
|
-
const lines = [];
|
|
18041
|
-
for (const node of Array.isArray(run.nodes) ? run.nodes : []) {
|
|
18042
|
-
lines.push(`# ${node.id}${node.title ? ` ${node.title}` : ''}`);
|
|
18043
|
-
const body = summarizeTaskLogs(node.logs || [], 120);
|
|
18044
|
-
if (body) {
|
|
18045
|
-
lines.push(body);
|
|
18046
|
-
} else {
|
|
18047
|
-
lines.push('(no logs)');
|
|
18048
|
-
}
|
|
18049
|
-
lines.push('');
|
|
18050
|
-
}
|
|
18051
|
-
return {
|
|
18052
|
-
runId: runIdValidation.runId,
|
|
18053
|
-
logs: lines.join('\n').trim(),
|
|
18054
|
-
detail
|
|
18055
|
-
};
|
|
18056
|
-
}
|
|
18057
|
-
|
|
18058
|
-
function writeDetachedTaskWorkerPayload(payload = {}) {
|
|
18059
|
-
const tempRoot = path.join(TASK_RUN_DETAILS_DIR, 'tmp');
|
|
18060
|
-
ensureDir(tempRoot);
|
|
18061
|
-
const payloadPath = path.join(tempRoot, `task-worker-${Date.now()}-${crypto.randomBytes(3).toString('hex')}.json`);
|
|
18062
|
-
writeJsonAtomic(payloadPath, payload);
|
|
18063
|
-
return payloadPath;
|
|
18064
|
-
}
|
|
18065
|
-
|
|
18066
|
-
function readDetachedTaskWorkerPayload(payloadPath = '') {
|
|
18067
|
-
const filePath = typeof payloadPath === 'string' ? payloadPath.trim() : '';
|
|
18068
|
-
if (!filePath) {
|
|
18069
|
-
return { error: 'task worker payload path is required' };
|
|
18070
|
-
}
|
|
18071
|
-
const parsed = readJsonObjectFromFile(filePath, {});
|
|
18072
|
-
try {
|
|
18073
|
-
fs.unlinkSync(filePath);
|
|
18074
|
-
} catch (_) { }
|
|
18075
|
-
if (!parsed.ok || !parsed.exists) {
|
|
18076
|
-
return { error: parsed.error || 'task worker payload not found' };
|
|
18077
|
-
}
|
|
18078
|
-
return { payload: parsed.data && typeof parsed.data === 'object' ? parsed.data : {} };
|
|
18079
|
-
}
|
|
18080
|
-
|
|
18081
|
-
function spawnDetachedTaskWorker(payload = {}) {
|
|
18082
|
-
const payloadPath = writeDetachedTaskWorkerPayload(payload);
|
|
18083
|
-
const child = spawn(process.execPath, [__filename, '__task-worker', payloadPath], {
|
|
18084
|
-
stdio: 'ignore',
|
|
18085
|
-
detached: true,
|
|
18086
|
-
windowsHide: true
|
|
18087
|
-
});
|
|
18088
|
-
child.on('error', () => { });
|
|
18089
|
-
if (typeof child.unref === 'function') {
|
|
18090
|
-
child.unref();
|
|
18091
|
-
}
|
|
18092
|
-
return {
|
|
18093
|
-
ok: true,
|
|
18094
|
-
detached: true,
|
|
18095
|
-
pid: child.pid || 0
|
|
18096
|
-
};
|
|
18097
|
-
}
|
|
18098
|
-
|
|
18099
|
-
function isLiveProcessId(value) {
|
|
18100
|
-
const pid = Number.isFinite(Number(value)) ? Math.floor(Number(value)) : 0;
|
|
18101
|
-
if (!pid) {
|
|
18102
|
-
return false;
|
|
18103
|
-
}
|
|
18104
|
-
try {
|
|
18105
|
-
process.kill(pid, 0);
|
|
18106
|
-
return true;
|
|
18107
|
-
} catch (_) {
|
|
18108
|
-
return false;
|
|
18109
|
-
}
|
|
18110
|
-
}
|
|
18111
|
-
|
|
18112
|
-
function isTaskWorkerProcessId(value) {
|
|
18113
|
-
const pid = Number.isFinite(Number(value)) ? Math.floor(Number(value)) : 0;
|
|
18114
|
-
if (!isLiveProcessId(pid)) return false;
|
|
18115
|
-
if (process.platform === 'linux') {
|
|
18116
|
-
try {
|
|
18117
|
-
const raw = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf-8');
|
|
18118
|
-
return raw.includes('__task-worker');
|
|
18119
|
-
} catch (_) {
|
|
18120
|
-
return true;
|
|
18121
|
-
}
|
|
18122
|
-
}
|
|
18123
|
-
if (process.platform === 'darwin') {
|
|
18124
|
-
try {
|
|
18125
|
-
const probe = spawnSync('ps', ['-p', String(pid), '-o', 'command='], { encoding: 'utf-8', timeout: 1500 });
|
|
18126
|
-
if (probe.error || probe.status !== 0) return true;
|
|
18127
|
-
const cmd = String(probe.stdout || '');
|
|
18128
|
-
return cmd.includes('__task-worker');
|
|
18129
|
-
} catch (_) {
|
|
18130
|
-
return true;
|
|
18131
|
-
}
|
|
18132
|
-
}
|
|
18133
|
-
return true;
|
|
18134
|
-
}
|
|
18135
|
-
|
|
18136
|
-
function readTaskQueueWorkerState() {
|
|
18137
|
-
const parsed = readJsonObjectFromFile(TASK_QUEUE_WORKER_FILE, {});
|
|
18138
|
-
if (!parsed.ok || !parsed.exists || !parsed.data || typeof parsed.data !== 'object') {
|
|
18139
|
-
return null;
|
|
18140
|
-
}
|
|
18141
|
-
const state = parsed.data;
|
|
18142
|
-
if (!isTaskWorkerProcessId(state.pid)) {
|
|
18143
|
-
try {
|
|
18144
|
-
fs.unlinkSync(TASK_QUEUE_WORKER_FILE);
|
|
18145
|
-
} catch (_) { }
|
|
18146
|
-
return null;
|
|
18147
|
-
}
|
|
18148
|
-
return state;
|
|
18149
|
-
}
|
|
18150
|
-
|
|
18151
|
-
function writeTaskQueueWorkerState(state = {}) {
|
|
18152
|
-
ensureDir(path.dirname(TASK_QUEUE_WORKER_FILE));
|
|
18153
|
-
writeJsonAtomic(TASK_QUEUE_WORKER_FILE, {
|
|
18154
|
-
pid: process.pid,
|
|
18155
|
-
taskId: typeof state.taskId === 'string' ? state.taskId.trim() : '',
|
|
18156
|
-
startedAt: state.startedAt || toIsoTime(Date.now())
|
|
18157
|
-
});
|
|
18158
|
-
}
|
|
18159
|
-
|
|
18160
|
-
function clearTaskQueueWorkerState() {
|
|
18161
|
-
try {
|
|
18162
|
-
fs.unlinkSync(TASK_QUEUE_WORKER_FILE);
|
|
18163
|
-
} catch (_) { }
|
|
18164
|
-
}
|
|
18165
|
-
|
|
18166
|
-
function findRunningTaskRunDetailByTaskId(taskId = '') {
|
|
18167
|
-
const normalizedTaskId = typeof taskId === 'string' ? taskId.trim() : '';
|
|
18168
|
-
if (!normalizedTaskId || !fs.existsSync(TASK_RUN_DETAILS_DIR)) {
|
|
18169
|
-
return null;
|
|
18170
|
-
}
|
|
18171
|
-
let entries = [];
|
|
18172
|
-
try {
|
|
18173
|
-
entries = fs.readdirSync(TASK_RUN_DETAILS_DIR, { withFileTypes: true });
|
|
18174
|
-
} catch (_) {
|
|
18175
|
-
return null;
|
|
18176
|
-
}
|
|
18177
|
-
const candidates = [];
|
|
18178
|
-
for (const entry of entries) {
|
|
18179
|
-
if (!entry || !entry.isFile() || !entry.name.endsWith('.json')) {
|
|
18180
|
-
continue;
|
|
18181
|
-
}
|
|
18182
|
-
const detail = readTaskRunDetail(entry.name.slice(0, -5));
|
|
18183
|
-
if (!detail || detail.taskId !== normalizedTaskId) {
|
|
18184
|
-
continue;
|
|
18185
|
-
}
|
|
18186
|
-
const status = detail.run && detail.run.status ? detail.run.status : detail.status;
|
|
18187
|
-
if (status !== 'running') {
|
|
18188
|
-
continue;
|
|
18189
|
-
}
|
|
18190
|
-
candidates.push(detail);
|
|
18191
|
-
}
|
|
18192
|
-
candidates.sort((a, b) => String(b.updatedAt || b.createdAt || '').localeCompare(String(a.updatedAt || a.createdAt || '')));
|
|
18193
|
-
return candidates[0] || null;
|
|
18194
|
-
}
|
|
18195
|
-
|
|
18196
|
-
function signalDetachedTaskWorker(detail = {}) {
|
|
18197
|
-
const workerPid = Number.isFinite(Number(detail && detail.workerPid)) ? Math.floor(Number(detail.workerPid)) : 0;
|
|
18198
|
-
if (!workerPid) {
|
|
18199
|
-
return { error: 'task worker pid is missing' };
|
|
18200
|
-
}
|
|
18201
|
-
try {
|
|
18202
|
-
process.kill(workerPid, 'SIGTERM');
|
|
18203
|
-
return {
|
|
18204
|
-
ok: true,
|
|
18205
|
-
cancelled: true,
|
|
18206
|
-
runId: detail.runId || '',
|
|
18207
|
-
taskId: detail.taskId || '',
|
|
18208
|
-
workerPid,
|
|
18209
|
-
mode: 'running'
|
|
18210
|
-
};
|
|
18211
|
-
} catch (error) {
|
|
18212
|
-
return {
|
|
18213
|
-
error: error && error.code === 'ESRCH'
|
|
18214
|
-
? `task worker is not running: ${workerPid}`
|
|
18215
|
-
: (error && error.message ? error.message : 'failed to signal task worker')
|
|
18216
|
-
};
|
|
18217
|
-
}
|
|
18218
|
-
}
|
|
18219
|
-
|
|
18220
|
-
async function cmdTaskWorker(args = []) {
|
|
18221
|
-
const payloadResult = readDetachedTaskWorkerPayload(args[0] || '');
|
|
18222
|
-
if (payloadResult.error) {
|
|
18223
|
-
throw new Error(payloadResult.error);
|
|
18224
|
-
}
|
|
18225
|
-
const payload = payloadResult.payload || {};
|
|
18226
|
-
if (payload.type === 'run-plan') {
|
|
18227
|
-
const runId = typeof payload.runId === 'string' ? payload.runId.trim() : '';
|
|
18228
|
-
const taskId = typeof payload.taskId === 'string' ? payload.taskId.trim() : '';
|
|
18229
|
-
let cancelRequested = false;
|
|
18230
|
-
const cancelHandler = () => {
|
|
18231
|
-
if (cancelRequested) return;
|
|
18232
|
-
cancelRequested = true;
|
|
18233
|
-
cancelTaskRunOrQueue({ runId, taskId });
|
|
18234
|
-
};
|
|
18235
|
-
process.once('SIGTERM', cancelHandler);
|
|
18236
|
-
process.once('SIGINT', cancelHandler);
|
|
18237
|
-
try {
|
|
18238
|
-
const detail = await runTaskPlanInternal(payload.plan || {}, { taskId, runId });
|
|
18239
|
-
if (detail && detail.error) {
|
|
18240
|
-
throw new Error(detail.error);
|
|
18241
|
-
}
|
|
18242
|
-
if (detail && detail.run && detail.run.status === 'failed') {
|
|
18243
|
-
throw new Error(detail.run.error || 'task run failed');
|
|
18244
|
-
}
|
|
18245
|
-
return;
|
|
18246
|
-
} finally {
|
|
18247
|
-
process.removeListener('SIGTERM', cancelHandler);
|
|
18248
|
-
process.removeListener('SIGINT', cancelHandler);
|
|
18249
|
-
}
|
|
18250
|
-
}
|
|
18251
|
-
if (payload.type === 'queue-runner') {
|
|
18252
|
-
writeTaskQueueWorkerState({ taskId: payload.taskId || '', startedAt: toIsoTime(Date.now()) });
|
|
18253
|
-
try {
|
|
18254
|
-
await runTaskQueueProcessingInternal({ taskId: payload.taskId || '' });
|
|
18255
|
-
return;
|
|
18256
|
-
} finally {
|
|
18257
|
-
clearTaskQueueWorkerState();
|
|
18258
|
-
}
|
|
18259
|
-
}
|
|
18260
|
-
throw new Error(`unknown task worker payload type: ${payload.type || ''}`);
|
|
18261
|
-
}
|
|
18262
|
-
|
|
18263
15633
|
function createMcpTools(options = {}) {
|
|
18264
15634
|
const allowWrite = !!options.allowWrite;
|
|
18265
15635
|
const tools = [];
|
|
@@ -19096,7 +16466,6 @@ function printMainHelp() {
|
|
|
19096
16466
|
console.log(' codexmate add-model <模型> 添加模型');
|
|
19097
16467
|
console.log(' codexmate delete-model <模型> 删除模型');
|
|
19098
16468
|
console.log(' codexmate workflow <list|get|validate|run|runs> MCP 工作流中心');
|
|
19099
|
-
console.log(' codexmate task <plan|run|runs|queue|retry|cancel|logs> 本地任务编排');
|
|
19100
16469
|
console.log(' codexmate analytics export [--format csv|json] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--model <MODEL>] [--output <PATH|->] [-o <PATH|->] 导出 Usage 数据');
|
|
19101
16470
|
console.log(' codexmate run [--host <HOST>] [--no-browser] 启动 Web 界面');
|
|
19102
16471
|
console.log(' codexmate update [--check] 检查并快速更新工具');
|
|
@@ -19168,7 +16537,6 @@ async function main() {
|
|
|
19168
16537
|
};
|
|
19169
16538
|
|
|
19170
16539
|
switch (command) {
|
|
19171
|
-
case '__task-worker': await cmdTaskWorker(args.slice(1)); break;
|
|
19172
16540
|
case 'status': cmdStatus(); break;
|
|
19173
16541
|
case 'doctor': await cmdDoctor(args.slice(1)); break;
|
|
19174
16542
|
case 'import-skills': await cmdImportSkills(args.slice(1)); break;
|
|
@@ -19197,7 +16565,6 @@ async function main() {
|
|
|
19197
16565
|
case 'auth': cmdAuth(args.slice(1)); break;
|
|
19198
16566
|
case 'proxy': await cmdProxy(args.slice(1)); break;
|
|
19199
16567
|
case 'workflow': await cmdWorkflow(args.slice(1)); break;
|
|
19200
|
-
case 'task': await cmdTask(args.slice(1)); break;
|
|
19201
16568
|
case 'analytics': await cmdAnalytics(args.slice(1)); break;
|
|
19202
16569
|
case 'run': await cmdStart(parseStartOptions(args.slice(1))); break;
|
|
19203
16570
|
case 'update': await cmdToolUpdate(args.slice(1)); break;
|