codeharbor 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +5 -0
- package/README.md +21 -4
- package/dist/cli.js +551 -12
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -17,6 +17,11 @@ CODEX_EXTRA_ARGS=
|
|
|
17
17
|
# Optional JSON object for additional environment variables passed to codex child process.
|
|
18
18
|
CODEX_EXTRA_ENV_JSON=
|
|
19
19
|
|
|
20
|
+
# Multi-agent workflow (Phase B, opt-in).
|
|
21
|
+
AGENT_WORKFLOW_ENABLED=false
|
|
22
|
+
# Auto-repair rounds after reviewer rejects output.
|
|
23
|
+
AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS=1
|
|
24
|
+
|
|
20
25
|
# SQLite state database path.
|
|
21
26
|
STATE_DB_PATH=data/state.db
|
|
22
27
|
# Legacy JSON path for one-time migration import.
|
package/README.md
CHANGED
|
@@ -67,27 +67,33 @@ curl -fsSL https://raw.githubusercontent.com/biglone/CodeHarbor/main/scripts/ins
|
|
|
67
67
|
Install first, then enable systemd service with one command:
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
|
-
|
|
70
|
+
codeharbor service install
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
Install + enable main and admin services:
|
|
74
74
|
|
|
75
75
|
```bash
|
|
76
|
-
|
|
76
|
+
codeharbor service install --with-admin
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
Restart installed service(s):
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
|
-
|
|
82
|
+
codeharbor service restart --with-admin
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
Remove installed services:
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
|
-
|
|
88
|
+
codeharbor service uninstall --with-admin
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
Notes:
|
|
92
|
+
|
|
93
|
+
- Service commands auto-elevate with `sudo` when root privileges are required.
|
|
94
|
+
- If your environment blocks interactive `sudo`, use explicit fallback:
|
|
95
|
+
- `sudo <node-bin> <codeharbor-cli-script> service ...`
|
|
96
|
+
|
|
91
97
|
Enable Admin service at install time:
|
|
92
98
|
|
|
93
99
|
```bash
|
|
@@ -391,6 +397,17 @@ If any check fails, it prints actionable fix commands (for example `codeharbor i
|
|
|
391
397
|
- `/status` show session + limiter + metrics + runtime worker status
|
|
392
398
|
- `/reset` clear bound Codex session and keep conversation active
|
|
393
399
|
- `/stop` cancel in-flight execution (if running) and reset session context
|
|
400
|
+
- `/agents status` show multi-agent workflow status for current session (when enabled)
|
|
401
|
+
- `/agents run <objective>` run Planner -> Executor -> Reviewer workflow (when enabled)
|
|
402
|
+
|
|
403
|
+
### Multi-Agent Workflow (Phase B, Opt-In)
|
|
404
|
+
|
|
405
|
+
- `AGENT_WORKFLOW_ENABLED=true`
|
|
406
|
+
- enable `/agents` workflow commands
|
|
407
|
+
- `AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS`
|
|
408
|
+
- reviewer reject loop upper bound (default `1`)
|
|
409
|
+
|
|
410
|
+
Default is disabled to keep legacy behavior unchanged.
|
|
394
411
|
|
|
395
412
|
## CLI Compatibility Mode
|
|
396
413
|
|
package/dist/cli.js
CHANGED
|
@@ -24,6 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/cli.ts
|
|
27
|
+
var import_node_child_process6 = require("child_process");
|
|
27
28
|
var import_node_fs11 = __toESM(require("fs"));
|
|
28
29
|
var import_node_path12 = __toESM(require("path"));
|
|
29
30
|
var import_commander = require("commander");
|
|
@@ -599,6 +600,27 @@ var AdminServer = class {
|
|
|
599
600
|
updatedKeys.push("cliCompat.fetchMedia");
|
|
600
601
|
}
|
|
601
602
|
}
|
|
603
|
+
if ("agentWorkflow" in body) {
|
|
604
|
+
const workflow = asObject(body.agentWorkflow, "agentWorkflow");
|
|
605
|
+
const currentAgentWorkflow = ensureAgentWorkflowConfig(this.config);
|
|
606
|
+
if ("enabled" in workflow) {
|
|
607
|
+
const value = normalizeBoolean(workflow.enabled, currentAgentWorkflow.enabled);
|
|
608
|
+
currentAgentWorkflow.enabled = value;
|
|
609
|
+
envUpdates.AGENT_WORKFLOW_ENABLED = String(value);
|
|
610
|
+
updatedKeys.push("agentWorkflow.enabled");
|
|
611
|
+
}
|
|
612
|
+
if ("autoRepairMaxRounds" in workflow) {
|
|
613
|
+
const value = normalizePositiveInt(
|
|
614
|
+
workflow.autoRepairMaxRounds,
|
|
615
|
+
currentAgentWorkflow.autoRepairMaxRounds,
|
|
616
|
+
0,
|
|
617
|
+
10
|
|
618
|
+
);
|
|
619
|
+
currentAgentWorkflow.autoRepairMaxRounds = value;
|
|
620
|
+
envUpdates.AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS = String(value);
|
|
621
|
+
updatedKeys.push("agentWorkflow.autoRepairMaxRounds");
|
|
622
|
+
}
|
|
623
|
+
}
|
|
602
624
|
if (updatedKeys.length === 0) {
|
|
603
625
|
throw new HttpError(400, "No supported global config fields provided.");
|
|
604
626
|
}
|
|
@@ -719,8 +741,22 @@ function buildGlobalConfigSnapshot(config) {
|
|
|
719
741
|
matrixProgressMinIntervalMs: config.matrixProgressMinIntervalMs,
|
|
720
742
|
matrixTypingTimeoutMs: config.matrixTypingTimeoutMs,
|
|
721
743
|
sessionActiveWindowMinutes: config.sessionActiveWindowMinutes,
|
|
722
|
-
cliCompat: { ...config.cliCompat }
|
|
744
|
+
cliCompat: { ...config.cliCompat },
|
|
745
|
+
agentWorkflow: { ...ensureAgentWorkflowConfig(config) }
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
function ensureAgentWorkflowConfig(config) {
|
|
749
|
+
const mutable = config;
|
|
750
|
+
const existing = mutable.agentWorkflow;
|
|
751
|
+
if (existing && typeof existing.enabled === "boolean" && Number.isFinite(existing.autoRepairMaxRounds)) {
|
|
752
|
+
return existing;
|
|
753
|
+
}
|
|
754
|
+
const fallback = {
|
|
755
|
+
enabled: false,
|
|
756
|
+
autoRepairMaxRounds: 1
|
|
723
757
|
};
|
|
758
|
+
mutable.agentWorkflow = fallback;
|
|
759
|
+
return fallback;
|
|
724
760
|
}
|
|
725
761
|
function formatAuditEntry(entry) {
|
|
726
762
|
return {
|
|
@@ -1221,6 +1257,11 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
1221
1257
|
<input id="global-cli-throttle" type="number" min="0" />
|
|
1222
1258
|
</label>
|
|
1223
1259
|
<label class="checkbox"><input id="global-cli-fetch-media" type="checkbox" /><span>Fetch media attachments</span></label>
|
|
1260
|
+
<label class="checkbox"><input id="global-agent-enabled" type="checkbox" /><span>Enable multi-agent workflow</span></label>
|
|
1261
|
+
<label class="field">
|
|
1262
|
+
<span class="field-label">Workflow auto-repair rounds</span>
|
|
1263
|
+
<input id="global-agent-repair-rounds" type="number" min="0" max="10" />
|
|
1264
|
+
</label>
|
|
1224
1265
|
</div>
|
|
1225
1266
|
<div class="actions">
|
|
1226
1267
|
<button id="global-save-btn" type="button">Save Global Config</button>
|
|
@@ -1488,6 +1529,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
1488
1529
|
var rateLimiter = data.rateLimiter || {};
|
|
1489
1530
|
var trigger = data.defaultGroupTriggerPolicy || {};
|
|
1490
1531
|
var cliCompat = data.cliCompat || {};
|
|
1532
|
+
var agentWorkflow = data.agentWorkflow || {};
|
|
1491
1533
|
|
|
1492
1534
|
document.getElementById("global-matrix-prefix").value = data.matrixCommandPrefix || "";
|
|
1493
1535
|
document.getElementById("global-workdir").value = data.codexWorkdir || "";
|
|
@@ -1513,6 +1555,10 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
1513
1555
|
document.getElementById("global-cli-disable-split").checked = Boolean(cliCompat.disableReplyChunkSplit);
|
|
1514
1556
|
document.getElementById("global-cli-throttle").value = String(cliCompat.progressThrottleMs || 0);
|
|
1515
1557
|
document.getElementById("global-cli-fetch-media").checked = Boolean(cliCompat.fetchMedia);
|
|
1558
|
+
document.getElementById("global-agent-enabled").checked = Boolean(agentWorkflow.enabled);
|
|
1559
|
+
document.getElementById("global-agent-repair-rounds").value = String(
|
|
1560
|
+
typeof agentWorkflow.autoRepairMaxRounds === "number" ? agentWorkflow.autoRepairMaxRounds : 1
|
|
1561
|
+
);
|
|
1516
1562
|
|
|
1517
1563
|
showNotice("ok", "Global config loaded.");
|
|
1518
1564
|
} catch (error) {
|
|
@@ -1550,6 +1596,10 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
1550
1596
|
disableReplyChunkSplit: asBool("global-cli-disable-split"),
|
|
1551
1597
|
progressThrottleMs: asNumber("global-cli-throttle", 300),
|
|
1552
1598
|
fetchMedia: asBool("global-cli-fetch-media")
|
|
1599
|
+
},
|
|
1600
|
+
agentWorkflow: {
|
|
1601
|
+
enabled: asBool("global-agent-enabled"),
|
|
1602
|
+
autoRepairMaxRounds: asNumber("global-agent-repair-rounds", 1)
|
|
1553
1603
|
}
|
|
1554
1604
|
};
|
|
1555
1605
|
var response = await apiRequest("/api/admin/config/global", "PUT", body);
|
|
@@ -2880,6 +2930,273 @@ function computeRetryAfter(timestamps, windowMs, now) {
|
|
|
2880
2930
|
return Math.max(0, oldest + windowMs - now);
|
|
2881
2931
|
}
|
|
2882
2932
|
|
|
2933
|
+
// src/workflow/multi-agent-workflow.ts
|
|
2934
|
+
var MultiAgentWorkflowRunner = class {
|
|
2935
|
+
executor;
|
|
2936
|
+
logger;
|
|
2937
|
+
config;
|
|
2938
|
+
constructor(executor, logger, config) {
|
|
2939
|
+
this.executor = executor;
|
|
2940
|
+
this.logger = logger;
|
|
2941
|
+
this.config = config;
|
|
2942
|
+
}
|
|
2943
|
+
isEnabled() {
|
|
2944
|
+
return this.config.enabled;
|
|
2945
|
+
}
|
|
2946
|
+
async run(input) {
|
|
2947
|
+
const startedAt = Date.now();
|
|
2948
|
+
const objective = input.objective.trim();
|
|
2949
|
+
if (!objective) {
|
|
2950
|
+
throw new Error("workflow objective cannot be empty.");
|
|
2951
|
+
}
|
|
2952
|
+
const maxRepairRounds = Math.max(0, this.config.autoRepairMaxRounds);
|
|
2953
|
+
let plannerSessionId = null;
|
|
2954
|
+
let executorSessionId = null;
|
|
2955
|
+
let reviewerSessionId = null;
|
|
2956
|
+
let activeHandle = null;
|
|
2957
|
+
let cancelled = false;
|
|
2958
|
+
input.onRegisterCancel?.(() => {
|
|
2959
|
+
cancelled = true;
|
|
2960
|
+
activeHandle?.cancel();
|
|
2961
|
+
});
|
|
2962
|
+
await emitProgress(input, {
|
|
2963
|
+
stage: "planner",
|
|
2964
|
+
round: 0,
|
|
2965
|
+
message: "Planner \u6B63\u5728\u751F\u6210\u6267\u884C\u8BA1\u5212"
|
|
2966
|
+
});
|
|
2967
|
+
const planResult = await this.executeRole(
|
|
2968
|
+
"planner",
|
|
2969
|
+
buildPlannerPrompt(objective),
|
|
2970
|
+
plannerSessionId,
|
|
2971
|
+
input.workdir,
|
|
2972
|
+
() => cancelled,
|
|
2973
|
+
(handle) => {
|
|
2974
|
+
activeHandle = handle;
|
|
2975
|
+
}
|
|
2976
|
+
);
|
|
2977
|
+
plannerSessionId = planResult.sessionId;
|
|
2978
|
+
const plan = planResult.reply;
|
|
2979
|
+
await emitProgress(input, {
|
|
2980
|
+
stage: "executor",
|
|
2981
|
+
round: 0,
|
|
2982
|
+
message: "Executor \u6B63\u5728\u6839\u636E\u8BA1\u5212\u6267\u884C\u4EFB\u52A1"
|
|
2983
|
+
});
|
|
2984
|
+
let outputResult = await this.executeRole(
|
|
2985
|
+
"executor",
|
|
2986
|
+
buildExecutorPrompt(objective, plan),
|
|
2987
|
+
executorSessionId,
|
|
2988
|
+
input.workdir,
|
|
2989
|
+
() => cancelled,
|
|
2990
|
+
(handle) => {
|
|
2991
|
+
activeHandle = handle;
|
|
2992
|
+
}
|
|
2993
|
+
);
|
|
2994
|
+
executorSessionId = outputResult.sessionId;
|
|
2995
|
+
let finalReviewReply = "";
|
|
2996
|
+
let approved = false;
|
|
2997
|
+
let repairRounds = 0;
|
|
2998
|
+
for (let attempt = 0; attempt <= maxRepairRounds; attempt += 1) {
|
|
2999
|
+
await emitProgress(input, {
|
|
3000
|
+
stage: "reviewer",
|
|
3001
|
+
round: attempt,
|
|
3002
|
+
message: `Reviewer \u6B63\u5728\u8FDB\u884C\u8D28\u91CF\u5BA1\u67E5\uFF08round ${attempt + 1}\uFF09`
|
|
3003
|
+
});
|
|
3004
|
+
const reviewResult = await this.executeRole(
|
|
3005
|
+
"reviewer",
|
|
3006
|
+
buildReviewerPrompt(objective, plan, outputResult.reply),
|
|
3007
|
+
reviewerSessionId,
|
|
3008
|
+
input.workdir,
|
|
3009
|
+
() => cancelled,
|
|
3010
|
+
(handle) => {
|
|
3011
|
+
activeHandle = handle;
|
|
3012
|
+
}
|
|
3013
|
+
);
|
|
3014
|
+
reviewerSessionId = reviewResult.sessionId;
|
|
3015
|
+
finalReviewReply = reviewResult.reply;
|
|
3016
|
+
const verdict = parseReviewerVerdict(finalReviewReply);
|
|
3017
|
+
if (verdict.approved) {
|
|
3018
|
+
approved = true;
|
|
3019
|
+
break;
|
|
3020
|
+
}
|
|
3021
|
+
if (attempt >= maxRepairRounds) {
|
|
3022
|
+
break;
|
|
3023
|
+
}
|
|
3024
|
+
repairRounds = attempt + 1;
|
|
3025
|
+
await emitProgress(input, {
|
|
3026
|
+
stage: "repair",
|
|
3027
|
+
round: repairRounds,
|
|
3028
|
+
message: `Executor \u6B63\u5728\u6309 Reviewer \u53CD\u9988\u8FDB\u884C\u4FEE\u590D\uFF08round ${repairRounds}\uFF09`
|
|
3029
|
+
});
|
|
3030
|
+
outputResult = await this.executeRole(
|
|
3031
|
+
"executor",
|
|
3032
|
+
buildRepairPrompt(objective, plan, outputResult.reply, verdict.feedback, repairRounds),
|
|
3033
|
+
executorSessionId,
|
|
3034
|
+
input.workdir,
|
|
3035
|
+
() => cancelled,
|
|
3036
|
+
(handle) => {
|
|
3037
|
+
activeHandle = handle;
|
|
3038
|
+
}
|
|
3039
|
+
);
|
|
3040
|
+
executorSessionId = outputResult.sessionId;
|
|
3041
|
+
}
|
|
3042
|
+
const durationMs = Date.now() - startedAt;
|
|
3043
|
+
this.logger.info("Multi-agent workflow finished", {
|
|
3044
|
+
objective,
|
|
3045
|
+
approved,
|
|
3046
|
+
repairRounds,
|
|
3047
|
+
durationMs
|
|
3048
|
+
});
|
|
3049
|
+
return {
|
|
3050
|
+
objective,
|
|
3051
|
+
plan,
|
|
3052
|
+
output: outputResult.reply,
|
|
3053
|
+
review: finalReviewReply,
|
|
3054
|
+
approved,
|
|
3055
|
+
repairRounds,
|
|
3056
|
+
durationMs
|
|
3057
|
+
};
|
|
3058
|
+
}
|
|
3059
|
+
async executeRole(role, prompt, sessionId, workdir, getCancelled, setActiveHandle) {
|
|
3060
|
+
if (getCancelled()) {
|
|
3061
|
+
throw new CodexExecutionCancelledError("workflow cancelled");
|
|
3062
|
+
}
|
|
3063
|
+
const handle = this.executor.startExecution(prompt, sessionId, void 0, { workdir });
|
|
3064
|
+
setActiveHandle(handle);
|
|
3065
|
+
try {
|
|
3066
|
+
const result = await handle.result;
|
|
3067
|
+
return {
|
|
3068
|
+
sessionId: result.sessionId,
|
|
3069
|
+
reply: result.reply
|
|
3070
|
+
};
|
|
3071
|
+
} finally {
|
|
3072
|
+
setActiveHandle(null);
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
};
|
|
3076
|
+
async function emitProgress(input, event) {
|
|
3077
|
+
if (!input.onProgress) {
|
|
3078
|
+
return;
|
|
3079
|
+
}
|
|
3080
|
+
await input.onProgress(event);
|
|
3081
|
+
}
|
|
3082
|
+
function buildPlannerPrompt(objective) {
|
|
3083
|
+
return [
|
|
3084
|
+
"[role:planner]",
|
|
3085
|
+
"\u4F60\u662F\u8F6F\u4EF6\u4EA4\u4ED8\u89C4\u5212\u4EE3\u7406\u3002\u8BF7\u57FA\u4E8E\u76EE\u6807\u7ED9\u51FA\u53EF\u6267\u884C\u8BA1\u5212\u3002",
|
|
3086
|
+
"\u8F93\u51FA\u8981\u6C42\uFF1A",
|
|
3087
|
+
"1. \u4EFB\u52A1\u62C6\u89E3\uFF083-7 \u6B65\uFF09",
|
|
3088
|
+
"2. \u6BCF\u6B65\u8F93\u5165/\u8F93\u51FA",
|
|
3089
|
+
"3. \u98CE\u9669\u4E0E\u56DE\u9000\u65B9\u6848",
|
|
3090
|
+
"",
|
|
3091
|
+
`\u76EE\u6807\uFF1A${objective}`
|
|
3092
|
+
].join("\n");
|
|
3093
|
+
}
|
|
3094
|
+
function buildExecutorPrompt(objective, plan) {
|
|
3095
|
+
return [
|
|
3096
|
+
"[role:executor]",
|
|
3097
|
+
"\u4F60\u662F\u8F6F\u4EF6\u6267\u884C\u4EE3\u7406\u3002\u8BF7\u6839\u636E\u8BA1\u5212\u5B8C\u6210\u4EA4\u4ED8\u5185\u5BB9\u3002",
|
|
3098
|
+
"\u8F93\u51FA\u8981\u6C42\uFF1A",
|
|
3099
|
+
"1. \u76F4\u63A5\u7ED9\u51FA\u6700\u7EC8\u53EF\u6267\u884C\u7ED3\u679C",
|
|
3100
|
+
"2. \u8BF4\u660E\u4F60\u5B9E\u9645\u5B8C\u6210\u4E86\u54EA\u4E9B\u6B65\u9AA4",
|
|
3101
|
+
"3. \u5982\u679C\u9700\u8981\u6587\u4EF6\u843D\u76D8\uFF0C\u8BF7\u7ED9\u51FA\u7EDD\u5BF9\u8DEF\u5F84",
|
|
3102
|
+
"",
|
|
3103
|
+
`\u76EE\u6807\uFF1A${objective}`,
|
|
3104
|
+
"",
|
|
3105
|
+
"[planner_plan]",
|
|
3106
|
+
plan,
|
|
3107
|
+
"[/planner_plan]"
|
|
3108
|
+
].join("\n");
|
|
3109
|
+
}
|
|
3110
|
+
function buildReviewerPrompt(objective, plan, output) {
|
|
3111
|
+
return [
|
|
3112
|
+
"[role:reviewer]",
|
|
3113
|
+
"\u4F60\u662F\u8D28\u91CF\u5BA1\u67E5\u4EE3\u7406\u3002\u8BF7\u4E25\u683C\u5BA1\u67E5\u6267\u884C\u7ED3\u679C\u662F\u5426\u8FBE\u6210\u76EE\u6807\u3002",
|
|
3114
|
+
"\u8F93\u51FA\u683C\u5F0F\u5FC5\u987B\u5305\u542B\u4EE5\u4E0B\u5B57\u6BB5\uFF1A",
|
|
3115
|
+
"VERDICT: APPROVED \u6216 REJECTED",
|
|
3116
|
+
"SUMMARY: \u4E00\u53E5\u8BDD\u603B\u7ED3",
|
|
3117
|
+
"ISSUES:",
|
|
3118
|
+
"- issue 1",
|
|
3119
|
+
"- issue 2",
|
|
3120
|
+
"SUGGESTIONS:",
|
|
3121
|
+
"- suggestion 1",
|
|
3122
|
+
"- suggestion 2",
|
|
3123
|
+
"",
|
|
3124
|
+
`\u76EE\u6807\uFF1A${objective}`,
|
|
3125
|
+
"",
|
|
3126
|
+
"[planner_plan]",
|
|
3127
|
+
plan,
|
|
3128
|
+
"[/planner_plan]",
|
|
3129
|
+
"",
|
|
3130
|
+
"[executor_output]",
|
|
3131
|
+
output,
|
|
3132
|
+
"[/executor_output]"
|
|
3133
|
+
].join("\n");
|
|
3134
|
+
}
|
|
3135
|
+
function buildRepairPrompt(objective, plan, previousOutput, reviewerFeedback, round) {
|
|
3136
|
+
return [
|
|
3137
|
+
"[role:executor]",
|
|
3138
|
+
`\u4F60\u662F\u8F6F\u4EF6\u6267\u884C\u4EE3\u7406\u3002\u8BF7\u6839\u636E\u5BA1\u67E5\u53CD\u9988\u8FDB\u884C\u7B2C ${round} \u8F6E\u4FEE\u590D\u5E76\u8F93\u51FA\u6700\u7EC8\u7248\u672C\u3002`,
|
|
3139
|
+
"\u8981\u6C42\uFF1A\u4FDD\u6301\u6B63\u786E\u5185\u5BB9\uFF0C\u4FEE\u590D\u95EE\u9898\uFF0C\u4E0D\u8981\u4E22\u5931\u5DF2\u5B8C\u6210\u90E8\u5206\u3002",
|
|
3140
|
+
"",
|
|
3141
|
+
`\u76EE\u6807\uFF1A${objective}`,
|
|
3142
|
+
"",
|
|
3143
|
+
"[planner_plan]",
|
|
3144
|
+
plan,
|
|
3145
|
+
"[/planner_plan]",
|
|
3146
|
+
"",
|
|
3147
|
+
"[previous_output]",
|
|
3148
|
+
previousOutput,
|
|
3149
|
+
"[/previous_output]",
|
|
3150
|
+
"",
|
|
3151
|
+
"[reviewer_feedback]",
|
|
3152
|
+
reviewerFeedback,
|
|
3153
|
+
"[/reviewer_feedback]"
|
|
3154
|
+
].join("\n");
|
|
3155
|
+
}
|
|
3156
|
+
function parseReviewerVerdict(review) {
|
|
3157
|
+
const approved = /\bVERDICT\s*:\s*APPROVED\b/i.test(review);
|
|
3158
|
+
const rejected = /\bVERDICT\s*:\s*REJECTED\b/i.test(review);
|
|
3159
|
+
if (approved) {
|
|
3160
|
+
return { approved: true, feedback: review };
|
|
3161
|
+
}
|
|
3162
|
+
if (rejected) {
|
|
3163
|
+
return { approved: false, feedback: review };
|
|
3164
|
+
}
|
|
3165
|
+
return {
|
|
3166
|
+
approved: false,
|
|
3167
|
+
feedback: review.trim() || "Reviewer \u672A\u8FD4\u56DE\u89C4\u8303 verdict\uFF0C\u9ED8\u8BA4\u6309 REJECTED \u5904\u7406\u3002"
|
|
3168
|
+
};
|
|
3169
|
+
}
|
|
3170
|
+
function parseWorkflowCommand(text) {
|
|
3171
|
+
const normalized = text.trim();
|
|
3172
|
+
if (!normalized.startsWith("/agents")) {
|
|
3173
|
+
return null;
|
|
3174
|
+
}
|
|
3175
|
+
const parts = normalized.split(/\s+/);
|
|
3176
|
+
if (parts.length === 1 || parts[1]?.toLowerCase() === "status") {
|
|
3177
|
+
return { kind: "status" };
|
|
3178
|
+
}
|
|
3179
|
+
if (parts[1]?.toLowerCase() !== "run") {
|
|
3180
|
+
return null;
|
|
3181
|
+
}
|
|
3182
|
+
const objective = normalized.replace(/^\/agents\s+run\s*/i, "").trim();
|
|
3183
|
+
return {
|
|
3184
|
+
kind: "run",
|
|
3185
|
+
objective
|
|
3186
|
+
};
|
|
3187
|
+
}
|
|
3188
|
+
function createIdleWorkflowSnapshot() {
|
|
3189
|
+
return {
|
|
3190
|
+
state: "idle",
|
|
3191
|
+
startedAt: null,
|
|
3192
|
+
endedAt: null,
|
|
3193
|
+
objective: null,
|
|
3194
|
+
approved: null,
|
|
3195
|
+
repairRounds: 0,
|
|
3196
|
+
error: null
|
|
3197
|
+
};
|
|
3198
|
+
}
|
|
3199
|
+
|
|
2883
3200
|
// src/orchestrator.ts
|
|
2884
3201
|
var RequestMetrics = class {
|
|
2885
3202
|
total = 0;
|
|
@@ -2965,6 +3282,8 @@ var Orchestrator = class {
|
|
|
2965
3282
|
rateLimiter;
|
|
2966
3283
|
cliCompat;
|
|
2967
3284
|
cliCompatRecorder;
|
|
3285
|
+
workflowRunner;
|
|
3286
|
+
workflowSnapshots = /* @__PURE__ */ new Map();
|
|
2968
3287
|
metrics = new RequestMetrics();
|
|
2969
3288
|
lastLockPruneAt = 0;
|
|
2970
3289
|
constructor(channel, executor, stateStore, logger, options) {
|
|
@@ -3011,6 +3330,10 @@ var Orchestrator = class {
|
|
|
3011
3330
|
maxConcurrentPerRoom: 4
|
|
3012
3331
|
}
|
|
3013
3332
|
);
|
|
3333
|
+
this.workflowRunner = new MultiAgentWorkflowRunner(this.executor, this.logger, {
|
|
3334
|
+
enabled: options?.multiAgentWorkflow?.enabled ?? false,
|
|
3335
|
+
autoRepairMaxRounds: options?.multiAgentWorkflow?.autoRepairMaxRounds ?? 1
|
|
3336
|
+
});
|
|
3014
3337
|
this.sessionRuntime = new CodexSessionRuntime(this.executor);
|
|
3015
3338
|
}
|
|
3016
3339
|
async handleMessage(message) {
|
|
@@ -3054,6 +3377,12 @@ var Orchestrator = class {
|
|
|
3054
3377
|
this.stateStore.markEventProcessed(sessionKey, message.eventId);
|
|
3055
3378
|
return;
|
|
3056
3379
|
}
|
|
3380
|
+
const workflowCommand = this.workflowRunner.isEnabled() ? parseWorkflowCommand(route.prompt) : null;
|
|
3381
|
+
if (workflowCommand?.kind === "status") {
|
|
3382
|
+
await this.handleWorkflowStatusCommand(sessionKey, message);
|
|
3383
|
+
this.stateStore.markEventProcessed(sessionKey, message.eventId);
|
|
3384
|
+
return;
|
|
3385
|
+
}
|
|
3057
3386
|
const rateDecision = this.rateLimiter.tryAcquire({
|
|
3058
3387
|
userId: message.senderId,
|
|
3059
3388
|
roomId: message.conversationId
|
|
@@ -3071,6 +3400,37 @@ var Orchestrator = class {
|
|
|
3071
3400
|
});
|
|
3072
3401
|
return;
|
|
3073
3402
|
}
|
|
3403
|
+
if (workflowCommand?.kind === "run") {
|
|
3404
|
+
const executionStartedAt = Date.now();
|
|
3405
|
+
let sendDurationMs2 = 0;
|
|
3406
|
+
this.stateStore.activateSession(sessionKey, this.sessionActiveWindowMs);
|
|
3407
|
+
try {
|
|
3408
|
+
const sendStartedAt = Date.now();
|
|
3409
|
+
await this.handleWorkflowRunCommand(
|
|
3410
|
+
workflowCommand.objective,
|
|
3411
|
+
sessionKey,
|
|
3412
|
+
message,
|
|
3413
|
+
requestId,
|
|
3414
|
+
roomConfig.workdir
|
|
3415
|
+
);
|
|
3416
|
+
sendDurationMs2 += Date.now() - sendStartedAt;
|
|
3417
|
+
this.stateStore.markEventProcessed(sessionKey, message.eventId);
|
|
3418
|
+
this.metrics.record("success", queueWaitMs, Date.now() - executionStartedAt, sendDurationMs2);
|
|
3419
|
+
} catch (error) {
|
|
3420
|
+
sendDurationMs2 += await this.sendWorkflowFailure(message.conversationId, error);
|
|
3421
|
+
this.stateStore.commitExecutionHandled(sessionKey, message.eventId);
|
|
3422
|
+
const status = classifyExecutionOutcome(error);
|
|
3423
|
+
this.metrics.record(status, queueWaitMs, Date.now() - executionStartedAt, sendDurationMs2);
|
|
3424
|
+
this.logger.error("Workflow request failed", {
|
|
3425
|
+
requestId,
|
|
3426
|
+
sessionKey,
|
|
3427
|
+
error: formatError2(error)
|
|
3428
|
+
});
|
|
3429
|
+
} finally {
|
|
3430
|
+
rateDecision.release?.();
|
|
3431
|
+
}
|
|
3432
|
+
return;
|
|
3433
|
+
}
|
|
3074
3434
|
this.stateStore.activateSession(sessionKey, this.sessionActiveWindowMs);
|
|
3075
3435
|
const previousCodexSessionId = this.stateStore.getCodexSessionId(sessionKey);
|
|
3076
3436
|
const executionPrompt = this.buildExecutionPrompt(route.prompt, message);
|
|
@@ -3291,6 +3651,7 @@ var Orchestrator = class {
|
|
|
3291
3651
|
const metrics = this.metrics.snapshot(this.runningExecutions.size);
|
|
3292
3652
|
const limiter = this.rateLimiter.snapshot();
|
|
3293
3653
|
const runtime = this.sessionRuntime.getRuntimeStats();
|
|
3654
|
+
const workflow = this.workflowSnapshots.get(sessionKey) ?? createIdleWorkflowSnapshot();
|
|
3294
3655
|
await this.channel.sendNotice(
|
|
3295
3656
|
message.conversationId,
|
|
3296
3657
|
`[CodeHarbor] \u5F53\u524D\u72B6\u6001
|
|
@@ -3303,9 +3664,122 @@ var Orchestrator = class {
|
|
|
3303
3664
|
- \u6307\u6807: total=${metrics.total}, success=${metrics.success}, failed=${metrics.failed}, timeout=${metrics.timeout}, cancelled=${metrics.cancelled}, rate_limited=${metrics.rateLimited}
|
|
3304
3665
|
- \u5E73\u5747\u8017\u65F6: queue=${metrics.avgQueueMs}ms, exec=${metrics.avgExecMs}ms, send=${metrics.avgSendMs}ms
|
|
3305
3666
|
- \u9650\u6D41\u5E76\u53D1: global=${limiter.activeGlobal}, users=${limiter.activeUsers}, rooms=${limiter.activeRooms}
|
|
3306
|
-
- CLI runtime: workers=${runtime.workerCount}, running=${runtime.runningCount}, compat_mode=${this.cliCompat.enabled ? "on" : "off"}
|
|
3667
|
+
- CLI runtime: workers=${runtime.workerCount}, running=${runtime.runningCount}, compat_mode=${this.cliCompat.enabled ? "on" : "off"}
|
|
3668
|
+
- Multi-Agent workflow: enabled=${this.workflowRunner.isEnabled() ? "on" : "off"}, state=${workflow.state}`
|
|
3307
3669
|
);
|
|
3308
3670
|
}
|
|
3671
|
+
async handleWorkflowStatusCommand(sessionKey, message) {
|
|
3672
|
+
const snapshot = this.workflowSnapshots.get(sessionKey) ?? createIdleWorkflowSnapshot();
|
|
3673
|
+
await this.channel.sendNotice(
|
|
3674
|
+
message.conversationId,
|
|
3675
|
+
`[CodeHarbor] Multi-Agent \u5DE5\u4F5C\u6D41\u72B6\u6001
|
|
3676
|
+
- state: ${snapshot.state}
|
|
3677
|
+
- startedAt: ${snapshot.startedAt ?? "N/A"}
|
|
3678
|
+
- endedAt: ${snapshot.endedAt ?? "N/A"}
|
|
3679
|
+
- objective: ${snapshot.objective ?? "N/A"}
|
|
3680
|
+
- approved: ${snapshot.approved === null ? "N/A" : snapshot.approved ? "yes" : "no"}
|
|
3681
|
+
- repairRounds: ${snapshot.repairRounds}
|
|
3682
|
+
- error: ${snapshot.error ?? "N/A"}`
|
|
3683
|
+
);
|
|
3684
|
+
}
|
|
3685
|
+
async handleWorkflowRunCommand(objective, sessionKey, message, requestId, workdir) {
|
|
3686
|
+
const normalizedObjective = objective.trim();
|
|
3687
|
+
if (!normalizedObjective) {
|
|
3688
|
+
await this.channel.sendNotice(message.conversationId, "[CodeHarbor] /agents run \u9700\u8981\u63D0\u4F9B\u4EFB\u52A1\u76EE\u6807\u3002");
|
|
3689
|
+
return;
|
|
3690
|
+
}
|
|
3691
|
+
const requestStartedAt = Date.now();
|
|
3692
|
+
let progressNoticeEventId = null;
|
|
3693
|
+
const progressCtx = {
|
|
3694
|
+
conversationId: message.conversationId,
|
|
3695
|
+
isDirectMessage: message.isDirectMessage,
|
|
3696
|
+
getProgressNoticeEventId: () => progressNoticeEventId,
|
|
3697
|
+
setProgressNoticeEventId: (next) => {
|
|
3698
|
+
progressNoticeEventId = next;
|
|
3699
|
+
}
|
|
3700
|
+
};
|
|
3701
|
+
const startedAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
3702
|
+
this.workflowSnapshots.set(sessionKey, {
|
|
3703
|
+
state: "running",
|
|
3704
|
+
startedAt: startedAtIso,
|
|
3705
|
+
endedAt: null,
|
|
3706
|
+
objective: normalizedObjective,
|
|
3707
|
+
approved: null,
|
|
3708
|
+
repairRounds: 0,
|
|
3709
|
+
error: null
|
|
3710
|
+
});
|
|
3711
|
+
const stopTyping = this.startTypingHeartbeat(message.conversationId);
|
|
3712
|
+
let cancelWorkflow = () => {
|
|
3713
|
+
};
|
|
3714
|
+
let cancelRequested = false;
|
|
3715
|
+
this.runningExecutions.set(sessionKey, {
|
|
3716
|
+
requestId,
|
|
3717
|
+
startedAt: requestStartedAt,
|
|
3718
|
+
cancel: () => {
|
|
3719
|
+
cancelRequested = true;
|
|
3720
|
+
cancelWorkflow();
|
|
3721
|
+
}
|
|
3722
|
+
});
|
|
3723
|
+
await this.sendProgressUpdate(progressCtx, "[CodeHarbor] Multi-Agent workflow \u542F\u52A8\uFF1APlanner -> Executor -> Reviewer");
|
|
3724
|
+
try {
|
|
3725
|
+
const result = await this.workflowRunner.run({
|
|
3726
|
+
objective: normalizedObjective,
|
|
3727
|
+
workdir,
|
|
3728
|
+
onRegisterCancel: (cancel) => {
|
|
3729
|
+
cancelWorkflow = cancel;
|
|
3730
|
+
if (cancelRequested) {
|
|
3731
|
+
cancelWorkflow();
|
|
3732
|
+
}
|
|
3733
|
+
},
|
|
3734
|
+
onProgress: async (event) => {
|
|
3735
|
+
const stepLabel = event.stage.toUpperCase();
|
|
3736
|
+
await this.sendProgressUpdate(progressCtx, `[CodeHarbor] [${stepLabel}] ${event.message}`);
|
|
3737
|
+
}
|
|
3738
|
+
});
|
|
3739
|
+
const endedAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
3740
|
+
this.workflowSnapshots.set(sessionKey, {
|
|
3741
|
+
state: "succeeded",
|
|
3742
|
+
startedAt: startedAtIso,
|
|
3743
|
+
endedAt: endedAtIso,
|
|
3744
|
+
objective: normalizedObjective,
|
|
3745
|
+
approved: result.approved,
|
|
3746
|
+
repairRounds: result.repairRounds,
|
|
3747
|
+
error: null
|
|
3748
|
+
});
|
|
3749
|
+
await this.channel.sendMessage(message.conversationId, buildWorkflowResultReply(result));
|
|
3750
|
+
await this.finishProgress(progressCtx, `\u591A\u667A\u80FD\u4F53\u6D41\u7A0B\u5B8C\u6210\uFF08\u8017\u65F6 ${formatDurationMs(Date.now() - requestStartedAt)}\uFF09`);
|
|
3751
|
+
} catch (error) {
|
|
3752
|
+
const status = classifyExecutionOutcome(error);
|
|
3753
|
+
const endedAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
3754
|
+
this.workflowSnapshots.set(sessionKey, {
|
|
3755
|
+
state: status === "cancelled" ? "idle" : "failed",
|
|
3756
|
+
startedAt: startedAtIso,
|
|
3757
|
+
endedAt: endedAtIso,
|
|
3758
|
+
objective: normalizedObjective,
|
|
3759
|
+
approved: null,
|
|
3760
|
+
repairRounds: 0,
|
|
3761
|
+
error: formatError2(error)
|
|
3762
|
+
});
|
|
3763
|
+
await this.finishProgress(progressCtx, buildFailureProgressSummary(status, requestStartedAt, error));
|
|
3764
|
+
throw error;
|
|
3765
|
+
} finally {
|
|
3766
|
+
const running = this.runningExecutions.get(sessionKey);
|
|
3767
|
+
if (running?.requestId === requestId) {
|
|
3768
|
+
this.runningExecutions.delete(sessionKey);
|
|
3769
|
+
}
|
|
3770
|
+
await stopTyping();
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
async sendWorkflowFailure(conversationId, error) {
|
|
3774
|
+
const startedAt = Date.now();
|
|
3775
|
+
const status = classifyExecutionOutcome(error);
|
|
3776
|
+
if (status === "cancelled") {
|
|
3777
|
+
await this.channel.sendNotice(conversationId, "[CodeHarbor] Multi-Agent workflow \u5DF2\u53D6\u6D88\u3002");
|
|
3778
|
+
return Date.now() - startedAt;
|
|
3779
|
+
}
|
|
3780
|
+
await this.channel.sendMessage(conversationId, `[CodeHarbor] Multi-Agent workflow \u5931\u8D25: ${formatError2(error)}`);
|
|
3781
|
+
return Date.now() - startedAt;
|
|
3782
|
+
}
|
|
3309
3783
|
async handleStopCommand(sessionKey, message, requestId) {
|
|
3310
3784
|
this.stateStore.deactivateSession(sessionKey);
|
|
3311
3785
|
this.stateStore.clearCodexSessionId(sessionKey);
|
|
@@ -3619,6 +4093,25 @@ function buildFailureProgressSummary(status, startedAt, error) {
|
|
|
3619
4093
|
}
|
|
3620
4094
|
return `\u5904\u7406\u5931\u8D25\uFF08\u8017\u65F6 ${elapsed}\uFF09: ${formatError2(error)}`;
|
|
3621
4095
|
}
|
|
4096
|
+
function buildWorkflowResultReply(result) {
|
|
4097
|
+
return `[CodeHarbor] Multi-Agent workflow \u5B8C\u6210
|
|
4098
|
+
- objective: ${result.objective}
|
|
4099
|
+
- approved: ${result.approved ? "yes" : "no"}
|
|
4100
|
+
- repairRounds: ${result.repairRounds}
|
|
4101
|
+
- duration: ${formatDurationMs(result.durationMs)}
|
|
4102
|
+
|
|
4103
|
+
[planner]
|
|
4104
|
+
${result.plan}
|
|
4105
|
+
[/planner]
|
|
4106
|
+
|
|
4107
|
+
[executor]
|
|
4108
|
+
${result.output}
|
|
4109
|
+
[/executor]
|
|
4110
|
+
|
|
4111
|
+
[reviewer]
|
|
4112
|
+
${result.review}
|
|
4113
|
+
[/reviewer]`;
|
|
4114
|
+
}
|
|
3622
4115
|
|
|
3623
4116
|
// src/store/state-store.ts
|
|
3624
4117
|
var import_node_fs5 = __toESM(require("fs"));
|
|
@@ -4061,6 +4554,7 @@ var CodeHarborApp = class {
|
|
|
4061
4554
|
roomTriggerPolicies: config.roomTriggerPolicies,
|
|
4062
4555
|
rateLimiterOptions: config.rateLimiter,
|
|
4063
4556
|
cliCompat: config.cliCompat,
|
|
4557
|
+
multiAgentWorkflow: config.agentWorkflow,
|
|
4064
4558
|
configService: this.configService,
|
|
4065
4559
|
defaultCodexWorkdir: config.codexWorkdir
|
|
4066
4560
|
});
|
|
@@ -4185,6 +4679,8 @@ var configSchema = import_zod.z.object({
|
|
|
4185
4679
|
CODEX_APPROVAL_POLICY: import_zod.z.string().optional(),
|
|
4186
4680
|
CODEX_EXTRA_ARGS: import_zod.z.string().default(""),
|
|
4187
4681
|
CODEX_EXTRA_ENV_JSON: import_zod.z.string().default(""),
|
|
4682
|
+
AGENT_WORKFLOW_ENABLED: import_zod.z.string().default("false").transform((v) => v.toLowerCase() === "true"),
|
|
4683
|
+
AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS: import_zod.z.string().default("1").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().min(0).max(10)),
|
|
4188
4684
|
STATE_DB_PATH: import_zod.z.string().default("data/state.db"),
|
|
4189
4685
|
STATE_PATH: import_zod.z.string().default("data/state.json"),
|
|
4190
4686
|
MAX_PROCESSED_EVENTS_PER_SESSION: import_zod.z.string().default("200").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
@@ -4234,6 +4730,10 @@ var configSchema = import_zod.z.object({
|
|
|
4234
4730
|
codexApprovalPolicy: v.CODEX_APPROVAL_POLICY?.trim() || null,
|
|
4235
4731
|
codexExtraArgs: parseExtraArgs(v.CODEX_EXTRA_ARGS),
|
|
4236
4732
|
codexExtraEnv: parseExtraEnv(v.CODEX_EXTRA_ENV_JSON),
|
|
4733
|
+
agentWorkflow: {
|
|
4734
|
+
enabled: v.AGENT_WORKFLOW_ENABLED,
|
|
4735
|
+
autoRepairMaxRounds: v.AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS
|
|
4736
|
+
},
|
|
4237
4737
|
stateDbPath: import_node_path7.default.resolve(v.STATE_DB_PATH),
|
|
4238
4738
|
legacyStateJsonPath: v.STATE_PATH.trim() ? import_node_path7.default.resolve(v.STATE_PATH) : null,
|
|
4239
4739
|
maxProcessedEventsPerSession: v.MAX_PROCESSED_EVENTS_PER_SESSION,
|
|
@@ -4382,6 +4882,8 @@ var CONFIG_SNAPSHOT_ENV_KEYS = [
|
|
|
4382
4882
|
"CODEX_APPROVAL_POLICY",
|
|
4383
4883
|
"CODEX_EXTRA_ARGS",
|
|
4384
4884
|
"CODEX_EXTRA_ENV_JSON",
|
|
4885
|
+
"AGENT_WORKFLOW_ENABLED",
|
|
4886
|
+
"AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS",
|
|
4385
4887
|
"STATE_DB_PATH",
|
|
4386
4888
|
"STATE_PATH",
|
|
4387
4889
|
"MAX_PROCESSED_EVENTS_PER_SESSION",
|
|
@@ -4444,6 +4946,8 @@ var envSnapshotSchema = import_zod2.z.object({
|
|
|
4444
4946
|
CODEX_APPROVAL_POLICY: import_zod2.z.string(),
|
|
4445
4947
|
CODEX_EXTRA_ARGS: import_zod2.z.string(),
|
|
4446
4948
|
CODEX_EXTRA_ENV_JSON: jsonObjectStringSchema("CODEX_EXTRA_ENV_JSON", true),
|
|
4949
|
+
AGENT_WORKFLOW_ENABLED: booleanStringSchema("AGENT_WORKFLOW_ENABLED"),
|
|
4950
|
+
AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS: integerStringSchema("AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS", 0, 10),
|
|
4447
4951
|
STATE_DB_PATH: import_zod2.z.string().min(1),
|
|
4448
4952
|
STATE_PATH: import_zod2.z.string(),
|
|
4449
4953
|
MAX_PROCESSED_EVENTS_PER_SESSION: integerStringSchema("MAX_PROCESSED_EVENTS_PER_SESSION", 1),
|
|
@@ -4630,6 +5134,8 @@ function buildSnapshotEnv(config) {
|
|
|
4630
5134
|
CODEX_APPROVAL_POLICY: config.codexApprovalPolicy ?? "",
|
|
4631
5135
|
CODEX_EXTRA_ARGS: config.codexExtraArgs.join(" "),
|
|
4632
5136
|
CODEX_EXTRA_ENV_JSON: serializeJsonObject(config.codexExtraEnv),
|
|
5137
|
+
AGENT_WORKFLOW_ENABLED: String(config.agentWorkflow.enabled),
|
|
5138
|
+
AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS: String(config.agentWorkflow.autoRepairMaxRounds),
|
|
4633
5139
|
STATE_DB_PATH: config.stateDbPath,
|
|
4634
5140
|
STATE_PATH: config.legacyStateJsonPath ?? "",
|
|
4635
5141
|
MAX_PROCESSED_EVENTS_PER_SESSION: String(config.maxProcessedEventsPerSession),
|
|
@@ -4988,7 +5494,7 @@ function buildMainServiceUnit(options) {
|
|
|
4988
5494
|
"NoNewPrivileges=true",
|
|
4989
5495
|
"PrivateTmp=true",
|
|
4990
5496
|
"ProtectSystem=full",
|
|
4991
|
-
"ProtectHome=
|
|
5497
|
+
"ProtectHome=false",
|
|
4992
5498
|
`ReadWritePaths=${runtimeHome2}`,
|
|
4993
5499
|
"",
|
|
4994
5500
|
"[Install]",
|
|
@@ -5016,7 +5522,7 @@ function buildAdminServiceUnit(options) {
|
|
|
5016
5522
|
"NoNewPrivileges=true",
|
|
5017
5523
|
"PrivateTmp=true",
|
|
5018
5524
|
"ProtectSystem=full",
|
|
5019
|
-
"ProtectHome=
|
|
5525
|
+
"ProtectHome=false",
|
|
5020
5526
|
`ReadWritePaths=${runtimeHome2}`,
|
|
5021
5527
|
"",
|
|
5022
5528
|
"[Install]",
|
|
@@ -5302,6 +5808,7 @@ configCommand.command("import").description("Import config snapshot from JSON").
|
|
|
5302
5808
|
});
|
|
5303
5809
|
serviceCommand.command("install").description("Install and enable codeharbor systemd service (requires root)").option("--run-user <user>", "service user (default: sudo user or current user)").option("--runtime-home <path>", "runtime home used as CODEHARBOR_HOME").option("--with-admin", "also install codeharbor-admin.service").option("--no-start", "enable service without starting immediately").action((options) => {
|
|
5304
5810
|
try {
|
|
5811
|
+
maybeReexecServiceCommandWithSudo();
|
|
5305
5812
|
const runUser = options.runUser?.trim() || resolveDefaultRunUser();
|
|
5306
5813
|
const runtimeHomePath = resolveRuntimeHomeForUser(runUser, process.env, options.runtimeHome);
|
|
5307
5814
|
installSystemdServices({
|
|
@@ -5317,9 +5824,10 @@ serviceCommand.command("install").description("Install and enable codeharbor sys
|
|
|
5317
5824
|
`);
|
|
5318
5825
|
process.stderr.write(
|
|
5319
5826
|
[
|
|
5320
|
-
"Hint:
|
|
5321
|
-
|
|
5322
|
-
"
|
|
5827
|
+
"Hint:",
|
|
5828
|
+
" - Run directly: codeharbor service install --with-admin",
|
|
5829
|
+
" - The command auto-elevates with sudo when needed.",
|
|
5830
|
+
` - Fallback explicit form: ${buildExplicitSudoCommand("service install --with-admin")}`,
|
|
5323
5831
|
""
|
|
5324
5832
|
].join("\n")
|
|
5325
5833
|
);
|
|
@@ -5328,6 +5836,7 @@ serviceCommand.command("install").description("Install and enable codeharbor sys
|
|
|
5328
5836
|
});
|
|
5329
5837
|
serviceCommand.command("uninstall").description("Remove codeharbor systemd service (requires root)").option("--with-admin", "also remove codeharbor-admin.service").action((options) => {
|
|
5330
5838
|
try {
|
|
5839
|
+
maybeReexecServiceCommandWithSudo();
|
|
5331
5840
|
uninstallSystemdServices({
|
|
5332
5841
|
removeAdmin: options.withAdmin ?? false
|
|
5333
5842
|
});
|
|
@@ -5336,8 +5845,10 @@ serviceCommand.command("uninstall").description("Remove codeharbor systemd servi
|
|
|
5336
5845
|
`);
|
|
5337
5846
|
process.stderr.write(
|
|
5338
5847
|
[
|
|
5339
|
-
"Hint:
|
|
5340
|
-
|
|
5848
|
+
"Hint:",
|
|
5849
|
+
" - Run directly: codeharbor service uninstall --with-admin",
|
|
5850
|
+
" - The command auto-elevates with sudo when needed.",
|
|
5851
|
+
` - Fallback explicit form: ${buildExplicitSudoCommand("service uninstall --with-admin")}`,
|
|
5341
5852
|
""
|
|
5342
5853
|
].join("\n")
|
|
5343
5854
|
);
|
|
@@ -5346,6 +5857,7 @@ serviceCommand.command("uninstall").description("Remove codeharbor systemd servi
|
|
|
5346
5857
|
});
|
|
5347
5858
|
serviceCommand.command("restart").description("Restart installed codeharbor systemd service (requires root)").option("--with-admin", "also restart codeharbor-admin.service").action((options) => {
|
|
5348
5859
|
try {
|
|
5860
|
+
maybeReexecServiceCommandWithSudo();
|
|
5349
5861
|
restartSystemdServices({
|
|
5350
5862
|
restartAdmin: options.withAdmin ?? false
|
|
5351
5863
|
});
|
|
@@ -5354,9 +5866,10 @@ serviceCommand.command("restart").description("Restart installed codeharbor syst
|
|
|
5354
5866
|
`);
|
|
5355
5867
|
process.stderr.write(
|
|
5356
5868
|
[
|
|
5357
|
-
"Hint:
|
|
5358
|
-
|
|
5359
|
-
"
|
|
5869
|
+
"Hint:",
|
|
5870
|
+
" - Run directly: codeharbor service restart --with-admin",
|
|
5871
|
+
" - The command auto-elevates with sudo when needed.",
|
|
5872
|
+
` - Fallback explicit form: ${buildExplicitSudoCommand("service restart --with-admin")}`,
|
|
5360
5873
|
""
|
|
5361
5874
|
].join("\n")
|
|
5362
5875
|
);
|
|
@@ -5439,6 +5952,32 @@ function resolveCliScriptPath() {
|
|
|
5439
5952
|
}
|
|
5440
5953
|
return import_node_path12.default.resolve(__dirname, "cli.js");
|
|
5441
5954
|
}
|
|
5955
|
+
function maybeReexecServiceCommandWithSudo() {
|
|
5956
|
+
if (typeof process.getuid !== "function" || process.getuid() === 0) {
|
|
5957
|
+
return;
|
|
5958
|
+
}
|
|
5959
|
+
const serviceArgs = process.argv.slice(2);
|
|
5960
|
+
if (serviceArgs.length === 0 || serviceArgs[0] !== "service") {
|
|
5961
|
+
return;
|
|
5962
|
+
}
|
|
5963
|
+
const cliScriptPath = resolveCliScriptPath();
|
|
5964
|
+
const child = (0, import_node_child_process6.spawnSync)("sudo", [process.execPath, cliScriptPath, ...serviceArgs], {
|
|
5965
|
+
stdio: "inherit"
|
|
5966
|
+
});
|
|
5967
|
+
if (child.error) {
|
|
5968
|
+
throw new Error(`failed to auto-elevate with sudo: ${child.error.message}`);
|
|
5969
|
+
}
|
|
5970
|
+
process.exit(child.status ?? 1);
|
|
5971
|
+
}
|
|
5972
|
+
function shellQuote(value) {
|
|
5973
|
+
if (!value) {
|
|
5974
|
+
return "''";
|
|
5975
|
+
}
|
|
5976
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
5977
|
+
}
|
|
5978
|
+
function buildExplicitSudoCommand(subcommand) {
|
|
5979
|
+
return `sudo ${shellQuote(process.execPath)} ${shellQuote(resolveCliScriptPath())} ${subcommand}`;
|
|
5980
|
+
}
|
|
5442
5981
|
function formatError3(error) {
|
|
5443
5982
|
if (error instanceof Error) {
|
|
5444
5983
|
return error.message;
|