codexmate 0.1.9 → 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 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', 'orchestration', 'market', 'plugins', 'docs', 'settings', 'trash', 'prompts']);
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
- return normalized === 'claude-project' ? 'claude-project' : 'codex';
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
- function printTaskHelp() {
14758
- console.log('\n用法: codexmate task <plan|run|runs|queue|retry|cancel|logs> [参数]');
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
- title: '',
14799
- target: '',
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 rest = [];
14819
- const pushValue = (key, value, optionName) => {
14820
- const text = value === undefined || value === null ? '' : String(value).trim();
14821
- if (!text) {
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[key].push(text);
14532
+ options.queuedFollowUps.push(raw);
14825
14533
  };
14826
- for (let i = 0; i < args.length; i += 1) {
14827
- const arg = String(args[i] || '');
14828
- if (!arg) continue;
14829
- if (arg === '--allow-write') {
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
- if (arg.startsWith('--task-id=')) {
15004
- options.taskId = arg.slice('--task-id='.length).trim();
15005
- options.explicit.taskId = true;
15006
- continue;
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 (arg === '--run-id') {
15009
- options.runId = String(args[i + 1] || '').trim();
15010
- options.explicit.runId = true;
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 (arg.startsWith('--run-id=')) {
15015
- options.runId = arg.slice('--run-id='.length).trim();
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 (arg.startsWith('--status=')) {
15025
- options.status = arg.slice('--status='.length).trim().toLowerCase();
14558
+ if (text.startsWith('--follow-up=')) {
14559
+ pushFollowUp(text.slice('--follow-up='.length), '--follow-up');
15026
14560
  continue;
15027
14561
  }
15028
- rest.push(arg);
14562
+ options.passthroughArgs.push(text);
15029
14563
  }
15030
- return { options, rest };
14564
+
14565
+ return options;
15031
14566
  }
14567
+ // #endregion parseCodexProxyOptions
15032
14568
 
15033
- function buildTaskCliPayload(options = {}, rest = []) {
15034
- const explicit = options && options.explicit && typeof options.explicit === 'object' ? options.explicit : {};
15035
- const payload = {};
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
- function printTaskRunSummary(detail = {}) {
15092
- const run = detail.run && typeof detail.run === 'object' ? detail.run : {};
15093
- console.log(`\n任务执行 ${run.status === 'success' ? '完成' : '结束'}: ${detail.title || detail.taskId || ''}`.trim());
15094
- console.log(` taskId: ${detail.taskId || ''}`);
15095
- console.log(` runId: ${detail.runId || ''}`);
15096
- console.log(` status: ${run.status || detail.status || 'unknown'}`);
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
- if (run.error) {
15102
- console.log(` error: ${run.error}`);
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
- const nodes = Array.isArray(run.nodes) ? run.nodes : [];
15105
- for (const node of nodes) {
15106
- console.log(` - ${node.id}: ${node.status || 'unknown'} attempts=${node.attemptCount || 0}`);
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;